diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fb0b516a..111b0222 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -28,7 +28,7 @@ jobs: run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.24 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index c3db3738..f2129300 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v5 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '22' diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index d0ca1685..4a574d91 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -14,7 +14,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: days-before-stale: 14 days-before-close: 14 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12316cd7..7d22c300 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: '22' diff --git a/README.md b/README.md index 287f5e20..1d21b8c8 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,24 @@ _Pangolin tunnels your services to the internet so you can access anything from Website | - - Install Guide + + Quick Install Guide | - + Contact Us + | + + Slack + + | + + Discord + +[![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://digpangolin.com/slack) [![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) ![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) [![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) diff --git a/blueprint.py b/blueprint.py new file mode 100644 index 00000000..b4f1a99c --- /dev/null +++ b/blueprint.py @@ -0,0 +1,72 @@ +import requests +import yaml +import json +import base64 + +# The file path for the YAML file to be read +# You can change this to the path of your YAML file +YAML_FILE_PATH = 'blueprint.yaml' + +# The API endpoint and headers from the curl request +API_URL = 'http://localhost:3004/v1/org/test/blueprint' +HEADERS = { + 'accept': '*/*', + 'Authorization': 'Bearer v7ix7xha1bmq2on.tzsden374mtmkeczm3tx44uzxsljnrst7nmg7ccr', + 'Content-Type': 'application/json' +} + +def convert_and_send(file_path, url, headers): + """ + Reads a YAML file, converts its content to a JSON payload, + and sends it via a PUT request to a specified URL. + """ + try: + # Read the YAML file content + with open(file_path, 'r') as file: + yaml_content = file.read() + + # Parse the YAML string to a Python dictionary + # This will be used to ensure the YAML is valid before sending + parsed_yaml = yaml.safe_load(yaml_content) + + # convert the parsed YAML to a JSON string + json_payload = json.dumps(parsed_yaml) + print("Converted JSON payload:") + print(json_payload) + + # Encode the JSON string to Base64 + encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8') + + # Create the final payload with the base64 encoded data + final_payload = { + "blueprint": encoded_json + } + + print("Sending the following Base64 encoded JSON payload:") + print(final_payload) + print("-" * 20) + + # Make the PUT request with the base64 encoded payload + response = requests.put(url, headers=headers, json=final_payload) + + # Print the API response for debugging + print(f"API Response Status Code: {response.status_code}") + print("API Response Content:") + print(response.text) + + # Raise an exception for bad status codes (4xx or 5xx) + response.raise_for_status() + + except FileNotFoundError: + print(f"Error: The file '{file_path}' was not found.") + except yaml.YAMLError as e: + print(f"Error parsing YAML file: {e}") + except requests.exceptions.RequestException as e: + print(f"An error occurred during the API request: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + +# Run the function +if __name__ == "__main__": + convert_and_send(YAML_FILE_PATH, API_URL, HEADERS) + diff --git a/blueprint.yaml b/blueprint.yaml new file mode 100644 index 00000000..03c51521 --- /dev/null +++ b/blueprint.yaml @@ -0,0 +1,69 @@ +client-resources: + client-resource-nice-id-uno: + name: this is my resource + protocol: tcp + proxy-port: 3001 + hostname: localhost + internal-port: 3000 + site: lively-yosemite-toad + client-resource-nice-id-duce: + name: this is my resource + protocol: udp + proxy-port: 3000 + hostname: localhost + internal-port: 3000 + site: lively-yosemite-toad + +proxy-resources: + resource-nice-id-uno: + name: this is my resource + protocol: http + full-domain: duce.test.example.com + host-header: example.com + tls-server-name: example.com + # auth: + # pincode: 123456 + # password: sadfasdfadsf + # sso-enabled: true + # sso-roles: + # - Member + # sso-users: + # - owen@fossorial.io + # whitelist-users: + # - owen@fossorial.io + headers: + - name: X-Example-Header + value: example-value + - name: X-Another-Header + value: another-value + rules: + - action: allow + match: ip + value: 1.1.1.1 + - action: deny + match: cidr + value: 2.2.2.2/32 + - action: pass + match: path + value: /admin + targets: + - site: lively-yosemite-toad + path: /path + pathMatchType: prefix + hostname: localhost + method: http + port: 8000 + - site: slim-alpine-chipmunk + hostname: localhost + path: /yoman + pathMatchType: exact + method: http + port: 8001 + resource-nice-id-duce: + name: this is other resource + protocol: tcp + proxy-port: 3000 + targets: + - site: lively-yosemite-toad + hostname: localhost + port: 3000 \ No newline at end of file diff --git a/config/traefik/dynamic_config.yml b/config/traefik/dynamic_config.yml index 8fcf8e55..8465a9cf 100644 --- a/config/traefik/dynamic_config.yml +++ b/config/traefik/dynamic_config.yml @@ -16,8 +16,9 @@ http: # Next.js router (handles everything except API and WebSocket paths) next-router: - rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)" + rule: "Host(`{{.DashboardDomain}}`)" service: next-service + priority: 10 entryPoints: - websecure tls: @@ -27,15 +28,7 @@ http: api-router: rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)" service: api-service - entryPoints: - - websecure - tls: - certResolver: letsencrypt - - # WebSocket router - ws-router: - rule: "Host(`{{.DashboardDomain}}`)" - service: api-service + priority: 100 entryPoints: - websecure tls: diff --git a/docker-compose.yml b/docker-compose.yml index 09b150d7..469f9b4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,6 @@ services: environment: - NODE_ENV=development - ENVIRONMENT=dev - - DB_TYPE=pg volumes: # Mount source code for hot reload - ./src:/app/src diff --git a/esbuild.mjs b/esbuild.mjs index d76c0753..8086a77e 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -52,7 +52,7 @@ esbuild bundle: true, outfile: argv.out, format: "esm", - minify: true, + minify: false, banner: { js: banner, }, @@ -63,7 +63,7 @@ esbuild packagePath: getPackagePaths(), }), ], - sourcemap: "external", + sourcemap: "inline", target: "node22", }) .then(() => { diff --git a/install/go.mod b/install/go.mod index b12f9ef4..b1465ac5 100644 --- a/install/go.mod +++ b/install/go.mod @@ -1,10 +1,10 @@ module installer -go 1.24 +go 1.24.0 require ( - golang.org/x/term v0.34.0 + golang.org/x/term v0.35.0 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.35.0 // indirect +require golang.org/x/sys v0.36.0 // indirect diff --git a/install/go.sum b/install/go.sum index 320762f0..789a291b 100644 --- a/install/go.sum +++ b/install/go.sum @@ -1,7 +1,7 @@ -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/install/main.go b/install/main.go index 1d684b51..1f7213a1 100644 --- a/install/main.go +++ b/install/main.go @@ -8,6 +8,8 @@ import ( "io" "io/fs" "math/rand" + "net" + "net/http" "os" "os/exec" "path/filepath" @@ -15,14 +17,13 @@ import ( "strings" "text/template" "time" - "net" ) // DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD func loadVersions(config *Config) { - config.PangolinVersion = "replaceme" - config.GerbilVersion = "replaceme" - config.BadgerVersion = "replaceme" + config.PangolinVersion = "1.9.4" + config.GerbilVersion = "1.2.1" + config.BadgerVersion = "1.2.0" } //go:embed config/* @@ -74,7 +75,7 @@ func main() { if err := checkPortsAvailable(p); err != nil { fmt.Fprintln(os.Stderr, err) - fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly") + fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly. If you already have the Pangolin stack running, shut them down before proceeding.\n") os.Exit(1) } } @@ -126,7 +127,7 @@ func main() { if readBool(reader, "Would you like to install and start the containers?", true) { config.InstallationContainerType = podmanOrDocker(reader) - + if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { if readBool(reader, "Docker is not installed. Would you like to install it?", true) { installDocker() @@ -204,8 +205,17 @@ func main() { } } + config.InstallationContainerType = podmanOrDocker(reader) + config.DoCrowdsecInstall = true - installCrowdsec(config) + err := installCrowdsec(config) + if (err != nil) { + fmt.Printf("Error installing CrowdSec: %v\n", err) + return + } + + fmt.Println("CrowdSec installed successfully!") + return } } } @@ -322,13 +332,18 @@ func collectUserInput(reader *bufio.Reader) Config { if config.HybridMode { alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) - + if alreadyHaveCreds { config.HybridId = readString(reader, "Enter your ID", "") config.HybridSecret = readString(reader, "Enter your secret", "") - } + } - config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "") + // Try to get public IP as default + publicIP := getPublicIP() + if publicIP != "" { + fmt.Printf("Detected public IP: %s\n", publicIP) + } + config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP) config.InstallGerbil = true } else { config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") @@ -345,7 +360,7 @@ func collectUserInput(reader *bufio.Reader) Config { // Email configuration fmt.Println("\n=== Email Configuration ===") config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) - + if config.EnableEmail { config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) @@ -353,7 +368,7 @@ func collectUserInput(reader *bufio.Reader) Config { config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } - + // Validate required fields if config.BaseDomain == "" { fmt.Println("Error: Domain name is required") @@ -584,6 +599,32 @@ func generateRandomSecretKey() string { return string(b) } +func getPublicIP() string { + client := &http.Client{ + Timeout: 10 * time.Second, + } + + resp, err := client.Get("https://ifconfig.io/ip") + if err != nil { + return "" + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "" + } + + ip := strings.TrimSpace(string(body)) + + // Validate that it's a valid IP address + if net.ParseIP(ip) != nil { + return ip + } + + return "" +} + // Run external commands with stdio/stderr attached. func run(name string, args ...string) error { cmd := exec.Command(name, args...) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index d17c99f3..f617a768 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "An error occurred while adding user to the role.", "userSaved": "User saved", "userSavedDescription": "The user has been updated.", + "autoProvisioned": "Auto Provisioned", + "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", "roles": "Roles", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Invalid IP address format", "ipAddressErrorInvalidOctet": "Invalid IP address octet", "path": "Path", + "matchPath": "Match Path", "ipAddressRange": "IP Range", "rulesErrorFetch": "Failed to fetch rules", "rulesErrorFetchDescription": "An error occurred while fetching rules", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Connected", "idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.", "idpErrorNotFound": "IdP not found", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Invalid Invite", "inviteInvalidDescription": "The invite link is invalid.", "inviteErrorWrongUser": "Invite is not for this user", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Professional Edition Required", "licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.", "actionGetOrg": "Get Organization", + "updateOrgUser": "Update Org User", + "createOrgUser": "Create Org User", "actionUpdateOrg": "Update Organization", "actionUpdateUser": "Update User", "actionGetUser": "Get User", @@ -991,6 +998,7 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "actionApplyBlueprint": "Apply Blueprint", "setupToken": "Setup Token", "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", @@ -1133,8 +1141,8 @@ "sidebarLicense": "License", "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", - "enableDockerSocket": "Enable Docker Socket", - "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", + "enableDockerSocket": "Enable Docker Blueprint", + "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketLink": "Learn More", "viewDockerContainers": "View Docker Containers", "containersIn": "Containers in {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", "domainPickerEnterDomain": "Domain", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Enter the full domain of the resource to see available options.", "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", "domainPickerTabAll": "All", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocol", "editInternalResourceDialogSitePort": "Site Port", "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", "editInternalResourceDialogCancel": "Cancel", "editInternalResourceDialogSaveResource": "Save Resource", "editInternalResourceDialogSuccess": "Success", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Site Port", "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.", "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", "createInternalResourceDialogCancel": "Cancel", "createInternalResourceDialogCreateResource": "Create Resource", @@ -1496,5 +1500,24 @@ "convertButton": "Convert This Node to Managed Self-Hosted" }, "internationaldomaindetected": "International Domain Detected", - "willbestoredas": "Will be stored as:" + "willbestoredas": "Will be stored as:", + "idpGoogleDescription": "Google OAuth2/OIDC provider", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Custom Headers", + "headersValidationError": "Headers must be in the format: Header-Name: value.", + "domainPickerProvidedDomain": "Provided Domain", + "domainPickerFreeProvidedDomain": "Free Provided Domain", + "domainPickerVerified": "Verified", + "domainPickerUnverified": "Unverified", + "domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.", + "domainPickerError": "Error", + "domainPickerErrorLoadDomains": "Failed to load organization domains", + "domainPickerErrorCheckAvailability": "Failed to check domain availability", + "domainPickerInvalidSubdomain": "Invalid subdomain", + "domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", + "domainPickerSubdomainSanitized": "Subdomain sanitized", + "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Edit file: docker-compose.yml" } diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 727d9a5e..7b391431 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "An error occurred while adding user to the role.", "userSaved": "User saved", "userSavedDescription": "The user has been updated.", + "autoProvisioned": "Auto Provisioned", + "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", "roles": "Roles", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Invalid IP address format", "ipAddressErrorInvalidOctet": "Invalid IP address octet", "path": "Path", + "matchPath": "Match Path", "ipAddressRange": "IP Range", "rulesErrorFetch": "Failed to fetch rules", "rulesErrorFetchDescription": "An error occurred while fetching rules", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Connected", "idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.", "idpErrorNotFound": "IdP not found", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Invalid Invite", "inviteInvalidDescription": "The invite link is invalid.", "inviteErrorWrongUser": "Invite is not for this user", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Professional Edition Required", "licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.", "actionGetOrg": "Get Organization", + "updateOrgUser": "Update Org User", + "createOrgUser": "Create Org User", "actionUpdateOrg": "Update Organization", "actionUpdateUser": "Update User", "actionGetUser": "Get User", @@ -991,6 +998,7 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "actionApplyBlueprint": "Apply Blueprint", "setupToken": "Setup Token", "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", @@ -1133,8 +1141,8 @@ "sidebarLicense": "License", "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", - "enableDockerSocket": "Enable Docker Socket", - "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", + "enableDockerSocket": "Enable Docker Blueprint", + "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketLink": "Learn More", "viewDockerContainers": "View Docker Containers", "containersIn": "Containers in {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", "domainPickerEnterDomain": "Domain", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Enter the full domain of the resource to see available options.", "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", "domainPickerTabAll": "All", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocol", "editInternalResourceDialogSitePort": "Site Port", "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", "editInternalResourceDialogCancel": "Cancel", "editInternalResourceDialogSaveResource": "Save Resource", "editInternalResourceDialogSuccess": "Success", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Site Port", "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.", "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", "createInternalResourceDialogCancel": "Cancel", "createInternalResourceDialogCreateResource": "Create Resource", @@ -1496,5 +1500,24 @@ "convertButton": "Convert This Node to Managed Self-Hosted" }, "internationaldomaindetected": "International Domain Detected", - "willbestoredas": "Will be stored as:" + "willbestoredas": "Will be stored as:", + "idpGoogleDescription": "Google OAuth2/OIDC provider", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Custom Headers", + "headersValidationError": "Headers must be in the format: Header-Name: value.", + "domainPickerProvidedDomain": "Provided Domain", + "domainPickerFreeProvidedDomain": "Free Provided Domain", + "domainPickerVerified": "Verified", + "domainPickerUnverified": "Unverified", + "domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.", + "domainPickerError": "Error", + "domainPickerErrorLoadDomains": "Failed to load organization domains", + "domainPickerErrorCheckAvailability": "Failed to check domain availability", + "domainPickerInvalidSubdomain": "Invalid subdomain", + "domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", + "domainPickerSubdomainSanitized": "Subdomain sanitized", + "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Edit file: docker-compose.yml" } diff --git a/messages/de-DE.json b/messages/de-DE.json index 54d14c8d..099bcff9 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Beim Hinzufügen des Benutzers zur Rolle ist ein Fehler aufgetreten.", "userSaved": "Benutzer gespeichert", "userSavedDescription": "Der Benutzer wurde aktualisiert.", + "autoProvisioned": "Automatisch vorgesehen", + "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", "roles": "Rollen", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Ungültiges IP-Adressformat", "ipAddressErrorInvalidOctet": "Ungültiges IP-Adress-Oktett", "path": "Pfad", + "matchPath": "Unterverzeichnis", "ipAddressRange": "IP-Bereich", "rulesErrorFetch": "Fehler beim Abrufen der Regeln", "rulesErrorFetchDescription": "Beim Abrufen der Regeln ist ein Fehler aufgetreten", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Verbunden", "idpErrorConnectingTo": "Es gab ein Problem bei der Verbindung zu {name}. Bitte kontaktieren Sie Ihren Administrator.", "idpErrorNotFound": "IdP nicht gefunden", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Ungültige Einladung", "inviteInvalidDescription": "Der Einladungslink ist ungültig.", "inviteErrorWrongUser": "Einladung ist nicht für diesen Benutzer", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Professional Edition erforderlich", "licenseTierProfessionalRequiredDescription": "Diese Funktion ist nur in der Professional Edition verfügbar.", "actionGetOrg": "Organisation abrufen", + "updateOrgUser": "Org Benutzer aktualisieren", + "createOrgUser": "Org Benutzer erstellen", "actionUpdateOrg": "Organisation aktualisieren", "actionUpdateUser": "Benutzer aktualisieren", "actionGetUser": "Benutzer abrufen", @@ -991,6 +998,7 @@ "actionDeleteSite": "Standort löschen", "actionGetSite": "Standort abrufen", "actionListSites": "Standorte auflisten", + "actionApplyBlueprint": "Blaupause anwenden", "setupToken": "Setup-Token", "setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.", "setupTokenRequired": "Setup-Token ist erforderlich", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Lizenz", "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", - "enableDockerSocket": "Docker Socket aktivieren", - "enableDockerSocketDescription": "Docker Socket-Erkennung aktivieren, um Container-Informationen zu befüllen. Socket-Pfad muss Newt bereitgestellt werden.", + "enableDockerSocket": "Docker Blaupause aktivieren", + "enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.", "enableDockerSocketLink": "Mehr erfahren", "viewDockerContainers": "Docker Container anzeigen", "containersIn": "Container in {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Update verfügbar", "newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.", "domainPickerEnterDomain": "Domain", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, oder einfach myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Geben Sie die vollständige Domäne der Ressource ein, um verfügbare Optionen zu sehen.", "domainPickerDescriptionSaas": "Geben Sie eine vollständige Domäne, Subdomäne oder einfach einen Namen ein, um verfügbare Optionen zu sehen", "domainPickerTabAll": "Alle", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protokoll", "editInternalResourceDialogSitePort": "Site-Port", "editInternalResourceDialogTargetConfiguration": "Zielkonfiguration", - "editInternalResourceDialogDestinationIP": "Ziel-IP", - "editInternalResourceDialogDestinationPort": "Ziel-Port", "editInternalResourceDialogCancel": "Abbrechen", "editInternalResourceDialogSaveResource": "Ressource speichern", "editInternalResourceDialogSuccess": "Erfolg", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Site-Port", "createInternalResourceDialogSitePortDescription": "Verwenden Sie diesen Port, um bei Verbindung mit einem Client auf die Ressource an der Site zuzugreifen.", "createInternalResourceDialogTargetConfiguration": "Zielkonfiguration", - "createInternalResourceDialogDestinationIP": "Ziel-IP", - "createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse der Ressource im Netzwerkstandort der Site.", - "createInternalResourceDialogDestinationPort": "Ziel-Port", + "createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse oder Hostname Adresse der Ressource im Netzwerk der Website.", "createInternalResourceDialogDestinationPortDescription": "Der Port auf der Ziel-IP, unter dem die Ressource zugänglich ist.", "createInternalResourceDialogCancel": "Abbrechen", "createInternalResourceDialogCreateResource": "Ressource erstellen", @@ -1496,5 +1500,24 @@ "convertButton": "Diesen Knoten in Managed Self-Hosted umwandeln" }, "internationaldomaindetected": "Internationale Domain erkannt", - "willbestoredas": "Wird gespeichert als:" + "willbestoredas": "Wird gespeichert als:", + "idpGoogleDescription": "Google OAuth2/OIDC Provider", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Eigene Kopfzeilen", + "headersValidationError": "Header müssen im Format Header-Name: Wert sein.", + "domainPickerProvidedDomain": "Angegebene Domain", + "domainPickerFreeProvidedDomain": "Kostenlose Domain", + "domainPickerVerified": "Verifiziert", + "domainPickerUnverified": "Nicht verifiziert", + "domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.", + "domainPickerError": "Fehler", + "domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domänen", + "domainPickerErrorCheckAvailability": "Fehler beim Prüfen der Domain-Verfügbarkeit", + "domainPickerInvalidSubdomain": "Ungültige Subdomain", + "domainPickerInvalidSubdomainRemoved": "Die Eingabe \"{sub}\" wurde entfernt, weil sie nicht gültig ist.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" konnte nicht für {domain} gültig gemacht werden.", + "domainPickerSubdomainSanitized": "Subdomain bereinigt", + "domainPickerSubdomainCorrected": "\"{sub}\" wurde korrigiert zu \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Datei bearbeiten: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Datei bearbeiten: docker-compose.yml" } diff --git a/messages/en-US.json b/messages/en-US.json index d238f73c..a483eed2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "An error occurred while adding user to the role.", "userSaved": "User saved", "userSavedDescription": "The user has been updated.", + "autoProvisioned": "Auto Provisioned", + "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", "roles": "Roles", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Invalid IP address format", "ipAddressErrorInvalidOctet": "Invalid IP address octet", "path": "Path", + "matchPath": "Match Path", "ipAddressRange": "IP Range", "rulesErrorFetch": "Failed to fetch rules", "rulesErrorFetchDescription": "An error occurred while fetching rules", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Connected", "idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.", "idpErrorNotFound": "IdP not found", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Invalid Invite", "inviteInvalidDescription": "The invite link is invalid.", "inviteErrorWrongUser": "Invite is not for this user", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Professional Edition Required", "licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.", "actionGetOrg": "Get Organization", + "updateOrgUser": "Update Org User", + "createOrgUser": "Create Org User", "actionUpdateOrg": "Update Organization", "actionUpdateUser": "Update User", "actionGetUser": "Get User", @@ -991,6 +998,7 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "actionApplyBlueprint": "Apply Blueprint", "setupToken": "Setup Token", "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", @@ -1133,8 +1141,8 @@ "sidebarLicense": "License", "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", - "enableDockerSocket": "Enable Docker Socket", - "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", + "enableDockerSocket": "Enable Docker Blueprint", + "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketLink": "Learn More", "viewDockerContainers": "View Docker Containers", "containersIn": "Containers in {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", "domainPickerEnterDomain": "Domain", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Enter the full domain of the resource to see available options.", "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", "domainPickerTabAll": "All", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocol", "editInternalResourceDialogSitePort": "Site Port", "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", "editInternalResourceDialogCancel": "Cancel", "editInternalResourceDialogSaveResource": "Save Resource", "editInternalResourceDialogSuccess": "Success", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Site Port", "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.", "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", "createInternalResourceDialogCancel": "Cancel", "createInternalResourceDialogCreateResource": "Create Resource", @@ -1496,5 +1500,24 @@ "convertButton": "Convert This Node to Managed Self-Hosted" }, "internationaldomaindetected": "International Domain Detected", - "willbestoredas": "Will be stored as:" + "willbestoredas": "Will be stored as:", + "idpGoogleDescription": "Google OAuth2/OIDC provider", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Custom Headers", + "headersValidationError": "Headers must be in the format: Header-Name: value.", + "domainPickerProvidedDomain": "Provided Domain", + "domainPickerFreeProvidedDomain": "Free Provided Domain", + "domainPickerVerified": "Verified", + "domainPickerUnverified": "Unverified", + "domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.", + "domainPickerError": "Error", + "domainPickerErrorLoadDomains": "Failed to load organization domains", + "domainPickerErrorCheckAvailability": "Failed to check domain availability", + "domainPickerInvalidSubdomain": "Invalid subdomain", + "domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", + "domainPickerSubdomainSanitized": "Subdomain sanitized", + "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Edit file: docker-compose.yml" } diff --git a/messages/es-ES.json b/messages/es-ES.json index fe8c52d1..0a835b33 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Ocurrió un error mientras se añadía el usuario al rol.", "userSaved": "Usuario guardado", "userSavedDescription": "El usuario ha sido actualizado.", + "autoProvisioned": "Auto asegurado", + "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", "roles": "Roles", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Formato de dirección IP inválido", "ipAddressErrorInvalidOctet": "Octet de dirección IP no válido", "path": "Ruta", + "matchPath": "Coincidir ruta", "ipAddressRange": "Rango IP", "rulesErrorFetch": "Error al obtener las reglas", "rulesErrorFetchDescription": "Se ha producido un error al recuperar las reglas", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Conectado", "idpErrorConnectingTo": "Hubo un problema al conectar con {name}. Por favor, póngase en contacto con su administrador.", "idpErrorNotFound": "IdP no encontrado", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Invitación inválida", "inviteInvalidDescription": "El enlace de invitación no es válido.", "inviteErrorWrongUser": "La invitación no es para este usuario", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Edición Profesional requerida", "licenseTierProfessionalRequiredDescription": "Esta característica sólo está disponible en la Edición Profesional.", "actionGetOrg": "Obtener organización", + "updateOrgUser": "Actualizar usuario Org", + "createOrgUser": "Crear usuario Org", "actionUpdateOrg": "Actualizar organización", "actionUpdateUser": "Actualizar usuario", "actionGetUser": "Obtener usuario", @@ -991,6 +998,7 @@ "actionDeleteSite": "Eliminar sitio", "actionGetSite": "Obtener sitio", "actionListSites": "Listar sitios", + "actionApplyBlueprint": "Aplicar plano", "setupToken": "Configuración de token", "setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.", "setupTokenRequired": "Se requiere el token de configuración", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Licencia", "sidebarClients": "Clientes (Beta)", "sidebarDomains": "Dominios", - "enableDockerSocket": "Habilitar conector Docker", - "enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. La ruta del socket debe proporcionarse a Newt.", + "enableDockerSocket": "Habilitar Plano Docker", + "enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.", "enableDockerSocketLink": "Saber más", "viewDockerContainers": "Ver contenedores Docker", "containersIn": "Contenedores en {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Nueva actualización disponible", "newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.", "domainPickerEnterDomain": "Dominio", - "domainPickerPlaceholder": "myapp.example.com, api.v1.miDominio.com, o solo myapp", + "domainPickerPlaceholder": "miapp.ejemplo.com", "domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.", "domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles", "domainPickerTabAll": "Todo", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocolo", "editInternalResourceDialogSitePort": "Puerto del sitio", "editInternalResourceDialogTargetConfiguration": "Configuración de objetivos", - "editInternalResourceDialogDestinationIP": "IP de destino", - "editInternalResourceDialogDestinationPort": "Puerto de destino", "editInternalResourceDialogCancel": "Cancelar", "editInternalResourceDialogSaveResource": "Guardar recurso", "editInternalResourceDialogSuccess": "Éxito", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Puerto del sitio", "createInternalResourceDialogSitePortDescription": "Use este puerto para acceder al recurso en el sitio cuando se conecta con un cliente.", "createInternalResourceDialogTargetConfiguration": "Configuración de objetivos", - "createInternalResourceDialogDestinationIP": "IP de destino", - "createInternalResourceDialogDestinationIPDescription": "La dirección IP del recurso en la red del sitio.", - "createInternalResourceDialogDestinationPort": "Puerto de destino", + "createInternalResourceDialogDestinationIPDescription": "La dirección IP o nombre de host del recurso en la red del sitio.", "createInternalResourceDialogDestinationPortDescription": "El puerto en la IP de destino donde el recurso es accesible.", "createInternalResourceDialogCancel": "Cancelar", "createInternalResourceDialogCreateResource": "Crear recurso", @@ -1496,5 +1500,24 @@ "convertButton": "Convierte este nodo a autoalojado administrado" }, "internationaldomaindetected": "Dominio Internacional detectado", - "willbestoredas": "Se almacenará como:" + "willbestoredas": "Se almacenará como:", + "idpGoogleDescription": "Proveedor OAuth2/OIDC de Google", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Cabeceras personalizadas", + "headersValidationError": "Los encabezados deben estar en el formato: Nombre de cabecera: valor.", + "domainPickerProvidedDomain": "Dominio proporcionado", + "domainPickerFreeProvidedDomain": "Dominio proporcionado gratis", + "domainPickerVerified": "Verificado", + "domainPickerUnverified": "Sin verificar", + "domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.", + "domainPickerError": "Error", + "domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización", + "domainPickerErrorCheckAvailability": "No se pudo comprobar la disponibilidad del dominio", + "domainPickerInvalidSubdomain": "Subdominio inválido", + "domainPickerInvalidSubdomainRemoved": "La entrada \"{sub}\" fue eliminada porque no es válida.", + "domainPickerInvalidSubdomainCannotMakeValid": "No se ha podido hacer válido \"{sub}\" para {domain}.", + "domainPickerSubdomainSanitized": "Subdominio saneado", + "domainPickerSubdomainCorrected": "\"{sub}\" fue corregido a \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Editar archivo: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Editar archivo: docker-compose.yml" } diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 7f51a9c8..0918f943 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -10,7 +10,7 @@ "setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.", "componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.", "componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.", - "welcome": "Bienvenue à Pangolin", + "welcome": "Bienvenue sur Pangolin", "welcomeTo": "Bienvenue chez", "componentsCreateOrg": "Créer une organisation", "componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.", @@ -34,13 +34,13 @@ "confirmPassword": "Confirmer le mot de passe", "createAccount": "Créer un compte", "viewSettings": "Afficher les paramètres", - "delete": "Supprimez", + "delete": "Supprimer", "name": "Nom", "online": "En ligne", "offline": "Hors ligne", "site": "Site", - "dataIn": "Données dans", - "dataOut": "Données épuisées", + "dataIn": "Données reçues", + "dataOut": "Données envoyées", "connectionType": "Type de connexion", "tunnelType": "Type de tunnel", "local": "Locale", @@ -175,7 +175,7 @@ "resourceHTTPSSettingsDescription": "Configurer comment votre ressource sera accédée via HTTPS", "domainType": "Type de domaine", "subdomain": "Sous-domaine", - "baseDomain": "Domaine de base", + "baseDomain": "Domaine racine", "subdomnainDescription": "Le sous-domaine où votre ressource sera accessible.", "resourceRawSettings": "Paramètres TCP/UDP", "resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP", @@ -309,7 +309,7 @@ "numberOfSites": "Nombre de sites", "licenseKeySearch": "Rechercher des clés de licence...", "licenseKeyAdd": "Ajouter une clé de licence", - "type": "Type de texte", + "type": "Type", "licenseKeyRequired": "La clé de licence est requise", "licenseTermsAgree": "Vous devez accepter les conditions de licence", "licenseErrorKeyLoad": "Impossible de charger les clés de licence", @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Une erreur s'est produite lors de l'ajout de l'utilisateur au rôle.", "userSaved": "Utilisateur enregistré", "userSavedDescription": "L'utilisateur a été mis à jour.", + "autoProvisioned": "Auto-provisionné", + "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", "roles": "Rôles", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Format d'adresse IP invalide", "ipAddressErrorInvalidOctet": "Octet d'adresse IP invalide", "path": "Chemin", + "matchPath": "Chemin de correspondance", "ipAddressRange": "Plage IP", "rulesErrorFetch": "Échec de la récupération des règles", "rulesErrorFetchDescription": "Une erreur s'est produite lors de la récupération des règles", @@ -595,7 +598,7 @@ "newtId": "ID Newt", "newtSecretKey": "Clé secrète Newt", "architecture": "Architecture", - "sites": "Espaces", + "sites": "Sites", "siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.", "siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard", "siteWgManualConfigurationRequired": "Configuration manuelle requise", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Connecté", "idpErrorConnectingTo": "Un problème est survenu lors de la connexion à {name}. Veuillez contacter votre administrateur.", "idpErrorNotFound": "IdP introuvable", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Invitation invalide", "inviteInvalidDescription": "Le lien d'invitation n'est pas valide.", "inviteErrorWrongUser": "L'invitation n'est pas pour cet utilisateur", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Édition Professionnelle Requise", "licenseTierProfessionalRequiredDescription": "Cette fonctionnalité n'est disponible que dans l'Édition Professionnelle.", "actionGetOrg": "Obtenir l'organisation", + "updateOrgUser": "Mise à jour de l'utilisateur Org", + "createOrgUser": "Créer un utilisateur Org", "actionUpdateOrg": "Mettre à jour l'organisation", "actionUpdateUser": "Mettre à jour l'utilisateur", "actionGetUser": "Obtenir l'utilisateur", @@ -991,6 +998,7 @@ "actionDeleteSite": "Supprimer un site", "actionGetSite": "Obtenir un site", "actionListSites": "Lister les sites", + "actionApplyBlueprint": "Appliquer le Plan", "setupToken": "Jeton de configuration", "setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.", "setupTokenRequired": "Le jeton de configuration est requis.", @@ -1120,7 +1128,7 @@ "sidebarOverview": "Aperçu", "sidebarHome": "Domicile", "sidebarSites": "Espaces", - "sidebarResources": "Ressource", + "sidebarResources": "Ressources", "sidebarAccessControl": "Contrôle d'accès", "sidebarUsers": "Utilisateurs", "sidebarInvitations": "Invitations", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Licence", "sidebarClients": "Clients (Bêta)", "sidebarDomains": "Domaines", - "enableDockerSocket": "Activer Docker Socket", - "enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du socket doit être fourni à Newt.", + "enableDockerSocket": "Activer le Plan Docker", + "enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.", "enableDockerSocketLink": "En savoir plus", "viewDockerContainers": "Voir les conteneurs Docker", "containersIn": "Conteneurs en {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Mise à jour disponible", "newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.", "domainPickerEnterDomain": "Domaine", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, ou simplement myapp", + "domainPickerPlaceholder": "monapp.exemple.com", "domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.", "domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles", "domainPickerTabAll": "Tous", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocole", "editInternalResourceDialogSitePort": "Port du site", "editInternalResourceDialogTargetConfiguration": "Configuration de la cible", - "editInternalResourceDialogDestinationIP": "IP de destination", - "editInternalResourceDialogDestinationPort": "Port de destination", "editInternalResourceDialogCancel": "Abandonner", "editInternalResourceDialogSaveResource": "Enregistrer la ressource", "editInternalResourceDialogSuccess": "Succès", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Port du site", "createInternalResourceDialogSitePortDescription": "Utilisez ce port pour accéder à la ressource sur le site lors de la connexion avec un client.", "createInternalResourceDialogTargetConfiguration": "Configuration de la cible", - "createInternalResourceDialogDestinationIP": "IP de destination", - "createInternalResourceDialogDestinationIPDescription": "L'adresse IP de la ressource sur le réseau du site.", - "createInternalResourceDialogDestinationPort": "Port de destination", + "createInternalResourceDialogDestinationIPDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.", "createInternalResourceDialogDestinationPortDescription": "Le port sur l'IP de destination où la ressource est accessible.", "createInternalResourceDialogCancel": "Abandonner", "createInternalResourceDialogCreateResource": "Créer une ressource", @@ -1496,5 +1500,24 @@ "convertButton": "Convertir ce noeud en auto-hébergé géré" }, "internationaldomaindetected": "Domaine international détecté", - "willbestoredas": "Sera stocké comme :" + "willbestoredas": "Sera stocké comme :", + "idpGoogleDescription": "Fournisseur Google OAuth2/OIDC", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "En-têtes personnalisés", + "headersValidationError": "Les entêtes doivent être au format : Header-Name: valeur.", + "domainPickerProvidedDomain": "Domaine fourni", + "domainPickerFreeProvidedDomain": "Domaine fourni gratuitement", + "domainPickerVerified": "Vérifié", + "domainPickerUnverified": "Non vérifié", + "domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.", + "domainPickerError": "Erreur", + "domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation", + "domainPickerErrorCheckAvailability": "Impossible de vérifier la disponibilité du domaine", + "domainPickerInvalidSubdomain": "Sous-domaine invalide", + "domainPickerInvalidSubdomainRemoved": "L'entrée \"{sub}\" a été supprimée car elle n'est pas valide.", + "domainPickerInvalidSubdomainCannotMakeValid": "La «{sub}» n'a pas pu être validée pour {domain}.", + "domainPickerSubdomainSanitized": "Sous-domaine nettoyé", + "domainPickerSubdomainCorrected": "\"{sub}\" a été corrigé à \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Modifier le fichier : config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Modifier le fichier : docker-compose.yml" } diff --git a/messages/it-IT.json b/messages/it-IT.json index 9b935609..f0a862cd 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Si è verificato un errore durante l'aggiunta dell'utente al ruolo.", "userSaved": "Utente salvato", "userSavedDescription": "L'utente è stato aggiornato.", + "autoProvisioned": "Auto Provisioned", + "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", "roles": "Ruoli", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Formato indirizzo IP non valido", "ipAddressErrorInvalidOctet": "Ottetto indirizzo IP non valido", "path": "Percorso", + "matchPath": "Corrispondenza Tracciato", "ipAddressRange": "Intervallo IP", "rulesErrorFetch": "Impossibile recuperare le regole", "rulesErrorFetchDescription": "Si è verificato un errore durante il recupero delle regole", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Connesso", "idpErrorConnectingTo": "Si è verificato un problema durante la connessione a {name}. Contatta il tuo amministratore.", "idpErrorNotFound": "IdP non trovato", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Invito Non Valido", "inviteInvalidDescription": "Il link di invito non è valido.", "inviteErrorWrongUser": "L'invito non è per questo utente", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Edizione Professional Richiesta", "licenseTierProfessionalRequiredDescription": "Questa funzionalità è disponibile solo nell'Edizione Professional.", "actionGetOrg": "Ottieni Organizzazione", + "updateOrgUser": "Aggiorna Utente Org", + "createOrgUser": "Crea Utente Org", "actionUpdateOrg": "Aggiorna Organizzazione", "actionUpdateUser": "Aggiorna Utente", "actionGetUser": "Ottieni Utente", @@ -991,6 +998,7 @@ "actionDeleteSite": "Elimina Sito", "actionGetSite": "Ottieni Sito", "actionListSites": "Elenca Siti", + "actionApplyBlueprint": "Applica Progetto", "setupToken": "Configura Token", "setupTokenDescription": "Inserisci il token di configurazione dalla console del server.", "setupTokenRequired": "Il token di configurazione è richiesto", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Licenza", "sidebarClients": "Clienti (Beta)", "sidebarDomains": "Domini", - "enableDockerSocket": "Abilita Docker Socket", - "enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. Il percorso del socket deve essere fornito a Newt.", + "enableDockerSocket": "Abilita Progetto Docker", + "enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.", "enableDockerSocketLink": "Scopri di più", "viewDockerContainers": "Visualizza Contenitori Docker", "containersIn": "Contenitori in {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Aggiornamento Disponibile", "newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.", "domainPickerEnterDomain": "Dominio", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, o semplicemente myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.", "domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili", "domainPickerTabAll": "Tutti", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocollo", "editInternalResourceDialogSitePort": "Porta del Sito", "editInternalResourceDialogTargetConfiguration": "Configurazione Target", - "editInternalResourceDialogDestinationIP": "IP di Destinazione", - "editInternalResourceDialogDestinationPort": "Porta di Destinazione", "editInternalResourceDialogCancel": "Annulla", "editInternalResourceDialogSaveResource": "Salva Risorsa", "editInternalResourceDialogSuccess": "Successo", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Porta del Sito", "createInternalResourceDialogSitePortDescription": "Usa questa porta per accedere alla risorsa nel sito quando sei connesso con un client.", "createInternalResourceDialogTargetConfiguration": "Configurazione Target", - "createInternalResourceDialogDestinationIP": "IP di Destinazione", - "createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP della risorsa sulla rete del sito.", - "createInternalResourceDialogDestinationPort": "Porta di Destinazione", + "createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP o hostname della risorsa nella rete del sito.", "createInternalResourceDialogDestinationPortDescription": "La porta sull'IP di destinazione dove la risorsa è accessibile.", "createInternalResourceDialogCancel": "Annulla", "createInternalResourceDialogCreateResource": "Crea Risorsa", @@ -1496,5 +1500,24 @@ "convertButton": "Converti questo nodo in auto-ospitato gestito" }, "internationaldomaindetected": "Dominio Internazionale Rilevato", - "willbestoredas": "Verrà conservato come:" + "willbestoredas": "Verrà conservato come:", + "idpGoogleDescription": "Google OAuth2/OIDC provider", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Intestazioni Personalizzate", + "headersValidationError": "Le intestazioni devono essere nel formato: Intestazione-Nome: valore.", + "domainPickerProvidedDomain": "Dominio Fornito", + "domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito", + "domainPickerVerified": "Verificato", + "domainPickerUnverified": "Non Verificato", + "domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.", + "domainPickerError": "Errore", + "domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione", + "domainPickerErrorCheckAvailability": "Impossibile verificare la disponibilità del dominio", + "domainPickerInvalidSubdomain": "Sottodominio non valido", + "domainPickerInvalidSubdomainRemoved": "L'input \"{sub}\" è stato rimosso perché non è valido.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" non può essere reso valido per {domain}.", + "domainPickerSubdomainSanitized": "Sottodominio igienizzato", + "domainPickerSubdomainCorrected": "\"{sub}\" è stato corretto in \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Modifica file: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Modifica file: docker-compose.yml" } diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 2b9e7b1c..64a449d0 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.", "userSaved": "사용자 저장됨", "userSavedDescription": "사용자가 업데이트되었습니다.", + "autoProvisioned": "자동 프로비저닝됨", + "autoProvisionedDescription": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다", "accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요", "accessControlsSubmit": "접근 제어 저장", "roles": "역할", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "잘못된 IP 주소 형식", "ipAddressErrorInvalidOctet": "유효하지 않은 IP 주소 옥텟", "path": "경로", + "matchPath": "경로 맞춤", "ipAddressRange": "IP 범위", "rulesErrorFetch": "규칙을 가져오는 데 실패했습니다.", "rulesErrorFetchDescription": "규칙을 가져오는 중 오류가 발생했습니다", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "연결됨", "idpErrorConnectingTo": "{name}에 연결하는 데 문제가 발생했습니다. 관리자에게 문의하십시오.", "idpErrorNotFound": "IdP를 찾을 수 없습니다.", + "idpGoogleAlt": "구글", + "idpAzureAlt": "애저", "inviteInvalid": "유효하지 않은 초대", "inviteInvalidDescription": "초대 링크가 유효하지 않습니다.", "inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "전문 에디션이 필요합니다.", "licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.", "actionGetOrg": "조직 가져오기", + "updateOrgUser": "조직 사용자 업데이트", + "createOrgUser": "조직 사용자 생성", "actionUpdateOrg": "조직 업데이트", "actionUpdateUser": "사용자 업데이트", "actionGetUser": "사용자 조회", @@ -991,6 +998,7 @@ "actionDeleteSite": "사이트 삭제", "actionGetSite": "사이트 가져오기", "actionListSites": "사이트 목록", + "actionApplyBlueprint": "청사진 적용", "setupToken": "설정 토큰", "setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.", "setupTokenRequired": "설정 토큰이 필요합니다", @@ -1133,8 +1141,8 @@ "sidebarLicense": "라이선스", "sidebarClients": "클라이언트 (Beta)", "sidebarDomains": "도메인", - "enableDockerSocket": "Docker 소켓 활성화", - "enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", + "enableDockerSocket": "Docker 청사진 활성화", + "enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", "enableDockerSocketLink": "자세히 알아보기", "viewDockerContainers": "도커 컨테이너 보기", "containersIn": "{siteName}의 컨테이너", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "업데이트 가능", "newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.", "domainPickerEnterDomain": "도메인", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.", "domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.", "domainPickerTabAll": "모두", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "프로토콜", "editInternalResourceDialogSitePort": "사이트 포트", "editInternalResourceDialogTargetConfiguration": "대상 구성", - "editInternalResourceDialogDestinationIP": "대상 IP", - "editInternalResourceDialogDestinationPort": "대상 IP의 포트", "editInternalResourceDialogCancel": "취소", "editInternalResourceDialogSaveResource": "리소스 저장", "editInternalResourceDialogSuccess": "성공", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "사이트 포트", "createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.", "createInternalResourceDialogTargetConfiguration": "대상 설정", - "createInternalResourceDialogDestinationIP": "대상 IP", - "createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 주소입니다.", - "createInternalResourceDialogDestinationPort": "대상 포트", + "createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 또는 호스트 네임 주소입니다.", "createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.", "createInternalResourceDialogCancel": "취소", "createInternalResourceDialogCreateResource": "리소스 생성", @@ -1496,5 +1500,24 @@ "convertButton": "이 노드를 관리 자체 호스팅으로 변환" }, "internationaldomaindetected": "국제 도메인 감지됨", - "willbestoredas": "다음으로 저장됩니다:" + "willbestoredas": "다음으로 저장됩니다:", + "idpGoogleDescription": "Google OAuth2/OIDC 공급자", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC 공급자", + "customHeaders": "사용자 정의 헤더", + "headersValidationError": "헤더는 형식이어야 합니다: 헤더명: 값.", + "domainPickerProvidedDomain": "제공된 도메인", + "domainPickerFreeProvidedDomain": "무료 제공된 도메인", + "domainPickerVerified": "검증됨", + "domainPickerUnverified": "검증되지 않음", + "domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.", + "domainPickerError": "오류", + "domainPickerErrorLoadDomains": "조직 도메인 로드 실패", + "domainPickerErrorCheckAvailability": "도메인 가용성 확인 실패", + "domainPickerInvalidSubdomain": "잘못된 하위 도메인", + "domainPickerInvalidSubdomainRemoved": "입력 \"{sub}\"이(가) 유효하지 않으므로 제거되었습니다.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\"을(를) {domain}에 대해 유효하게 만들 수 없습니다.", + "domainPickerSubdomainSanitized": "하위 도메인 정리됨", + "domainPickerSubdomainCorrected": "\"{sub}\"이(가) \"{sanitized}\"로 수정되었습니다", + "resourceAddEntrypointsEditFile": "파일 편집: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "파일 편집: docker-compose.yml" } diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 6d1ae86a..ef5c0d2a 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.", "userSaved": "Bruker lagret", "userSavedDescription": "Brukeren har blitt oppdatert.", + "autoProvisioned": "Auto avlyst", + "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", "roles": "Roller", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat", "ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet", "path": "Sti", + "matchPath": "Match sti", "ipAddressRange": "IP-område", "rulesErrorFetch": "Klarte ikke å hente regler", "rulesErrorFetchDescription": "Det oppsto en feil under henting av regler", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Tilkoblet", "idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.", "idpErrorNotFound": "IdP ikke funnet", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Ugyldig invitasjon", "inviteInvalidDescription": "Invitasjonslenken er ugyldig.", "inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Profesjonell utgave påkrevd", "licenseTierProfessionalRequiredDescription": "Denne funksjonen er kun tilgjengelig i den profesjonelle utgaven.", "actionGetOrg": "Hent organisasjon", + "updateOrgUser": "Oppdater org.bruker", + "createOrgUser": "Opprett Org bruker", "actionUpdateOrg": "Oppdater organisasjon", "actionUpdateUser": "Oppdater bruker", "actionGetUser": "Hent bruker", @@ -991,6 +998,7 @@ "actionDeleteSite": "Slett område", "actionGetSite": "Hent område", "actionListSites": "List opp områder", + "actionApplyBlueprint": "Bruk blåkopi", "setupToken": "Oppsetttoken", "setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.", "setupTokenRequired": "Oppsetttoken er nødvendig", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Lisens", "sidebarClients": "Klienter (Beta)", "sidebarDomains": "Domener", - "enableDockerSocket": "Aktiver Docker Socket", - "enableDockerSocketDescription": "Aktiver Docker Socket-oppdagelse for å fylle ut containerinformasjon. Socket-stien må oppgis til Newt.", + "enableDockerSocket": "Aktiver Docker blåkopi", + "enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.", "enableDockerSocketLink": "Lær mer", "viewDockerContainers": "Vis Docker-containere", "containersIn": "Containere i {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Oppdatering tilgjengelig", "newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.", "domainPickerEnterDomain": "Domene", - "domainPickerPlaceholder": "minapp.eksempel.com, api.v1.mittdomene.com, eller bare minapp", + "domainPickerPlaceholder": "minapp.eksempel.no", "domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.", "domainPickerDescriptionSaas": "Skriv inn et fullt domene, underdomene eller bare et navn for å se tilgjengelige alternativer", "domainPickerTabAll": "Alle", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protokoll", "editInternalResourceDialogSitePort": "Områdeport", "editInternalResourceDialogTargetConfiguration": "Målkonfigurasjon", - "editInternalResourceDialogDestinationIP": "Destinasjons-IP", - "editInternalResourceDialogDestinationPort": "Destinasjonsport", "editInternalResourceDialogCancel": "Avbryt", "editInternalResourceDialogSaveResource": "Lagre ressurs", "editInternalResourceDialogSuccess": "Suksess", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Områdeport", "createInternalResourceDialogSitePortDescription": "Bruk denne porten for å få tilgang til ressursen på området når du er tilkoblet med en klient.", "createInternalResourceDialogTargetConfiguration": "Målkonfigurasjon", - "createInternalResourceDialogDestinationIP": "Destinasjons-IP", - "createInternalResourceDialogDestinationIPDescription": "IP-adressen til ressursen på områdets nettverk.", - "createInternalResourceDialogDestinationPort": "Destinasjonsport", + "createInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.", "createInternalResourceDialogDestinationPortDescription": "Porten på destinasjons-IP-en der ressursen kan nås.", "createInternalResourceDialogCancel": "Avbryt", "createInternalResourceDialogCreateResource": "Opprett ressurs", @@ -1496,5 +1500,24 @@ "convertButton": "Konverter denne noden til manuelt bruk" }, "internationaldomaindetected": "Internasjonalt domene oppdaget", - "willbestoredas": "Vil bli lagret som:" + "willbestoredas": "Vil bli lagret som:", + "idpGoogleDescription": "Google OAuth2/OIDC leverandør", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Egendefinerte topptekster", + "headersValidationError": "Topptekst må være i formatet: header-navn: verdi.", + "domainPickerProvidedDomain": "Gitt domene", + "domainPickerFreeProvidedDomain": "Gratis oppgitt domene", + "domainPickerVerified": "Bekreftet", + "domainPickerUnverified": "Uverifisert", + "domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.", + "domainPickerError": "Feil", + "domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener", + "domainPickerErrorCheckAvailability": "Kunne ikke kontrollere domenetilgjengelighet", + "domainPickerInvalidSubdomain": "Ugyldig underdomene", + "domainPickerInvalidSubdomainRemoved": "Inndata \"{sub}\" ble fjernet fordi det ikke er gyldig.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" kunne ikke gjøres gyldig for {domain}.", + "domainPickerSubdomainSanitized": "Underdomenet som ble sanivert", + "domainPickerSubdomainCorrected": "\"{sub}\" var korrigert til \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Rediger fil: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Rediger fil: docker-compose.yml" } diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 6252d752..ba4ab637 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -38,12 +38,12 @@ "name": "naam", "online": "Online", "offline": "Offline", - "site": "Website", - "dataIn": "Gegevens in", - "dataOut": "Data Uit", + "site": "Referentie", + "dataIn": "Dataverbruik inkomend", + "dataOut": "Dataverbruik uitgaand", "connectionType": "Type verbinding", "tunnelType": "Tunnel type", - "local": "lokaal", + "local": "Lokaal", "edit": "Bewerken", "siteConfirmDelete": "Verwijderen van site bevestigen", "siteDelete": "Site verwijderen", @@ -55,7 +55,7 @@ "siteCreate": "Site maken", "siteCreateDescription2": "Volg de onderstaande stappen om een nieuwe site aan te maken en te verbinden", "siteCreateDescription": "Maak een nieuwe site aan om verbinding te maken met uw bronnen", - "close": "Afsluiten", + "close": "Sluiten", "siteErrorCreate": "Fout bij maken site", "siteErrorCreateKeyPair": "Key pair of site standaard niet gevonden", "siteErrorCreateDefaults": "Standaardinstellingen niet gevonden", @@ -90,7 +90,7 @@ "siteGeneralDescription": "Algemene instellingen voor deze site configureren", "siteSettingDescription": "Configureer de instellingen op uw site", "siteSetting": "{siteName} instellingen", - "siteNewtTunnel": "Nieuwstunnel (Aanbevolen)", + "siteNewtTunnel": "Newttunnel (Aanbevolen)", "siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.", "siteWg": "Basis WireGuard", "siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.", @@ -104,7 +104,7 @@ "siteCredentialsSave": "Uw referenties opslaan", "siteCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.", "siteInfo": "Site informatie", - "status": "status", + "status": "Status", "shareTitle": "Beheer deellinks", "shareDescription": "Maak deelbare links aan om tijdelijke of permanente toegang tot uw bronnen te verlenen", "shareSearch": "Zoek share links...", @@ -146,19 +146,19 @@ "never": "Nooit", "shareErrorSelectResource": "Selecteer een bron", "resourceTitle": "Bronnen beheren", - "resourceDescription": "Veilige proxy's voor uw privé applicaties maken", + "resourceDescription": "Veilige proxy's voor uw privé applicaties aanmaken", "resourcesSearch": "Zoek bronnen...", "resourceAdd": "Bron toevoegen", "resourceErrorDelte": "Fout bij verwijderen document", "authentication": "Authenticatie", - "protected": "Beschermd", - "notProtected": "Niet beschermd", + "protected": "Beveiligd", + "notProtected": "Niet beveiligd", "resourceMessageRemove": "Eenmaal verwijderd, zal het bestand niet langer toegankelijk zijn. Alle doelen die gekoppeld zijn aan het hulpbron, zullen ook verwijderd worden.", "resourceMessageConfirm": "Om te bevestigen, typ de naam van de bron hieronder.", "resourceQuestionRemove": "Weet u zeker dat u de resource {selectedResource} uit de organisatie wilt verwijderen?", "resourceHTTP": "HTTPS bron", "resourceHTTPDescription": "Proxy verzoeken aan uw app via HTTPS via een subdomein of basisdomein.", - "resourceRaw": "Ruwe TCP/UDP bron", + "resourceRaw": "TCP/UDP bron", "resourceRawDescription": "Proxy verzoeken naar je app via TCP/UDP met behulp van een poortnummer.", "resourceCreate": "Bron maken", "resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken", @@ -183,7 +183,7 @@ "protocolSelect": "Selecteer een protocol", "resourcePortNumber": "Nummer van poort", "resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.", - "cancel": "annuleren", + "cancel": "Annuleren", "resourceConfig": "Configuratie tekstbouwstenen", "resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om je TCP/UDP-bron in te stellen", "resourceAddEntrypoints": "Traefik: Entrypoints toevoegen", @@ -212,7 +212,7 @@ "saveGeneralSettings": "Algemene instellingen opslaan", "saveSettings": "Instellingen opslaan", "orgDangerZone": "Gevaarlijke zone", - "orgDangerZoneDescription": "Als u deze instantie verwijdert, is er geen weg terug. Wees het alstublieft zeker.", + "orgDangerZoneDescription": "Deze instantie verwijderen is onomkeerbaar. Bevestig alstublieft dat u wilt doorgaan.", "orgDelete": "Verwijder organisatie", "orgDeleteConfirm": "Bevestig Verwijderen Organisatie", "orgMessageRemove": "Deze actie is onomkeerbaar en zal alle bijbehorende gegevens verwijderen.", @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Er is een fout opgetreden tijdens het toevoegen van de rol.", "userSaved": "Gebruiker opgeslagen", "userSavedDescription": "De gebruiker is bijgewerkt.", + "autoProvisioned": "Automatisch bevestigen", + "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", "roles": "Rollen", @@ -499,8 +501,8 @@ "targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.", "methodSelect": "Selecteer methode", "targetSubmit": "Doelwit toevoegen", - "targetNoOne": "Geen doelwitten. Voeg een doel toe via het formulier.", - "targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.", + "targetNoOne": "Geen doel toegevoegd. Voeg deze toe via dit formulier.", + "targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal load balancering mogelijk maken.", "targetsSubmit": "Doelstellingen opslaan", "proxyAdditional": "Extra Proxy-instellingen", "proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Ongeldig IP-adresformaat", "ipAddressErrorInvalidOctet": "Ongeldige IP adres octet", "path": "Pad", + "matchPath": "Overeenkomend pad", "ipAddressRange": "IP Bereik", "rulesErrorFetch": "Regels ophalen mislukt", "rulesErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de regels", @@ -595,7 +598,7 @@ "newtId": "Newt-ID", "newtSecretKey": "Nieuwe geheime sleutel", "architecture": "Architectuur", - "sites": "Werkruimtes", + "sites": "Verbindingen", "siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je moet je interne bronnen aanspreken met behulp van de peer IP.", "siteWgCompatibleAllClients": "Compatibel met alle WireGuard clients", "siteWgManualConfigurationRequired": "Handmatige configuratie vereist", @@ -726,7 +729,7 @@ "idpMessageConfirm": "Om dit te bevestigen, typt u de naam van onderstaande identiteitsprovider.", "idpConfirmDelete": "Bevestig verwijderen Identity Provider", "idpDelete": "Identity Provider verwijderen", - "idp": "Identiteit aanbieders", + "idp": "Identiteitsaanbieders", "idpSearch": "Identiteitsaanbieders zoeken...", "idpAdd": "Identity Provider toevoegen", "idpClientIdRequired": "Client-ID is vereist.", @@ -798,7 +801,7 @@ "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": "Rekening", + "org": "Organisatie", "orgSelect": "Selecteer organisatie", "orgSearch": "Zoek in org", "orgNotFound": "Geen org gevonden.", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Verbonden", "idpErrorConnectingTo": "Er was een probleem bij het verbinden met {name}. Neem contact op met uw beheerder.", "idpErrorNotFound": "IdP niet gevonden", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Ongeldige uitnodiging", "inviteInvalidDescription": "Uitnodigingslink is ongeldig.", "inviteErrorWrongUser": "Uitnodiging is niet voor deze gebruiker", @@ -971,10 +976,10 @@ "supportKeyEnterDescription": "Ontmoet je eigen huisdier Pangolin!", "githubUsername": "GitHub-gebruikersnaam", "supportKeyInput": "Supporter Sleutel", - "supportKeyBuy": "Koop Supportersleutel", + "supportKeyBuy": "Koop supportersleutel", "logoutError": "Fout bij uitloggen", "signingAs": "Ingelogd als", - "serverAdmin": "Server Beheerder", + "serverAdmin": "Server beheer", "managedSelfhosted": "Beheerde Self-Hosted", "otpEnable": "Twee-factor inschakelen", "otpDisable": "Tweestapsverificatie uitschakelen", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Professionele editie vereist", "licenseTierProfessionalRequiredDescription": "Deze functie is alleen beschikbaar in de Professional Edition.", "actionGetOrg": "Krijg Organisatie", + "updateOrgUser": "Org gebruiker bijwerken", + "createOrgUser": "Org gebruiker aanmaken", "actionUpdateOrg": "Organisatie bijwerken", "actionUpdateUser": "Gebruiker bijwerken", "actionGetUser": "Gebruiker ophalen", @@ -991,6 +998,7 @@ "actionDeleteSite": "Site verwijderen", "actionGetSite": "Site ophalen", "actionListSites": "Sites weergeven", + "actionApplyBlueprint": "Blauwdruk toepassen", "setupToken": "Setup Token", "setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.", "setupTokenRequired": "Setup-token is vereist", @@ -1120,7 +1128,7 @@ "sidebarOverview": "Overzicht.", "sidebarHome": "Startpagina", "sidebarSites": "Werkruimtes", - "sidebarResources": "Hulpmiddelen", + "sidebarResources": "Bronnen", "sidebarAccessControl": "Toegangs controle", "sidebarUsers": "Gebruikers", "sidebarInvitations": "Uitnodigingen", @@ -1133,13 +1141,13 @@ "sidebarLicense": "Licentie", "sidebarClients": "Clients (Bèta)", "sidebarDomains": "Domeinen", - "enableDockerSocket": "Docker Socket inschakelen", - "enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.", + "enableDockerSocket": "Schakel Docker Blauwdruk in", + "enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.", "enableDockerSocketLink": "Meer informatie", "viewDockerContainers": "Bekijk Docker containers", "containersIn": "Containers in {siteName}", "selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.", - "containerName": "naam", + "containerName": "Naam", "containerImage": "Afbeelding", "containerState": "Provincie", "containerNetworks": "Netwerken", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Update beschikbaar", "newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.", "domainPickerEnterDomain": "Domein", - "domainPickerPlaceholder": "mijnapp.voorbeeld.com, api.v1.mijndomein.com, of gewoon mijnapp", + "domainPickerPlaceholder": "mijnapp.voorbeeld.nl", "domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.", "domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien", "domainPickerTabAll": "Alles", @@ -1341,7 +1349,7 @@ "olmId": "Olm ID", "olmSecretKey": "Olm Geheime Sleutel", "clientCredentialsSave": "Uw referenties opslaan", - "clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.", + "clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer deze naar een veilige plek.", "generalSettingsDescription": "Configureer de algemene instellingen voor deze client", "clientUpdated": "Klant bijgewerkt ", "clientUpdatedDescription": "De client is bijgewerkt.", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocol", "editInternalResourceDialogSitePort": "Site Poort", "editInternalResourceDialogTargetConfiguration": "Doelconfiguratie", - "editInternalResourceDialogDestinationIP": "Bestemming IP", - "editInternalResourceDialogDestinationPort": "Bestemmingspoort", "editInternalResourceDialogCancel": "Annuleren", "editInternalResourceDialogSaveResource": "Sla bron op", "editInternalResourceDialogSuccess": "Succes", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Site Poort", "createInternalResourceDialogSitePortDescription": "Gebruik deze poort om toegang te krijgen tot de bron op de site wanneer verbonden met een client.", "createInternalResourceDialogTargetConfiguration": "Doelconfiguratie", - "createInternalResourceDialogDestinationIP": "Bestemming IP", - "createInternalResourceDialogDestinationIPDescription": "Het IP-adres van de bron op het netwerk van de site.", - "createInternalResourceDialogDestinationPort": "Bestemmingspoort", + "createInternalResourceDialogDestinationIPDescription": "Het IP of hostnaam adres van de bron op het netwerk van de site.", "createInternalResourceDialogDestinationPortDescription": "De poort op het bestemmings-IP waar de bron toegankelijk is.", "createInternalResourceDialogCancel": "Annuleren", "createInternalResourceDialogCreateResource": "Bron aanmaken", @@ -1496,5 +1500,24 @@ "convertButton": "Converteer deze node naar Beheerde Zelf-Hosted" }, "internationaldomaindetected": "Internationaal Domein Gedetecteerd", - "willbestoredas": "Zal worden opgeslagen als:" + "willbestoredas": "Zal worden opgeslagen als:", + "idpGoogleDescription": "Google OAuth2/OIDC provider", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Aangepaste headers", + "headersValidationError": "Headers moeten in het formaat zijn: Header-Naam: waarde.", + "domainPickerProvidedDomain": "Opgegeven domein", + "domainPickerFreeProvidedDomain": "Gratis verstrekt domein", + "domainPickerVerified": "Geverifieerd", + "domainPickerUnverified": "Ongeverifieerd", + "domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.", + "domainPickerError": "Foutmelding", + "domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen", + "domainPickerErrorCheckAvailability": "Kan domein beschikbaarheid niet controleren", + "domainPickerInvalidSubdomain": "Ongeldig subdomein", + "domainPickerInvalidSubdomainRemoved": "De invoer \"{sub}\" is verwijderd omdat het niet geldig is.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" kon niet geldig worden gemaakt voor {domain}.", + "domainPickerSubdomainSanitized": "Subdomein gesaniseerd", + "domainPickerSubdomainCorrected": "\"{sub}\" was gecorrigeerd op \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Bestand bewerken: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Bestand bewerken: docker-compose.yml" } diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 1aee50f2..4fe382e1 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Wystąpił błąd podczas dodawania użytkownika do roli.", "userSaved": "Użytkownik zapisany", "userSavedDescription": "Użytkownik został zaktualizowany.", + "autoProvisioned": "Przesłane automatycznie", + "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", "roles": "Role", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Nieprawidłowy format adresu IP", "ipAddressErrorInvalidOctet": "Nieprawidłowy oktet adresu IP", "path": "Ścieżka", + "matchPath": "Ścieżka dopasowania", "ipAddressRange": "Zakres IP", "rulesErrorFetch": "Nie udało się pobrać reguł", "rulesErrorFetchDescription": "Wystąpił błąd podczas pobierania reguł", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Połączono", "idpErrorConnectingTo": "Wystąpił problem z połączeniem z {name}. Skontaktuj się z administratorem.", "idpErrorNotFound": "Nie znaleziono IdP", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Nieprawidłowe zaproszenie", "inviteInvalidDescription": "Link zapraszający jest nieprawidłowy.", "inviteErrorWrongUser": "Zaproszenie nie jest dla tego użytkownika", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Wymagana edycja Professional", "licenseTierProfessionalRequiredDescription": "Ta funkcja jest dostępna tylko w edycji Professional.", "actionGetOrg": "Pobierz organizację", + "updateOrgUser": "Aktualizuj użytkownika Org", + "createOrgUser": "Utwórz użytkownika Org", "actionUpdateOrg": "Aktualizuj organizację", "actionUpdateUser": "Zaktualizuj użytkownika", "actionGetUser": "Pobierz użytkownika", @@ -991,6 +998,7 @@ "actionDeleteSite": "Usuń witrynę", "actionGetSite": "Pobierz witrynę", "actionListSites": "Lista witryn", + "actionApplyBlueprint": "Zastosuj schemat", "setupToken": "Skonfiguruj token", "setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.", "setupTokenRequired": "Wymagany jest token konfiguracji", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Licencja", "sidebarClients": "Klienci (Beta)", "sidebarDomains": "Domeny", - "enableDockerSocket": "Włącz gniazdo dokera", - "enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka gniazda musi być dostarczona do Newt.", + "enableDockerSocket": "Włącz schemat dokera", + "enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.", "enableDockerSocketLink": "Dowiedz się więcej", "viewDockerContainers": "Zobacz kontenery dokujące", "containersIn": "Pojemniki w {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Dostępna aktualizacja", "newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.", "domainPickerEnterDomain": "Domena", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com lub po prostu myapp", + "domainPickerPlaceholder": "mojapp.example.com", "domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.", "domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje", "domainPickerTabAll": "Wszystko", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protokół", "editInternalResourceDialogSitePort": "Port witryny", "editInternalResourceDialogTargetConfiguration": "Konfiguracja celu", - "editInternalResourceDialogDestinationIP": "IP docelowe", - "editInternalResourceDialogDestinationPort": "Port docelowy", "editInternalResourceDialogCancel": "Anuluj", "editInternalResourceDialogSaveResource": "Zapisz zasób", "editInternalResourceDialogSuccess": "Sukces", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Port witryny", "createInternalResourceDialogSitePortDescription": "Użyj tego portu, aby uzyskać dostęp do zasobu na stronie, gdy połączony z klientem.", "createInternalResourceDialogTargetConfiguration": "Konfiguracja celu", - "createInternalResourceDialogDestinationIP": "IP docelowe", - "createInternalResourceDialogDestinationIPDescription": "Adres IP zasobu w sieci strony.", - "createInternalResourceDialogDestinationPort": "Port docelowy", + "createInternalResourceDialogDestinationIPDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.", "createInternalResourceDialogDestinationPortDescription": "Port na docelowym IP, gdzie zasób jest dostępny.", "createInternalResourceDialogCancel": "Anuluj", "createInternalResourceDialogCreateResource": "Utwórz zasób", @@ -1496,5 +1500,24 @@ "convertButton": "Konwertuj ten węzeł do zarządzanego samodzielnie" }, "internationaldomaindetected": "Wykryto międzynarodową domenę", - "willbestoredas": "Będą przechowywane jako:" + "willbestoredas": "Będą przechowywane jako:", + "idpGoogleDescription": "Dostawca Google OAuth2/OIDC", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Niestandardowe nagłówki", + "headersValidationError": "Nagłówki muszą być w formacie: Nazwa nagłówka: wartość.", + "domainPickerProvidedDomain": "Dostarczona domena", + "domainPickerFreeProvidedDomain": "Darmowa oferowana domena", + "domainPickerVerified": "Zweryfikowano", + "domainPickerUnverified": "Niezweryfikowane", + "domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.", + "domainPickerError": "Błąd", + "domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji", + "domainPickerErrorCheckAvailability": "Nie udało się sprawdzić dostępności domeny", + "domainPickerInvalidSubdomain": "Nieprawidłowa subdomena", + "domainPickerInvalidSubdomainRemoved": "Wejście \"{sub}\" zostało usunięte, ponieważ jest nieprawidłowe.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" nie może być poprawne dla {domain}.", + "domainPickerSubdomainSanitized": "Poddomena oczyszczona", + "domainPickerSubdomainCorrected": "\"{sub}\" został skorygowany do \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Edytuj plik: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Edytuj plik: docker-compose.yml" } diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 84afb6aa..9fd73d49 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Ocorreu um erro ao adicionar usuário à função.", "userSaved": "Usuário salvo", "userSavedDescription": "O usuário foi atualizado.", + "autoProvisioned": "Auto provisionado", + "autoProvisionedDescription": "Permitir que este usuário seja gerenciado automaticamente pelo provedor de identidade", "accessControlsDescription": "Gerencie o que este usuário pode acessar e fazer na organização", "accessControlsSubmit": "Salvar Controles de Acesso", "roles": "Funções", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Formato de endereço IP inválido", "ipAddressErrorInvalidOctet": "Octeto de endereço IP inválido", "path": "Caminho", + "matchPath": "Correspondência de caminho", "ipAddressRange": "Faixa de IP", "rulesErrorFetch": "Falha ao buscar regras", "rulesErrorFetchDescription": "Ocorreu um erro ao buscar regras", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Conectado", "idpErrorConnectingTo": "Ocorreu um problema ao ligar a {name}. Por favor, contacte o seu administrador.", "idpErrorNotFound": "IdP não encontrado", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Convite Inválido", "inviteInvalidDescription": "O link do convite é inválido.", "inviteErrorWrongUser": "O convite não é para este usuário", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Edição Profissional Necessária", "licenseTierProfessionalRequiredDescription": "Esta funcionalidade só está disponível na Edição Profissional.", "actionGetOrg": "Obter Organização", + "updateOrgUser": "Atualizar usuário Org", + "createOrgUser": "Criar usuário Org", "actionUpdateOrg": "Atualizar Organização", "actionUpdateUser": "Atualizar Usuário", "actionGetUser": "Obter Usuário", @@ -991,6 +998,7 @@ "actionDeleteSite": "Eliminar Site", "actionGetSite": "Obter Site", "actionListSites": "Listar Sites", + "actionApplyBlueprint": "Aplicar Diagrama", "setupToken": "Configuração do Token", "setupTokenDescription": "Digite o token de configuração do console do servidor.", "setupTokenRequired": "Token de configuração é necessário", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Tipo:", "sidebarClients": "Clientes (Beta)", "sidebarDomains": "Domínios", - "enableDockerSocket": "Habilitar Docker Socket", - "enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao Newt.", + "enableDockerSocket": "Habilitar o Diagrama Docker", + "enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.", "enableDockerSocketLink": "Saiba mais", "viewDockerContainers": "Ver contêineres Docker", "containersIn": "Contêineres em {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Nova Atualização Disponível", "newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.", "domainPickerEnterDomain": "Domínio", - "domainPickerPlaceholder": "meuapp.exemplo.com, api.v1.meudominio.com, ou apenas meuapp", + "domainPickerPlaceholder": "myapp.exemplo.com", "domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.", "domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis", "domainPickerTabAll": "Todos", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protocolo", "editInternalResourceDialogSitePort": "Porta do Site", "editInternalResourceDialogTargetConfiguration": "Configuração do Alvo", - "editInternalResourceDialogDestinationIP": "IP de Destino", - "editInternalResourceDialogDestinationPort": "Porta de Destino", "editInternalResourceDialogCancel": "Cancelar", "editInternalResourceDialogSaveResource": "Salvar Recurso", "editInternalResourceDialogSuccess": "Sucesso", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Porta do Site", "createInternalResourceDialogSitePortDescription": "Use esta porta para acessar o recurso no site quando conectado com um cliente.", "createInternalResourceDialogTargetConfiguration": "Configuração do Alvo", - "createInternalResourceDialogDestinationIP": "IP de Destino", - "createInternalResourceDialogDestinationIPDescription": "O endereço IP do recurso na rede do site.", - "createInternalResourceDialogDestinationPort": "Porta de Destino", + "createInternalResourceDialogDestinationIPDescription": "O IP ou endereço do hostname do recurso na rede do site.", "createInternalResourceDialogDestinationPortDescription": "A porta no IP de destino onde o recurso está acessível.", "createInternalResourceDialogCancel": "Cancelar", "createInternalResourceDialogCreateResource": "Criar Recurso", @@ -1496,5 +1500,24 @@ "convertButton": "Converter este nó para Auto-Hospedado Gerenciado" }, "internationaldomaindetected": "Domínio Internacional Detectado", - "willbestoredas": "Será armazenado como:" + "willbestoredas": "Será armazenado como:", + "idpGoogleDescription": "Provedor Google OAuth2/OIDC", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Cabeçalhos Personalizados", + "headersValidationError": "Cabeçalhos devem estar no formato: Nome do Cabeçalho: valor.", + "domainPickerProvidedDomain": "Domínio fornecido", + "domainPickerFreeProvidedDomain": "Domínio fornecido grátis", + "domainPickerVerified": "Verificada", + "domainPickerUnverified": "Não verificado", + "domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.", + "domainPickerError": "ERRO", + "domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização", + "domainPickerErrorCheckAvailability": "Não foi possível verificar a disponibilidade do domínio", + "domainPickerInvalidSubdomain": "Subdomínio inválido", + "domainPickerInvalidSubdomainRemoved": "A entrada \"{sub}\" foi removida porque ela não é válida.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" não pôde ser válido para {domain}.", + "domainPickerSubdomainSanitized": "Subdomínio banalizado", + "domainPickerSubdomainCorrected": "\"{sub}\" foi corrigido para \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Editar arquivo: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Editar arquivo: docker-compose.yml" } diff --git a/messages/ru-RU.json b/messages/ru-RU.json index ffcbe8dc..9c38cc11 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Произошла ошибка при добавлении пользователя в роль.", "userSaved": "Пользователь сохранён", "userSavedDescription": "Пользователь был обновлён.", + "autoProvisioned": "Автоподбор", + "autoProvisionedDescription": "Разрешить автоматическое управление этим пользователем", "accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации", "accessControlsSubmit": "Сохранить контроль доступа", "roles": "Роли", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Неверный формат IP адреса", "ipAddressErrorInvalidOctet": "Неверный октет IP адреса", "path": "Путь", + "matchPath": "Путь матча", "ipAddressRange": "Диапазон IP", "rulesErrorFetch": "Не удалось получить правила", "rulesErrorFetchDescription": "Произошла ошибка при получении правил", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Подключено", "idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.", "idpErrorNotFound": "IdP не найден", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Недействительное приглашение", "inviteInvalidDescription": "Ссылка на приглашение недействительна.", "inviteErrorWrongUser": "Приглашение не для этого пользователя", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Требуется профессиональная версия", "licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.", "actionGetOrg": "Получить организацию", + "updateOrgUser": "Обновить пользователя Org", + "createOrgUser": "Создать пользователя Org", "actionUpdateOrg": "Обновить организацию", "actionUpdateUser": "Обновить пользователя", "actionGetUser": "Получить пользователя", @@ -991,6 +998,7 @@ "actionDeleteSite": "Удалить сайт", "actionGetSite": "Получить сайт", "actionListSites": "Список сайтов", + "actionApplyBlueprint": "Применить чертёж", "setupToken": "Код настройки", "setupTokenDescription": "Введите токен настройки из консоли сервера.", "setupTokenRequired": "Токен настройки обязателен", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Лицензия", "sidebarClients": "Клиенты (бета)", "sidebarDomains": "Домены", - "enableDockerSocket": "Включить Docker Socket", - "enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.", + "enableDockerSocket": "Включить чертёж Docker", + "enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.", "enableDockerSocketLink": "Узнать больше", "viewDockerContainers": "Просмотр контейнеров Docker", "containersIn": "Контейнеры в {siteName}", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Доступно обновление", "newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.", "domainPickerEnterDomain": "Домен", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, или просто myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.", "domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции", "domainPickerTabAll": "Все", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Протокол", "editInternalResourceDialogSitePort": "Порт сайта", "editInternalResourceDialogTargetConfiguration": "Настройка цели", - "editInternalResourceDialogDestinationIP": "Целевая IP", - "editInternalResourceDialogDestinationPort": "Целевой порт", "editInternalResourceDialogCancel": "Отмена", "editInternalResourceDialogSaveResource": "Сохранить ресурс", "editInternalResourceDialogSuccess": "Успешно", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Порт сайта", "createInternalResourceDialogSitePortDescription": "Используйте этот порт для доступа к ресурсу на сайте при подключении с клиентом.", "createInternalResourceDialogTargetConfiguration": "Настройка цели", - "createInternalResourceDialogDestinationIP": "Целевая IP", - "createInternalResourceDialogDestinationIPDescription": "IP-адрес ресурса в сети сайта.", - "createInternalResourceDialogDestinationPort": "Целевой порт", + "createInternalResourceDialogDestinationIPDescription": "IP или адрес хоста ресурса в сети сайта.", "createInternalResourceDialogDestinationPortDescription": "Порт на IP-адресе назначения, где доступен ресурс.", "createInternalResourceDialogCancel": "Отмена", "createInternalResourceDialogCreateResource": "Создать ресурс", @@ -1496,5 +1500,24 @@ "convertButton": "Конвертировать этот узел в управляемый себе-хост" }, "internationaldomaindetected": "Обнаружен международный домен", - "willbestoredas": "Будет храниться как:" + "willbestoredas": "Будет храниться как:", + "idpGoogleDescription": "Google OAuth2/OIDC провайдер", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "Пользовательские заголовки", + "headersValidationError": "Заголовки должны быть в формате: Название заголовка: значение.", + "domainPickerProvidedDomain": "Домен предоставлен", + "domainPickerFreeProvidedDomain": "Бесплатный домен", + "domainPickerVerified": "Подтверждено", + "domainPickerUnverified": "Не подтверждено", + "domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.", + "domainPickerError": "Ошибка", + "domainPickerErrorLoadDomains": "Не удалось загрузить домены организации", + "domainPickerErrorCheckAvailability": "Не удалось проверить доступность домена", + "domainPickerInvalidSubdomain": "Неверный поддомен", + "domainPickerInvalidSubdomainRemoved": "Ввод \"{sub}\" был удален, потому что он недействителен.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" не может быть действительным для {domain}.", + "domainPickerSubdomainSanitized": "Субдомен очищен", + "domainPickerSubdomainCorrected": "\"{sub}\" был исправлен на \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "Редактировать файл: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Редактировать файл: docker-compose.yml" } diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 2253dab2..ef812850 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "Kullanıcı role eklenirken bir hata oluştu.", "userSaved": "Kullanıcı kaydedildi", "userSavedDescription": "Kullanıcı güncellenmiştir.", + "autoProvisioned": "Otomatik Sağlandı", + "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", "roles": "Roller", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "Geçersiz IP adresi formatı", "ipAddressErrorInvalidOctet": "Geçersiz IP adresi okteti", "path": "Yol", + "matchPath": "Yol Eşleştir", "ipAddressRange": "IP Aralığı", "rulesErrorFetch": "Kurallar alınamadı", "rulesErrorFetchDescription": "Kurallar alınırken bir hata oluştu", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "Bağlandı", "idpErrorConnectingTo": "{name} ile bağlantı kurarken bir sorun meydana geldi. Lütfen yöneticiye danışın.", "idpErrorNotFound": "IdP bulunamadı", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "Geçersiz Davet", "inviteInvalidDescription": "Davet bağlantısı geçersiz.", "inviteErrorWrongUser": "Davet bu kullanıcı için değil", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "Profesyonel Sürüme Gereklidir", "licenseTierProfessionalRequiredDescription": "Bu özellik yalnızca Professional Edition'da kullanılabilir.", "actionGetOrg": "Kuruluşu Al", + "updateOrgUser": "Organizasyon Kullanıcısını Güncelle", + "createOrgUser": "Organizasyon Kullanıcısı Oluştur", "actionUpdateOrg": "Kuruluşu Güncelle", "actionUpdateUser": "Kullanıcıyı Güncelle", "actionGetUser": "Kullanıcıyı Getir", @@ -991,6 +998,7 @@ "actionDeleteSite": "Siteyi Sil", "actionGetSite": "Siteyi Al", "actionListSites": "Siteleri Listele", + "actionApplyBlueprint": "Planı Uygula", "setupToken": "Kurulum Simgesi", "setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.", "setupTokenRequired": "Kurulum simgesi gerekli", @@ -1133,8 +1141,8 @@ "sidebarLicense": "Lisans", "sidebarClients": "Müşteriler (Beta)", "sidebarDomains": "Alan Adları", - "enableDockerSocket": "Docker Soketi Etkinleştir", - "enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.", + "enableDockerSocket": "Docker Soketini Etkinleştir", + "enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.", "enableDockerSocketLink": "Daha fazla bilgi", "viewDockerContainers": "Docker Konteynerlerini Görüntüle", "containersIn": "{siteName} içindeki konteynerler", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "Güncelleme Mevcut", "newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.", "domainPickerEnterDomain": "Domain", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com veya sadece myapp", + "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.", "domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin", "domainPickerTabAll": "Tümü", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "Protokol", "editInternalResourceDialogSitePort": "Site Bağlantı Noktası", "editInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma", - "editInternalResourceDialogDestinationIP": "Hedef IP", - "editInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası", "editInternalResourceDialogCancel": "İptal", "editInternalResourceDialogSaveResource": "Kaynağı Kaydet", "editInternalResourceDialogSuccess": "Başarı", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "Site Bağlantı Noktası", "createInternalResourceDialogSitePortDescription": "İstemci ile bağlanıldığında site üzerindeki kaynağa erişmek için bu bağlantı noktasını kullanın.", "createInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma", - "createInternalResourceDialogDestinationIP": "Hedef IP", - "createInternalResourceDialogDestinationIPDescription": "Site ağındaki kaynağın IP adresi.", - "createInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası", + "createInternalResourceDialogDestinationIPDescription": "Kaynağın site ağındaki IP veya ana bilgisayar adresi.", "createInternalResourceDialogDestinationPortDescription": "Kaynağa erişilebilecek hedef IP üzerindeki bağlantı noktası.", "createInternalResourceDialogCancel": "İptal", "createInternalResourceDialogCreateResource": "Kaynak Oluştur", @@ -1496,5 +1500,24 @@ "convertButton": "Bu Düğümü Yönetilen Kendi Kendine Barındırma Dönüştürün" }, "internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi", - "willbestoredas": "Şu şekilde depolanacak:" + "willbestoredas": "Şu şekilde depolanacak:", + "idpGoogleDescription": "Google OAuth2/OIDC sağlayıcısı", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC sağlayıcısı", + "customHeaders": "Özel Başlıklar", + "headersValidationError": "Başlıklar şu formatta olmalıdır: Başlık-Adı: değer.", + "domainPickerProvidedDomain": "Sağlanan Alan Adı", + "domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı", + "domainPickerVerified": "Doğrulandı", + "domainPickerUnverified": "Doğrulanmadı", + "domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.", + "domainPickerError": "Hata", + "domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi", + "domainPickerErrorCheckAvailability": "Alan adı kullanılabilirliği kontrol edilemedi", + "domainPickerInvalidSubdomain": "Geçersiz alt alan adı", + "domainPickerInvalidSubdomainRemoved": "Girdi \"{sub}\" geçersiz olduğu için kaldırıldı.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" {domain} için geçerli yapılamadı.", + "domainPickerSubdomainSanitized": "Alt alan adı temizlendi", + "domainPickerSubdomainCorrected": "\"{sub}\" \"{sanitized}\" olarak düzeltildi", + "resourceAddEntrypointsEditFile": "Dosyayı düzenle: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Dosyayı düzenle: docker-compose.yml" } diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 1eaa2263..c78d7460 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -454,6 +454,8 @@ "accessRoleErrorAddDescription": "添加用户到角色时出错。", "userSaved": "用户已保存", "userSavedDescription": "用户已更新。", + "autoProvisioned": "自动设置", + "autoProvisionedDescription": "允许此用户由身份提供商自动管理", "accessControlsDescription": "管理此用户在组织中可以访问和做什么", "accessControlsSubmit": "保存访问控制", "roles": "角色", @@ -511,6 +513,7 @@ "ipAddressErrorInvalidFormat": "无效的 IP 地址格式", "ipAddressErrorInvalidOctet": "无效的 IP 地址", "path": "路径", + "matchPath": "匹配路径", "ipAddressRange": "IP 范围", "rulesErrorFetch": "获取规则失败", "rulesErrorFetchDescription": "获取规则时出错", @@ -911,6 +914,8 @@ "idpConnectingToFinished": "已连接", "idpErrorConnectingTo": "无法连接到 {name},请联系管理员协助处理。", "idpErrorNotFound": "找不到 IdP", + "idpGoogleAlt": "Google", + "idpAzureAlt": "Azure", "inviteInvalid": "无效邀请", "inviteInvalidDescription": "邀请链接无效。", "inviteErrorWrongUser": "邀请不是该用户的", @@ -982,6 +987,8 @@ "licenseTierProfessionalRequired": "需要专业版", "licenseTierProfessionalRequiredDescription": "此功能仅在专业版可用。", "actionGetOrg": "获取组织", + "updateOrgUser": "更新组织用户", + "createOrgUser": "创建组织用户", "actionUpdateOrg": "更新组织", "actionUpdateUser": "更新用户", "actionGetUser": "获取用户", @@ -991,6 +998,7 @@ "actionDeleteSite": "删除站点", "actionGetSite": "获取站点", "actionListSites": "站点列表", + "actionApplyBlueprint": "应用蓝图", "setupToken": "设置令牌", "setupTokenDescription": "从服务器控制台输入设置令牌。", "setupTokenRequired": "需要设置令牌", @@ -1133,8 +1141,8 @@ "sidebarLicense": "证书", "sidebarClients": "客户端(测试版)", "sidebarDomains": "域", - "enableDockerSocket": "启用停靠套接字", - "enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。", + "enableDockerSocket": "启用 Docker 蓝图", + "enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。", "enableDockerSocketLink": "了解更多", "viewDockerContainers": "查看停靠容器", "containersIn": "{siteName} 中的容器", @@ -1234,7 +1242,7 @@ "newtUpdateAvailable": "更新可用", "newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。", "domainPickerEnterDomain": "域名", - "domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp", + "domainPickerPlaceholder": "example.com", "domainPickerDescription": "输入资源的完整域名以查看可用选项。", "domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。", "domainPickerTabAll": "所有", @@ -1392,8 +1400,6 @@ "editInternalResourceDialogProtocol": "协议", "editInternalResourceDialogSitePort": "站点端口", "editInternalResourceDialogTargetConfiguration": "目标配置", - "editInternalResourceDialogDestinationIP": "目标IP", - "editInternalResourceDialogDestinationPort": "目标端口", "editInternalResourceDialogCancel": "取消", "editInternalResourceDialogSaveResource": "保存资源", "editInternalResourceDialogSuccess": "成功", @@ -1424,9 +1430,7 @@ "createInternalResourceDialogSitePort": "站点端口", "createInternalResourceDialogSitePortDescription": "使用此端口在连接到客户端时访问站点上的资源。", "createInternalResourceDialogTargetConfiguration": "目标配置", - "createInternalResourceDialogDestinationIP": "目标IP", - "createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP地址。", - "createInternalResourceDialogDestinationPort": "目标端口", + "createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP或主机名地址。", "createInternalResourceDialogDestinationPortDescription": "资源在目标IP上可访问的端口。", "createInternalResourceDialogCancel": "取消", "createInternalResourceDialogCreateResource": "创建资源", @@ -1496,5 +1500,24 @@ "convertButton": "将此节点转换为管理自托管的" }, "internationaldomaindetected": "检测到国际域", - "willbestoredas": "储存为:" + "willbestoredas": "储存为:", + "idpGoogleDescription": "Google OAuth2/OIDC 提供商", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", + "customHeaders": "自定义标题", + "headersValidationError": "头部必须是格式:头部名称:值。", + "domainPickerProvidedDomain": "提供的域", + "domainPickerFreeProvidedDomain": "免费提供的域", + "domainPickerVerified": "已验证", + "domainPickerUnverified": "未验证", + "domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。", + "domainPickerError": "错误", + "domainPickerErrorLoadDomains": "加载组织域名失败", + "domainPickerErrorCheckAvailability": "检查域可用性失败", + "domainPickerInvalidSubdomain": "无效的子域", + "domainPickerInvalidSubdomainRemoved": "输入 \"{sub}\" 已被移除,因为其无效。", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" 无法为 {domain} 变为有效。", + "domainPickerSubdomainSanitized": "子域已净化", + "domainPickerSubdomainCorrected": "\"{sub}\" 已被更正为 \"{sanitized}\"", + "resourceAddEntrypointsEditFile": "编辑文件:config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "编辑文件:docker-compose.yml" } diff --git a/package-lock.json b/package-lock.json index 2d8db128..931e3178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.4", - "@hookform/resolvers": "3.9.1", + "@hookform/resolvers": "4.1.3", "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", @@ -32,15 +32,15 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "^1.2.8", - "@react-email/components": "0.5.0", + "@react-email/components": "0.5.3", "@react-email/render": "^1.2.0", "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.1.0", + "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^9.0.3", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", - "axios": "1.11.0", + "axios": "^1.12.2", "better-sqlite3": "11.7.0", "canvas-confetti": "1.9.3", "class-variance-authority": "^0.7.1", @@ -51,11 +51,11 @@ "cookies": "^0.9.1", "cors": "2.8.5", "crypto-js": "^4.2.0", - "drizzle-orm": "0.44.4", - "eslint": "9.33.0", - "eslint-config-next": "15.4.6", + "drizzle-orm": "0.44.5", + "eslint": "9.35.0", + "eslint-config-next": "15.5.3", "express": "5.1.0", - "express-rate-limit": "8.0.1", + "express-rate-limit": "8.1.0", "glob": "11.0.3", "helmet": "8.1.0", "http-errors": "2.0.0", @@ -64,30 +64,29 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "0.539.0", + "lucide-react": "^0.544.0", "moment": "2.30.1", - "next": "15.4.6", - "next-intl": "^4.3.4", + "next": "15.5.3", + "next-intl": "^4.3.9", "next-themes": "0.4.6", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.5", - "npm": "^11.5.2", + "nodemailer": "7.0.6", + "npm": "^11.6.0", "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", "react": "19.1.1", "react-dom": "19.1.1", - "react-easy-sort": "^1.6.0", + "react-easy-sort": "^1.7.0", "react-hook-form": "7.62.0", "react-icons": "^5.5.0", "rebuild": "0.1.2", "semver": "^7.7.2", - "source-map-support": "0.5.21", "swagger-ui-express": "^5.0.1", "tailwind-merge": "3.3.1", - "tw-animate-css": "^1.3.7", - "uuid": "^11.1.0", + "tw-animate-css": "^1.3.8", + "uuid": "^13.0.0", "vaul": "1.1.2", "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", @@ -97,9 +96,9 @@ "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.49.0", + "@dotenvx/dotenvx": "1.49.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "^4.1.12", + "@tailwindcss/postcss": "^4.1.13", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", @@ -109,25 +108,25 @@ "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24", - "@types/nodemailer": "6.4.17", + "@types/node": "24.5.2", + "@types/nodemailer": "7.0.1", "@types/pg": "8.15.5", - "@types/react": "19.1.12", + "@types/react": "19.1.13", "@types/react-dom": "19.1.9", - "@types/semver": "^7.7.0", + "@types/semver": "^7.7.1", "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", "drizzle-kit": "0.31.4", - "esbuild": "0.25.9", + "esbuild": "0.25.10", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.2.8", + "react-email": "4.2.11", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", "tsx": "4.20.5", "typescript": "^5", - "typescript-eslint": "^8.40.0" + "typescript-eslint": "^8.44.0" } }, "node_modules/@alloc/quick-lru": { @@ -155,6 +154,731 @@ "zod": "^3.20.2" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.888.0.tgz", + "integrity": "sha512-Zy7AXvj4oVLE5Zkj61qYZxIFgJXbRgTmFJvQ/EqgxE87KPR9+gF5wtC3iqcKEmkqFlWlxWrlhV4K70Vqqj4bZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/credential-provider-node": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/signature-v4-multi-region": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.888.0.tgz", + "integrity": "sha512-8CLy/ehGKUmekjH+VtZJ4w40PqDg3u0K7uPziq/4P8Q7LLgsy8YQoHNbuY4am7JU3HWrqLXJI9aaz1+vPGPoWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.888.0.tgz", + "integrity": "sha512-L3S2FZywACo4lmWv37Y4TbefuPJ1fXWyWwIJ3J4wkPYFJ47mmtUPqThlVrSbdTHkEjnZgJe5cRfxk0qCLsFh1w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@aws-sdk/xml-builder": "3.887.0", + "@smithy/core": "^3.11.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.888.0.tgz", + "integrity": "sha512-shPi4AhUKbIk7LugJWvNpeZA8va7e5bOHAEKo89S0Ac8WDZt2OaNzbh/b9l0iSL2eEyte8UgIsYGcFxOwIF1VA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.888.0.tgz", + "integrity": "sha512-Jvuk6nul0lE7o5qlQutcqlySBHLXOyoPtiwE6zyKbGc7RVl0//h39Lab7zMeY2drMn8xAnIopL4606Fd8JI/Hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.888.0.tgz", + "integrity": "sha512-M82ItvS5yq+tO6ZOV1ruaVs2xOne+v8HW85GFCXnz8pecrzYdgxh6IsVqEbbWruryG/mUGkWMbkBZoEsy4MgyA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/credential-provider-env": "3.888.0", + "@aws-sdk/credential-provider-http": "3.888.0", + "@aws-sdk/credential-provider-process": "3.888.0", + "@aws-sdk/credential-provider-sso": "3.888.0", + "@aws-sdk/credential-provider-web-identity": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.888.0.tgz", + "integrity": "sha512-KCrQh1dCDC8Y+Ap3SZa6S81kHk+p+yAaOQ5jC3dak4zhHW3RCrsGR/jYdemTOgbEGcA6ye51UbhWfrrlMmeJSA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.888.0", + "@aws-sdk/credential-provider-http": "3.888.0", + "@aws-sdk/credential-provider-ini": "3.888.0", + "@aws-sdk/credential-provider-process": "3.888.0", + "@aws-sdk/credential-provider-sso": "3.888.0", + "@aws-sdk/credential-provider-web-identity": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.888.0.tgz", + "integrity": "sha512-+aX6piSukPQ8DUS4JAH344GePg8/+Q1t0+kvSHAZHhYvtQ/1Zek3ySOJWH2TuzTPCafY4nmWLcQcqvU1w9+4Lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.888.0.tgz", + "integrity": "sha512-b1ZJji7LJ6E/j1PhFTyvp51in2iCOQ3VP6mj5H6f5OUnqn7efm41iNMoinKr87n0IKZw7qput5ggXVxEdPhouA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.888.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/token-providers": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.888.0.tgz", + "integrity": "sha512-7P0QNtsDzMZdmBAaY/vY1BsZHwTGvEz3bsn2bm5VSKFAeMmZqsHK1QeYdNsFjLtegnVh+wodxMq50jqLv3LFlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.887.0.tgz", + "integrity": "sha512-ulzqXv6NNqdu/kr0sgBYupWmahISHY+azpJidtK6ZwQIC+vBUk9NdZeqQpy7KVhIk2xd4+5Oq9rxapPwPI21CA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.887.0.tgz", + "integrity": "sha512-YbbgLI6jKp2qSoAcHnXrQ5jcuc5EYAmGLVFgMVdk8dfCfJLfGGSaOLxF4CXC7QYhO50s+mPPkhBYejCik02Kug==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.887.0.tgz", + "integrity": "sha512-tjrUXFtQnFLo+qwMveq5faxP5MQakoLArXtqieHphSqZTXm21wDJM73hgT4/PQQGTwgYjDKqnqsE1hvk0hcfDw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.888.0.tgz", + "integrity": "sha512-rKOFNfqgqOfrdcLGF8fcO75azWS2aq2ksRHFoIEFru5FJxzu/yDAhY4C2FKiP/X34xeIUS2SbE/gQgrgWHSN2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/core": "^3.11.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.888.0.tgz", + "integrity": "sha512-ZkcUkoys8AdrNNG7ATjqw2WiXqrhTvT+r4CIK3KhOqIGPHX0p0DQWzqjaIl7ZhSUToKoZ4Ud7MjF795yUr73oA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@smithy/core": "^3.11.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.888.0.tgz", + "integrity": "sha512-py4o4RPSGt+uwGvSBzR6S6cCBjS4oTX5F8hrHFHfPCdIOMVjyOBejn820jXkCrcdpSj3Qg1yUZXxsByvxc9Lyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.887.0.tgz", + "integrity": "sha512-VdSMrIqJ3yjJb/fY+YAxrH/lCVv0iL8uA+lbMNfQGtO5tB3Zx6SU9LEpUwBNX8fPK1tUpI65CNE4w42+MY/7Mg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.888.0.tgz", + "integrity": "sha512-FmOHUaJzEhqfcpyh0L7HLwYcYopK13Dbmuf+oUyu56/RoeB1nLnltH1VMQVj8v3Am2IwlGR+/JpFyrdkErN+cA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.888.0.tgz", + "integrity": "sha512-WA3NF+3W8GEuCMG1WvkDYbB4z10G3O8xuhT7QSjhvLYWQ9CPt3w4VpVIfdqmUn131TCIbhCzD0KN/1VJTjAjyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.887.0.tgz", + "integrity": "sha512-fmTEJpUhsPsovQ12vZSpVTEP/IaRoJAMBGQXlQNjtCpkBp6Iq3KQDa/HDaPINE+3xxo6XvTdtibsNOd5zJLV9A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.873.0.tgz", + "integrity": "sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.887.0.tgz", + "integrity": "sha512-kpegvT53KT33BMeIcGLPA65CQVxLUL/C3gTz9AzlU/SDmeusBHX4nRApAicNzI/ltQ5lxZXbQn18UczzBuwF1w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.887.0.tgz", + "integrity": "sha512-X71UmVsYc6ZTH4KU6hA5urOzYowSXc3qvroagJNLJYU1ilgZ529lP4J9XOYfEvTXkLR1hPFSRxa43SrwgelMjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.888.0.tgz", + "integrity": "sha512-rSB3OHyuKXotIGfYEo//9sU0lXAUrTY28SUUnxzOGYuQsAt0XR5iYwBAp+RjV6x8f+Hmtbg0PdCsy1iNAXa0UQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.887.0.tgz", + "integrity": "sha512-lMwgWK1kNgUhHGfBvO/5uLe7TKhycwOn3eRCqsKPT9aPCx/HWuTlpcQp8oW2pCRGLS7qzcxqpQulcD+bbUL7XQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -302,9 +1026,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.49.0.tgz", - "integrity": "sha512-M1cyP6YstFQCjih54SAxCqHLMMi8QqV8tenpgGE48RTXWD7vfMYJiw/6xcCDpS2h28AcLpTsFCZA863Ge9yxzA==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.49.1.tgz", + "integrity": "sha512-LQ8cem3RU/mI2iz5Sy+ypnhfhVge3bc9tsLJg5rdf7j7u1RRTfmmSdLwSjeYI7sL9ToN7rgFkOGSBJqaBT+gSQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -831,9 +1555,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "cpu": [ "ppc64" ], @@ -848,9 +1572,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "cpu": [ "arm" ], @@ -865,9 +1589,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "cpu": [ "arm64" ], @@ -882,9 +1606,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "cpu": [ "x64" ], @@ -899,9 +1623,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "cpu": [ "arm64" ], @@ -916,9 +1640,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "cpu": [ "x64" ], @@ -933,9 +1657,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "cpu": [ "arm64" ], @@ -950,9 +1674,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "cpu": [ "x64" ], @@ -967,9 +1691,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "cpu": [ "arm" ], @@ -984,9 +1708,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "cpu": [ "arm64" ], @@ -1001,9 +1725,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "cpu": [ "ia32" ], @@ -1018,9 +1742,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "cpu": [ "loong64" ], @@ -1035,9 +1759,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "cpu": [ "mips64el" ], @@ -1052,9 +1776,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "cpu": [ "ppc64" ], @@ -1069,9 +1793,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "cpu": [ "riscv64" ], @@ -1086,9 +1810,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "cpu": [ "s390x" ], @@ -1103,9 +1827,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "cpu": [ "x64" ], @@ -1120,9 +1844,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", "cpu": [ "arm64" ], @@ -1137,9 +1861,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "cpu": [ "x64" ], @@ -1154,9 +1878,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", "cpu": [ "arm64" ], @@ -1171,9 +1895,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "cpu": [ "x64" ], @@ -1188,9 +1912,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", "cpu": [ "arm64" ], @@ -1205,9 +1929,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "cpu": [ "x64" ], @@ -1222,9 +1946,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "cpu": [ "arm64" ], @@ -1239,9 +1963,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "cpu": [ "ia32" ], @@ -1256,9 +1980,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "cpu": [ "x64" ], @@ -1273,9 +1997,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -1370,9 +2094,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1508,10 +2232,13 @@ "license": "MIT" }, "node_modules/@hookform/resolvers": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", - "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, "peerDependencies": { "react-hook-form": "^7.0.0" } @@ -2115,24 +2842,24 @@ } }, "node_modules/@next/env": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz", - "integrity": "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", + "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.6.tgz", - "integrity": "sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.3.tgz", + "integrity": "sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", - "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", + "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", "cpu": [ "arm64" ], @@ -2146,9 +2873,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", - "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", + "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", "cpu": [ "x64" ], @@ -2162,9 +2889,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", - "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", + "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", "cpu": [ "arm64" ], @@ -2178,9 +2905,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", - "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", + "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", "cpu": [ "arm64" ], @@ -2194,9 +2921,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", - "integrity": "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", + "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", "cpu": [ "x64" ], @@ -2210,9 +2937,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.6.tgz", - "integrity": "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", + "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", "cpu": [ "x64" ], @@ -2226,9 +2953,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", - "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", + "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", "cpu": [ "arm64" ], @@ -2242,9 +2969,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", - "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", + "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", "cpu": [ "x64" ], @@ -4110,9 +4837,9 @@ } }, "node_modules/@react-email/components": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.5.0.tgz", - "integrity": "sha512-esRbP+yMmSkNP9hcpiy2RwpDnvSmlxJcJ1HHbzSwlACGlCHTap+ma344QovvzhpVRhMccyWemdClLG822UvVpQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.5.3.tgz", + "integrity": "sha512-8G5vsoMehuGOT4cDqaYLdpagtqCYPl4vThXNylClxO6SrN2w9Mh1+i2RNGj/rdqh/woamHORjlXMYCA/kzDMew==", "license": "MIT", "dependencies": { "@react-email/body": "0.1.0", @@ -4130,7 +4857,7 @@ "@react-email/link": "0.0.12", "@react-email/markdown": "0.0.15", "@react-email/preview": "0.0.13", - "@react-email/render": "1.2.0", + "@react-email/render": "1.2.3", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", "@react-email/tailwind": "1.2.2", @@ -4264,9 +4991,9 @@ } }, "node_modules/@react-email/render": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.2.0.tgz", - "integrity": "sha512-5fpbV16VYR9Fmk8t7xiwPNAjxjdI8XzVtlx9J9OkhOsIHdr2s5DwAj8/MXzWa9qRYJyLirQ/l7rBSjjgyRAomw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.2.3.tgz", + "integrity": "sha512-qu3XYNkHGao3teJexVD5CrcgFkNLrzbZvpZN17a7EyQYUN3kHkTkE9saqY4VbvGx6QoNU3p8rsk/Xm++D/+pTw==", "license": "MIT", "dependencies": { "html-to-text": "^9.0.5", @@ -4397,8 +5124,646 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz", "integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT" }, + "node_modules/@smithy/abort-controller": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", + "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.1.tgz", + "integrity": "sha512-FXil8q4QN7mgKwU2hCLm0ltab8NyY/1RiqEf25Jnf6WLS3wmb11zGAoLETqg1nur2Aoibun4w4MjeN9CMJ4G6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.11.0.tgz", + "integrity": "sha512-Abs5rdP1o8/OINtE49wwNeWuynCu0kme1r4RI3VXVrHr4odVDG7h7mTnw1WXXfN5Il+c25QOnrdL2y56USfxkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.1.tgz", + "integrity": "sha512-1WdBfM9DwA59pnpIizxnUvBf/de18p4GP+6zP2AqrlFzoW3ERpZaT4QueBR0nS9deDMaQRkBlngpVlnkuuTisQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.1", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", + "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", + "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", + "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", + "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", + "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.1.tgz", + "integrity": "sha512-fUTMmQvQQZakXOuKizfu7fBLDpwvWZjfH6zUK2OLsoNZRZGbNUdNSdLJHpwk1vS208jtDjpUIskh+JoA8zMzZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/shared-ini-file-loader": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.2.1.tgz", + "integrity": "sha512-JzfvjwSJXWRl7LkLgIRTUTd2Wj639yr3sQGpViGNEOjtb0AkAuYqRAHs+jSOI/LPC0ZTjmFVVtfrCICMuebexw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/service-error-classification": "^4.1.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", + "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", + "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.1.tgz", + "integrity": "sha512-AIA0BJZq2h295J5NeCTKhg1WwtdTA/GqBCaVjk30bDgMHwniUETyh5cP9IiE9VrId7Kt8hS7zvREVMTv1VfA6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", + "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", + "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", + "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", + "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-uri-escape": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", + "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.1.tgz", + "integrity": "sha512-Iam75b/JNXyDE41UvrlM6n8DNOa/r1ylFyvgruTUx7h2Uk7vDNV9AAwP1vfL1fOL8ls0xArwEGVcGZVd7IO/Cw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.1.1.tgz", + "integrity": "sha512-YkpikhIqGc4sfXeIbzSj10t2bJI/sSoP5qxLue6zG+tEE3ngOBSm8sO3+djacYvS/R5DfpxN/L9CyZsvwjWOAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", + "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-uri-escape": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.1.tgz", + "integrity": "sha512-WolVLDb9UTPMEPPOncrCt6JmAMCSC/V2y5gst2STWJ5r7+8iNac+EFYQnmvDCYMfOLcilOSEpm5yXZXwbLak1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", + "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", + "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", + "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", + "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", + "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", + "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", + "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.1.tgz", + "integrity": "sha512-hA1AKIHFUMa9Tl6q6y8p0pJ9aWHCCG8s57flmIyLE0W7HcJeYrYtnqXDcGnftvXEhdQnSexyegXnzzTGk8bKLA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.1.tgz", + "integrity": "sha512-RGSpmoBrA+5D2WjwtK7tto6Pc2wO9KSXKLpLONhFZ8VyuCbqlLdiDAfuDTNY9AJe4JoE+Cx806cpTQQoQ71zPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.2.1", + "@smithy/credential-provider-imds": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.1.tgz", + "integrity": "sha512-qB4R9kO0SetA11Rzu6MVGFIaGYX3p6SGGGfWwsKnC6nXIf0n/0AKVwRTsYsz9ToN8CeNNtNgQRwKFBndGJZdyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", + "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", + "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.1.tgz", + "integrity": "sha512-jGeybqEZ/LIordPLMh5bnmnoIgsqnp4IEimmUp5c5voZ8yx+5kAlN5+juyr7p+f7AtZTgvhmInQk4Q0UVbrZ0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.1.tgz", + "integrity": "sha512-khKkW/Jqkgh6caxMWbMuox9+YfGlsk9OnHOYCGVEdYQb/XVzcORXHLYUubHmmda0pubEDncofUrPNniS9d+uAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", + "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", + "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -4406,6 +5771,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -4428,9 +5799,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", - "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", "dev": true, "license": "MIT", "dependencies": { @@ -4438,25 +5809,15 @@ "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", - "magic-string": "^0.30.17", + "magic-string": "^0.30.18", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.12" - } - }, - "node_modules/@tailwindcss/node/node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" + "tailwindcss": "4.1.13" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", - "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4468,24 +5829,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.12", - "@tailwindcss/oxide-darwin-arm64": "4.1.12", - "@tailwindcss/oxide-darwin-x64": "4.1.12", - "@tailwindcss/oxide-freebsd-x64": "4.1.12", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", - "@tailwindcss/oxide-linux-x64-musl": "4.1.12", - "@tailwindcss/oxide-wasm32-wasi": "4.1.12", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", - "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", "cpu": [ "arm64" ], @@ -4500,9 +5861,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", - "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", "cpu": [ "arm64" ], @@ -4517,9 +5878,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", - "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", "cpu": [ "x64" ], @@ -4534,9 +5895,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", - "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", "cpu": [ "x64" ], @@ -4551,9 +5912,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", - "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", "cpu": [ "arm" ], @@ -4568,9 +5929,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", - "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", "cpu": [ "arm64" ], @@ -4585,9 +5946,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", - "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", "cpu": [ "arm64" ], @@ -4602,9 +5963,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", - "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", "cpu": [ "x64" ], @@ -4619,9 +5980,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", - "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", "cpu": [ "x64" ], @@ -4636,9 +5997,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", - "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -4726,9 +6087,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", - "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", "cpu": [ "arm64" ], @@ -4743,9 +6104,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", - "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", "cpu": [ "x64" ], @@ -4760,17 +6121,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz", - "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz", + "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.12", - "@tailwindcss/oxide": "4.1.12", + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", "postcss": "^8.4.41", - "tailwindcss": "4.1.12" + "tailwindcss": "4.1.13" } }, "node_modules/@tanstack/react-table": { @@ -4974,22 +6335,23 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/nodemailer": { - "version": "6.4.17", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", - "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", + "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", "dev": true, "license": "MIT", "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", "@types/node": "*" } }, @@ -5020,9 +6382,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", - "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", + "version": "19.1.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", + "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5040,9 +6402,9 @@ } }, "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true, "license": "MIT" }, @@ -5086,6 +6448,13 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -5114,16 +6483,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", - "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", + "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/type-utils": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/type-utils": "8.44.0", + "@typescript-eslint/utils": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -5137,7 +6506,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -5152,15 +6521,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", - "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", + "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "engines": { @@ -5176,13 +6545,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", + "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", + "@typescript-eslint/tsconfig-utils": "^8.44.0", + "@typescript-eslint/types": "^8.44.0", "debug": "^4.3.4" }, "engines": { @@ -5197,13 +6566,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", - "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", + "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0" + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5214,9 +6583,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", + "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5230,14 +6599,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", - "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", + "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/utils": "8.44.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -5254,9 +6623,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", + "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5267,15 +6636,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", + "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/project-service": "8.44.0", + "@typescript-eslint/tsconfig-utils": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5347,15 +6716,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", - "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", + "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0" + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5370,12 +6739,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", + "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/types": "8.44.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -6018,9 +7387,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -6137,6 +7506,13 @@ "node": ">=18" } }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -6193,6 +7569,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/bytes": { @@ -7090,9 +8467,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.44.4", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.4.tgz", - "integrity": "sha512-ZyzKFpTC/Ut3fIqc2c0dPZ6nhchQXriTsqTNs4ayRgl6sZcFlMs9QZKPSHXK4bdOf41GHGWf+FrpcDDYwW+W6Q==", + "version": "0.44.5", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz", + "integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -7621,9 +8998,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7634,32 +9011,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" } }, "node_modules/esbuild-node-externals": { @@ -7719,18 +9096,18 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7779,12 +9156,12 @@ } }, "node_modules/eslint-config-next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.6.tgz", - "integrity": "sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.3.tgz", + "integrity": "sha512-e6j+QhQFOr5pfsc8VJbuTD9xTXJaRvMHYjEeLPA2pFkheNlgPLCkxdvhxhfuM4KGcqSZj2qEnpHisdTVs3BxuQ==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.4.6", + "@next/eslint-plugin-next": "15.5.3", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -8207,9 +9584,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", - "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", + "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", "license": "MIT", "dependencies": { "ip-address": "10.0.1" @@ -8295,6 +9672,25 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -9684,9 +11080,8 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "devOptional": true, "license": "MIT", - "optional": true, - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10254,22 +11649,22 @@ } }, "node_modules/lucide-react": { - "version": "0.539.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz", - "integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==", + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/marked": { @@ -10604,12 +11999,12 @@ } }, "node_modules/next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/next/-/next-15.4.6.tgz", - "integrity": "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", + "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", "license": "MIT", "dependencies": { - "@next/env": "15.4.6", + "@next/env": "15.5.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -10622,14 +12017,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.4.6", - "@next/swc-darwin-x64": "15.4.6", - "@next/swc-linux-arm64-gnu": "15.4.6", - "@next/swc-linux-arm64-musl": "15.4.6", - "@next/swc-linux-x64-gnu": "15.4.6", - "@next/swc-linux-x64-musl": "15.4.6", - "@next/swc-win32-arm64-msvc": "15.4.6", - "@next/swc-win32-x64-msvc": "15.4.6", + "@next/swc-darwin-arm64": "15.5.3", + "@next/swc-darwin-x64": "15.5.3", + "@next/swc-linux-arm64-gnu": "15.5.3", + "@next/swc-linux-arm64-musl": "15.5.3", + "@next/swc-linux-x64-gnu": "15.5.3", + "@next/swc-linux-x64-musl": "15.5.3", + "@next/swc-win32-arm64-msvc": "15.5.3", + "@next/swc-win32-x64-msvc": "15.5.3", "sharp": "^0.34.3" }, "peerDependencies": { @@ -10656,9 +12051,9 @@ } }, "node_modules/next-intl": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz", - "integrity": "sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.9.tgz", + "integrity": "sha512-4oSROHlgy8a5Qr2vH69wxo9F6K0uc6nZM2GNzqSe6ET79DEzOmBeSijCRzD5txcI4i+XTGytu4cxFsDXLKEDpQ==", "funding": [ { "type": "individual", @@ -10669,7 +12064,7 @@ "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", - "use-intl": "^4.3.4" + "use-intl": "^4.3.9" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", @@ -10783,9 +12178,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", - "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz", + "integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -10802,9 +12197,9 @@ } }, "node_modules/npm": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.5.2.tgz", - "integrity": "sha512-qsEkHPw/Qdw4eA1kKVxsa5F6QeJCiLM1GaexGt/FpUpfiBxkLXVXIVtscOAeVWVe17pmYwD9Aji8dfsXR4r68w==", + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.0.tgz", + "integrity": "sha512-d/P7DbvYgYNde9Ehfeq99+13/E7E82PfZPw8uYZASr9sQ3ZhBBCA9cXSJRA1COfJ6jDLJ0K36UJnXQWhCvLXuQ==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -10883,8 +12278,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.3", - "@npmcli/config": "^10.3.1", + "@npmcli/arborist": "^9.1.4", + "@npmcli/config": "^10.4.0", "@npmcli/fs": "^4.0.0", "@npmcli/map-workspaces": "^4.0.2", "@npmcli/package-json": "^6.2.0", @@ -10908,11 +12303,11 @@ "is-cidr": "^5.1.1", "json-parse-even-better-errors": "^4.0.0", "libnpmaccess": "^10.0.1", - "libnpmdiff": "^8.0.6", - "libnpmexec": "^10.1.5", - "libnpmfund": "^7.0.6", + "libnpmdiff": "^8.0.7", + "libnpmexec": "^10.1.6", + "libnpmfund": "^7.0.7", "libnpmorg": "^8.0.0", - "libnpmpack": "^9.0.6", + "libnpmpack": "^9.0.7", "libnpmpublish": "^11.1.0", "libnpmsearch": "^9.0.0", "libnpmteam": "^8.0.1", @@ -11064,7 +12459,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.3", + "version": "9.1.4", "inBundle": true, "license": "ISC", "dependencies": { @@ -11111,7 +12506,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.3.1", + "version": "10.4.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -11991,11 +13386,11 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.6", + "version": "8.0.7", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/installed-package-contents": "^3.0.0", "binary-extensions": "^3.0.0", "diff": "^7.0.0", @@ -12009,11 +13404,11 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.5", + "version": "10.1.6", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/package-json": "^6.1.1", "@npmcli/run-script": "^9.0.1", "ci-info": "^4.0.0", @@ -12030,11 +13425,11 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.6", + "version": "7.0.7", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3" + "@npmcli/arborist": "^9.1.4" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -12053,11 +13448,11 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.6", + "version": "9.0.7", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.3", + "@npmcli/arborist": "^9.1.4", "@npmcli/run-script": "^9.0.1", "npm-package-arg": "^12.0.0", "pacote": "^21.0.0" @@ -14533,9 +15928,9 @@ } }, "node_modules/react-easy-sort": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.6.0.tgz", - "integrity": "sha512-zd9Nn90wVlZPEwJrpqElN87sf9GZnFR1StfjgNQVbSpR5QTSzCHjEYK6REuwq49Ip+76KOMSln9tg/ST2KLelg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.7.0.tgz", + "integrity": "sha512-82I63kXdawFhhlFrWPrI74DL48v2LKs7e7PLf5le2E/eIR9+XyCEdL4Pyjbru8XjvtQ60mPLb6oextc4PPR8Lg==", "license": "MIT", "dependencies": { "array-move": "^3.0.1", @@ -14556,15 +15951,14 @@ "license": "0BSD" }, "node_modules/react-email": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.2.8.tgz", - "integrity": "sha512-Eqzs/xZnS881oghPO/4CQ1cULyESuUhEjfYboXmYNOokXnJ6QP5GKKJZ6zjkg9SnKXxSrIxSo5PxzCI5jReJMA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.2.11.tgz", + "integrity": "sha512-/7TXRgsTrXcV1u7kc5ZXDVlPvZqEBaYcflMhE2FgWIJh3OHLjj2FqctFTgYcp0iwzbR59a7gzJLmSKyD0wYJEQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.27.0", "@babel/traverse": "^7.27.0", - "chalk": "^5.0.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "debounce": "^2.0.0", @@ -14587,19 +15981,6 @@ "node": ">=18.0.0" } }, - "node_modules/react-email/node_modules/chalk": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", - "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/react-email/node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -15571,6 +16952,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -15589,6 +16971,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -15897,6 +17280,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -15979,19 +17375,23 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", - "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -16279,9 +17679,9 @@ } }, "node_modules/tw-animate-css": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.7.tgz", - "integrity": "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", + "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Wombosvideo" @@ -16401,16 +17801,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", - "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", + "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.40.0", - "@typescript-eslint/parser": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0" + "@typescript-eslint/eslint-plugin": "8.44.0", + "@typescript-eslint/parser": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/utils": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -16443,9 +17843,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "devOptional": true, "license": "MIT" }, @@ -16523,9 +17923,9 @@ } }, "node_modules/use-intl": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.4.tgz", - "integrity": "sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.9.tgz", + "integrity": "sha512-bZu+h13HIgOvsoGleQtUe4E6gM49CRm+AH36KnJVB/qb1+Beo7jr7HNrR8YWH8oaOkQfGNm6vh0HTepxng8UTg==", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "^2.2.0", @@ -16574,16 +17974,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/vary": { diff --git a/package.json b/package.json index 2c1c3fca..f2370e52 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,13 @@ "db:clear-migrations": "rm -rf server/migrations", "build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", - "start": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", + "start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3005", "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs" }, "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.4", - "@hookform/resolvers": "3.9.1", + "@hookform/resolvers": "4.1.3", "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", @@ -49,15 +49,15 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "^1.2.8", - "@react-email/components": "0.5.0", + "@react-email/components": "0.5.3", "@react-email/render": "^1.2.0", "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.1.0", + "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^9.0.3", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", - "axios": "1.11.0", + "axios": "^1.12.2", "better-sqlite3": "11.7.0", "canvas-confetti": "1.9.3", "class-variance-authority": "^0.7.1", @@ -68,11 +68,11 @@ "cookies": "^0.9.1", "cors": "2.8.5", "crypto-js": "^4.2.0", - "drizzle-orm": "0.44.4", - "eslint": "9.33.0", - "eslint-config-next": "15.4.6", + "drizzle-orm": "0.44.5", + "eslint": "9.35.0", + "eslint-config-next": "15.5.3", "express": "5.1.0", - "express-rate-limit": "8.0.1", + "express-rate-limit": "8.1.0", "glob": "11.0.3", "helmet": "8.1.0", "http-errors": "2.0.0", @@ -81,30 +81,29 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "0.539.0", + "lucide-react": "^0.544.0", "moment": "2.30.1", - "next": "15.4.6", - "next-intl": "^4.3.4", + "next": "15.5.3", + "next-intl": "^4.3.9", "next-themes": "0.4.6", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.5", - "npm": "^11.5.2", + "nodemailer": "7.0.6", + "npm": "^11.6.0", "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", "react": "19.1.1", "react-dom": "19.1.1", - "react-easy-sort": "^1.6.0", + "react-easy-sort": "^1.7.0", "react-hook-form": "7.62.0", "react-icons": "^5.5.0", "rebuild": "0.1.2", "semver": "^7.7.2", - "source-map-support": "0.5.21", "swagger-ui-express": "^5.0.1", "tailwind-merge": "3.3.1", - "tw-animate-css": "^1.3.7", - "uuid": "^11.1.0", + "tw-animate-css": "^1.3.8", + "uuid": "^13.0.0", "vaul": "1.1.2", "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", @@ -114,9 +113,9 @@ "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.49.0", + "@dotenvx/dotenvx": "1.49.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "^4.1.12", + "@tailwindcss/postcss": "^4.1.13", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", @@ -126,25 +125,25 @@ "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24", - "@types/nodemailer": "6.4.17", + "@types/node": "24.5.2", + "@types/nodemailer": "7.0.1", "@types/pg": "8.15.5", - "@types/react": "19.1.12", + "@types/react": "19.1.13", "@types/react-dom": "19.1.9", - "@types/semver": "^7.7.0", + "@types/semver": "^7.7.1", "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", "drizzle-kit": "0.31.4", - "esbuild": "0.25.9", + "esbuild": "0.25.10", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.2.8", + "react-email": "4.2.11", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", "tsx": "4.20.5", "typescript": "^5", - "typescript-eslint": "^8.40.0" + "typescript-eslint": "^8.44.0" }, "overrides": { "emblor": { diff --git a/public/idp/azure.png b/public/idp/azure.png new file mode 100644 index 00000000..d6ec5baf Binary files /dev/null and b/public/idp/azure.png differ diff --git a/public/idp/google.png b/public/idp/google.png new file mode 100644 index 00000000..da097687 Binary files /dev/null and b/public/idp/google.png differ diff --git a/server/auth/actions.ts b/server/auth/actions.ts index b5e4bbb3..f020c2ff 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -101,7 +101,9 @@ export enum ActionsEnum { getApiKey = "getApiKey", createOrgDomain = "createOrgDomain", deleteOrgDomain = "deleteOrgDomain", - restartOrgDomain = "restartOrgDomain" + restartOrgDomain = "restartOrgDomain", + updateOrgUser = "updateOrgUser", + applyBlueprint = "applyBlueprint" } export async function checkUserActionPermission( diff --git a/server/db/names.ts b/server/db/names.ts index 41f4c170..2da38f10 100644 --- a/server/db/names.ts +++ b/server/db/names.ts @@ -1,6 +1,6 @@ import { join } from "path"; import { readFileSync } from "fs"; -import { db } from "@server/db"; +import { db, resources, siteResources } from "@server/db"; import { exitNodes, sites } from "@server/db"; import { eq, and } from "drizzle-orm"; import { __DIRNAME } from "@server/lib/consts"; @@ -34,6 +34,44 @@ export async function getUniqueSiteName(orgId: string): Promise { } } +export async function getUniqueResourceName(orgId: string): Promise { + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + const name = generateName(); + const count = await db + .select({ niceId: resources.niceId, orgId: resources.orgId }) + .from(resources) + .where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))); + if (count.length === 0) { + return name; + } + loops++; + } +} + +export async function getUniqueSiteResourceName(orgId: string): Promise { + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + const name = generateName(); + const count = await db + .select({ niceId: siteResources.niceId, orgId: siteResources.orgId }) + .from(siteResources) + .where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId))); + if (count.length === 0) { + return name; + } + loops++; + } +} + export async function getUniqueExitNodeEndpointName(): Promise { let loops = 0; const count = await db diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 9625867d..c7c292f0 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -50,3 +50,4 @@ function createDb() { export const db = createDb(); export default db; +export type Transaction = Parameters[0]>[0]; \ No newline at end of file diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 8e725ab1..3cb5486b 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -71,6 +71,7 @@ export const resources = pgTable("resources", { onDelete: "cascade" }) .notNull(), + niceId: text("niceId").notNull(), name: varchar("name").notNull(), subdomain: varchar("subdomain"), fullDomain: varchar("fullDomain"), @@ -95,6 +96,7 @@ export const resources = pgTable("resources", { skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { onDelete: "cascade" }), + headers: text("headers"), // comma-separated list of headers to add to the request }); export const targets = pgTable("targets", { @@ -113,7 +115,9 @@ export const targets = pgTable("targets", { method: varchar("method"), port: integer("port").notNull(), internalPort: integer("internalPort"), - enabled: boolean("enabled").notNull().default(true) + enabled: boolean("enabled").notNull().default(true), + path: text("path"), + pathMatchType: text("pathMatchType"), // exact, prefix, regex }); export const exitNodes = pgTable("exitNodes", { @@ -127,7 +131,8 @@ export const exitNodes = pgTable("exitNodes", { maxConnections: integer("maxConnections"), online: boolean("online").notNull().default(false), lastPing: integer("lastPing"), - type: text("type").default("gerbil") // gerbil, remoteExitNode + type: text("type").default("gerbil"), // gerbil, remoteExitNode + region: varchar("region") }); export const siteResources = pgTable("siteResources", { // this is for the clients @@ -138,6 +143,7 @@ export const siteResources = pgTable("siteResources", { // this is for the clien orgId: varchar("orgId") .notNull() .references(() => orgs.orgId, { onDelete: "cascade" }), + niceId: varchar("niceId").notNull(), name: varchar("name").notNull(), protocol: varchar("protocol").notNull(), proxyPort: integer("proxyPort").notNull(), @@ -212,7 +218,8 @@ export const userOrgs = pgTable("userOrgs", { roleId: integer("roleId") .notNull() .references(() => roles.roleId), - isOwner: boolean("isOwner").notNull().default(false) + isOwner: boolean("isOwner").notNull().default(false), + autoProvisioned: boolean("autoProvisioned").default(false) }); export const emailVerificationCodes = pgTable("emailVerificationCodes", { @@ -458,6 +465,7 @@ export const idpOidcConfig = pgTable("idpOidcConfig", { idpId: integer("idpId") .notNull() .references(() => idp.idpId, { onDelete: "cascade" }), + variant: varchar("variant").notNull().default("oidc"), clientId: varchar("clientId").notNull(), clientSecret: varchar("clientSecret").notNull(), authUrl: varchar("authUrl").notNull(), diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index 124bd885..6369c268 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -18,6 +18,7 @@ function createDb() { export const db = createDb(); export default db; +export type Transaction = Parameters[0]>[0]; function checkFileExists(filePath: string): boolean { try { diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 579ff7b4..c623fae3 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -77,6 +77,7 @@ export const resources = sqliteTable("resources", { onDelete: "cascade" }) .notNull(), + niceId: text("niceId").notNull(), name: text("name").notNull(), subdomain: text("subdomain"), fullDomain: text("fullDomain"), @@ -107,6 +108,7 @@ export const resources = sqliteTable("resources", { skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { onDelete: "cascade" }), + headers: text("headers"), // comma-separated list of headers to add to the request }); export const targets = sqliteTable("targets", { @@ -125,7 +127,9 @@ export const targets = sqliteTable("targets", { method: text("method"), port: integer("port").notNull(), internalPort: integer("internalPort"), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + path: text("path"), + pathMatchType: text("pathMatchType"), // exact, prefix, regex }); export const exitNodes = sqliteTable("exitNodes", { @@ -139,23 +143,28 @@ export const exitNodes = sqliteTable("exitNodes", { maxConnections: integer("maxConnections"), online: integer("online", { mode: "boolean" }).notNull().default(false), lastPing: integer("lastPing"), - type: text("type").default("gerbil") // gerbil, remoteExitNode + type: text("type").default("gerbil"), // gerbil, remoteExitNode + region: text("region") }); -export const siteResources = sqliteTable("siteResources", { // this is for the clients - siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }), +export const siteResources = sqliteTable("siteResources", { + // this is for the clients + siteResourceId: integer("siteResourceId").primaryKey({ + autoIncrement: true + }), siteId: integer("siteId") .notNull() .references(() => sites.siteId, { onDelete: "cascade" }), orgId: text("orgId") .notNull() .references(() => orgs.orgId, { onDelete: "cascade" }), + niceId: text("niceId").notNull(), name: text("name").notNull(), protocol: text("protocol").notNull(), proxyPort: integer("proxyPort").notNull(), destinationPort: integer("destinationPort").notNull(), destinationIp: text("destinationIp").notNull(), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) }); export const users = sqliteTable("user", { @@ -259,7 +268,9 @@ export const clientSites = sqliteTable("clientSites", { siteId: integer("siteId") .notNull() .references(() => sites.siteId, { onDelete: "cascade" }), - isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false), + isRelayed: integer("isRelayed", { mode: "boolean" }) + .notNull() + .default(false), endpoint: text("endpoint") }); @@ -317,7 +328,10 @@ export const userOrgs = sqliteTable("userOrgs", { roleId: integer("roleId") .notNull() .references(() => roles.roleId), - isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false) + isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false), + autoProvisioned: integer("autoProvisioned", { + mode: "boolean" + }).default(false) }); export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { @@ -594,6 +608,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { idpOauthConfigId: integer("idpOauthConfigId").primaryKey({ autoIncrement: true }), + variant: text("variant").notNull().default("oidc"), idpId: integer("idpId") .notNull() .references(() => idp.idpId, { onDelete: "cascade" }), diff --git a/server/index.ts b/server/index.ts index fb2ad396..5210ba5d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,5 @@ #! /usr/bin/env node import "./extendZod.ts"; -import 'source-map-support/register.js' import { runSetupFunctions } from "./setup"; import { createApiServer } from "./apiServer"; diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts new file mode 100644 index 00000000..47193420 --- /dev/null +++ b/server/lib/blueprints/applyBlueprint.ts @@ -0,0 +1,170 @@ +import { db, newts, Target } from "@server/db"; +import { Config, ConfigSchema } from "./types"; +import { ProxyResourcesResults, updateProxyResources } from "./proxyResources"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { resources, targets, sites } from "@server/db"; +import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; +import { addTargets as addProxyTargets } from "@server/routers/newt/targets"; +import { addTargets as addClientTargets } from "@server/routers/client/targets"; +import { + ClientResourcesResults, + updateClientResources +} from "./clientResources"; + +export async function applyBlueprint( + orgId: string, + configData: unknown, + siteId?: number +): Promise { + // Validate the input data + const validationResult = ConfigSchema.safeParse(configData); + if (!validationResult.success) { + throw new Error(fromError(validationResult.error).toString()); + } + + const config: Config = validationResult.data; + + try { + let proxyResourcesResults: ProxyResourcesResults = []; + let clientResourcesResults: ClientResourcesResults = []; + await db.transaction(async (trx) => { + proxyResourcesResults = await updateProxyResources( + orgId, + config, + trx, + siteId + ); + clientResourcesResults = await updateClientResources( + orgId, + config, + trx, + siteId + ); + }); + + logger.debug( + `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}` + ); + + // We need to update the targets on the newts from the successfully updated information + for (const result of proxyResourcesResults) { + for (const target of result.targetsToUpdate) { + const [site] = await db + .select() + .from(sites) + .innerJoin(newts, eq(sites.siteId, newts.siteId)) + .where( + and( + eq(sites.siteId, target.siteId), + eq(sites.orgId, orgId), + eq(sites.type, "newt"), + isNotNull(sites.pubKey) + ) + ) + .limit(1); + + if (site) { + logger.debug( + `Updating target ${target.targetId} on site ${site.sites.siteId}` + ); + + await addProxyTargets( + site.newt.newtId, + [target], + result.proxyResource.protocol, + result.proxyResource.proxyPort + ); + } + } + } + + logger.debug( + `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}` + ); + + // We need to update the targets on the newts from the successfully updated information + for (const result of clientResourcesResults) { + const [site] = await db + .select() + .from(sites) + .innerJoin(newts, eq(sites.siteId, newts.siteId)) + .where( + and( + eq(sites.siteId, result.resource.siteId), + eq(sites.orgId, orgId), + eq(sites.type, "newt"), + isNotNull(sites.pubKey) + ) + ) + .limit(1); + + if (site) { + logger.debug( + `Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}` + ); + + await addClientTargets( + site.newt.newtId, + result.resource.destinationIp, + result.resource.destinationPort, + result.resource.protocol, + result.resource.proxyPort + ); + } + } + } catch (error) { + logger.error(`Failed to update database from config: ${error}`); + throw error; + } +} + +// await updateDatabaseFromConfig("org_i21aifypnlyxur2", { +// resources: { +// "resource-nice-id": { +// name: "this is my resource", +// protocol: "http", +// "full-domain": "level1.test.example.com", +// "host-header": "example.com", +// "tls-server-name": "example.com", +// auth: { +// pincode: 123456, +// password: "sadfasdfadsf", +// "sso-enabled": true, +// "sso-roles": ["Member"], +// "sso-users": ["owen@fossorial.io"], +// "whitelist-users": ["owen@fossorial.io"] +// }, +// targets: [ +// { +// site: "glossy-plains-viscacha-rat", +// hostname: "localhost", +// method: "http", +// port: 8000, +// healthcheck: { +// port: 8000, +// hostname: "localhost" +// } +// }, +// { +// site: "glossy-plains-viscacha-rat", +// hostname: "localhost", +// method: "http", +// port: 8001 +// } +// ] +// }, +// "resource-nice-id2": { +// name: "http server", +// protocol: "tcp", +// "proxy-port": 3000, +// targets: [ +// { +// site: "glossy-plains-viscacha-rat", +// hostname: "localhost", +// port: 3000, +// } +// ] +// } +// } +// }); diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts new file mode 100644 index 00000000..f69e4854 --- /dev/null +++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts @@ -0,0 +1,53 @@ +import { sendToClient } from "@server/routers/ws"; +import { processContainerLabels } from "./parseDockerContainers"; +import { applyBlueprint } from "./applyBlueprint"; +import { db, sites } from "@server/db"; +import { eq } from "drizzle-orm"; +import logger from "@server/logger"; + +export async function applyNewtDockerBlueprint( + siteId: number, + newtId: string, + containers: any +) { + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + + if (!site) { + logger.warn("Site not found in applyNewtDockerBlueprint"); + return; + } + + // logger.debug(`Applying Docker blueprint to site: ${siteId}`); + // logger.debug(`Containers: ${JSON.stringify(containers, null, 2)}`); + + try { + const blueprint = processContainerLabels(containers); + + logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`); + + // Update the blueprint in the database + await applyBlueprint(site.orgId, blueprint, site.siteId); + } catch (error) { + logger.error(`Failed to update database from config: ${error}`); + await sendToClient(newtId, { + type: "newt/blueprint/results", + data: { + success: false, + message: `Failed to update database from config: ${error}` + } + }); + return; + } + + await sendToClient(newtId, { + type: "newt/blueprint/results", + data: { + success: true, + message: "Config updated successfully" + } + }); +} diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts new file mode 100644 index 00000000..59bbc346 --- /dev/null +++ b/server/lib/blueprints/clientResources.ts @@ -0,0 +1,117 @@ +import { + SiteResource, + siteResources, + Transaction, +} from "@server/db"; +import { sites } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import { + Config, +} from "./types"; +import logger from "@server/logger"; + +export type ClientResourcesResults = { + resource: SiteResource; +}[]; + +export async function updateClientResources( + orgId: string, + config: Config, + trx: Transaction, + siteId?: number +): Promise { + const results: ClientResourcesResults = []; + + for (const [resourceNiceId, resourceData] of Object.entries( + config["client-resources"] + )) { + const [existingResource] = await trx + .select() + .from(siteResources) + .where( + and( + eq(siteResources.orgId, orgId), + eq(siteResources.niceId, resourceNiceId) + ) + ) + .limit(1); + + const resourceSiteId = resourceData.site; + let site; + + if (resourceSiteId) { + // Look up site by niceId + [site] = await trx + .select({ siteId: sites.siteId }) + .from(sites) + .where( + and( + eq(sites.niceId, resourceSiteId), + eq(sites.orgId, orgId) + ) + ) + .limit(1); + } else if (siteId) { + // Use the provided siteId directly, but verify it belongs to the org + [site] = await trx + .select({ siteId: sites.siteId }) + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + } else { + throw new Error(`Target site is required`); + } + + if (!site) { + throw new Error( + `Site not found: ${resourceSiteId} in org ${orgId}` + ); + } + + if (existingResource) { + // Update existing resource + const [updatedResource] = await trx + .update(siteResources) + .set({ + name: resourceData.name || resourceNiceId, + siteId: site.siteId, + proxyPort: resourceData["proxy-port"]!, + destinationIp: resourceData.hostname, + destinationPort: resourceData["internal-port"], + protocol: resourceData.protocol + }) + .where( + eq( + siteResources.siteResourceId, + existingResource.siteResourceId + ) + ) + .returning(); + + results.push({ resource: updatedResource }); + } else { + // Create new resource + const [newResource] = await trx + .insert(siteResources) + .values({ + orgId: orgId, + siteId: site.siteId, + niceId: resourceNiceId, + name: resourceData.name || resourceNiceId, + proxyPort: resourceData["proxy-port"]!, + destinationIp: resourceData.hostname, + destinationPort: resourceData["internal-port"], + protocol: resourceData.protocol + }) + .returning(); + + logger.info( + `Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}` + ); + + results.push({ resource: newResource }); + } + } + + return results; +} diff --git a/server/lib/blueprints/parseDockerContainers.ts b/server/lib/blueprints/parseDockerContainers.ts new file mode 100644 index 00000000..1510e6e1 --- /dev/null +++ b/server/lib/blueprints/parseDockerContainers.ts @@ -0,0 +1,301 @@ +import logger from "@server/logger"; +import { setNestedProperty } from "./parseDotNotation"; + +export type DockerLabels = { + [key: string]: string; +}; + +export type ParsedObject = { + [key: string]: any; +}; + +type ContainerPort = { + privatePort: number; + publicPort: number; + type: string; + ip: string; +}; + +type Container = { + id: string; + name: string; + image: string; + state: string; + status: string; + ports: ContainerPort[] | null; + labels: DockerLabels; + created: number; + networks: { [key: string]: any }; + hostname: string; +}; + +type Target = { + hostname?: string; + port?: number; + method?: string; + enabled?: boolean; + [key: string]: any; +}; + +type ResourceConfig = { + [key: string]: any; + targets?: (Target | null)[]; +}; + +function getContainerPort(container: Container): number | null { + if (!container.ports || container.ports.length === 0) { + return null; + } + // Return the first port's privatePort + return container.ports[0].privatePort; + // return container.ports[0].publicPort; +} + +export function processContainerLabels(containers: Container[]): { + "proxy-resources": { [key: string]: ResourceConfig }; + "client-resources": { [key: string]: ResourceConfig }; +} { + const result = { + "proxy-resources": {} as { [key: string]: ResourceConfig }, + "client-resources": {} as { [key: string]: ResourceConfig } + }; + + // Process each container + containers.forEach((container) => { + if (container.state !== "running") { + return; + } + + const proxyResourceLabels: DockerLabels = {}; + const clientResourceLabels: DockerLabels = {}; + + // Filter and separate proxy-resources and client-resources labels + Object.entries(container.labels).forEach(([key, value]) => { + if (key.startsWith("pangolin.proxy-resources.")) { + // remove the pangolin.proxy- prefix to get "resources.xxx" + const strippedKey = key.replace("pangolin.proxy-", ""); + proxyResourceLabels[strippedKey] = value; + } else if (key.startsWith("pangolin.client-resources.")) { + // remove the pangolin.client- prefix to get "resources.xxx" + const strippedKey = key.replace("pangolin.client-", ""); + clientResourceLabels[strippedKey] = value; + } + }); + + // Process proxy resources + if (Object.keys(proxyResourceLabels).length > 0) { + processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]); + } + + // Process client resources + if (Object.keys(clientResourceLabels).length > 0) { + processResourceLabels(clientResourceLabels, container, result["client-resources"]); + } + }); + + return result; +} + +function processResourceLabels( + resourceLabels: DockerLabels, + container: Container, + targetResult: { [key: string]: ResourceConfig } +) { + // Parse the labels using the existing parseDockerLabels logic + const tempResult: ParsedObject = {}; + Object.entries(resourceLabels).forEach(([key, value]) => { + setNestedProperty(tempResult, key, value); + }); + + // Merge into target result + if (tempResult.resources) { + Object.entries(tempResult.resources).forEach( + ([resourceKey, resourceConfig]: [string, any]) => { + // Initialize resource if it doesn't exist + if (!targetResult[resourceKey]) { + targetResult[resourceKey] = {}; + } + + // Merge all properties except targets + Object.entries(resourceConfig).forEach( + ([propKey, propValue]) => { + if (propKey !== "targets") { + targetResult[resourceKey][propKey] = propValue; + } + } + ); + + // Handle targets specially + if ( + resourceConfig.targets && + Array.isArray(resourceConfig.targets) + ) { + const resource = targetResult[resourceKey]; + if (resource) { + if (!resource.targets) { + resource.targets = []; + } + + resourceConfig.targets.forEach( + (target: any, targetIndex: number) => { + // check if the target is an empty object + if ( + typeof target === "object" && + Object.keys(target).length === 0 + ) { + logger.debug( + `Skipping null target at index ${targetIndex} for resource ${resourceKey}` + ); + resource.targets!.push(null); + return; + } + + // Ensure targets array is long enough + while ( + resource.targets!.length <= targetIndex + ) { + resource.targets!.push({}); + } + + // Set default hostname and port if not provided + const finalTarget = { ...target }; + if (!finalTarget.hostname) { + finalTarget.hostname = + container.name || + container.hostname; + } + if (!finalTarget.port) { + const containerPort = + getContainerPort(container); + if (containerPort !== null) { + finalTarget.port = containerPort; + } + } + + // Merge with existing target data + resource.targets![targetIndex] = { + ...resource.targets![targetIndex], + ...finalTarget + }; + } + ); + } + } + } + ); + } +} + +// // Test example +// const testContainers: Container[] = [ +// { +// id: "57e056cb0e3a", +// name: "nginx1", +// image: "nginxdemos/hello", +// state: "running", +// status: "Up 4 days", +// ports: [ +// { +// privatePort: 80, +// publicPort: 8000, +// type: "tcp", +// ip: "0.0.0.0" +// } +// ], +// labels: { +// "resources.nginx.name": "nginx", +// "resources.nginx.full-domain": "nginx.example.com", +// "resources.nginx.protocol": "http", +// "resources.nginx.targets[0].enabled": "true" +// }, +// created: 1756942725, +// networks: { +// owen_default: { +// networkId: +// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c" +// } +// }, +// hostname: "57e056cb0e3a" +// }, +// { +// id: "58e056cb0e3b", +// name: "nginx2", +// image: "nginxdemos/hello", +// state: "running", +// status: "Up 4 days", +// ports: [ +// { +// privatePort: 80, +// publicPort: 8001, +// type: "tcp", +// ip: "0.0.0.0" +// } +// ], +// labels: { +// "resources.nginx.name": "nginx", +// "resources.nginx.full-domain": "nginx.example.com", +// "resources.nginx.protocol": "http", +// "resources.nginx.targets[1].enabled": "true" +// }, +// created: 1756942726, +// networks: { +// owen_default: { +// networkId: +// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c" +// } +// }, +// hostname: "58e056cb0e3b" +// }, +// { +// id: "59e056cb0e3c", +// name: "api-server", +// image: "my-api:latest", +// state: "running", +// status: "Up 2 days", +// ports: [ +// { +// privatePort: 3000, +// publicPort: 3000, +// type: "tcp", +// ip: "0.0.0.0" +// } +// ], +// labels: { +// "resources.api.name": "API Server", +// "resources.api.protocol": "http", +// "resources.api.targets[0].enabled": "true", +// "resources.api.targets[0].hostname": "custom-host", +// "resources.api.targets[0].port": "3001" +// }, +// created: 1756942727, +// networks: { +// owen_default: { +// networkId: +// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c" +// } +// }, +// hostname: "59e056cb0e3c" +// }, +// { +// id: "d0e29b08361c", +// name: "beautiful_wilson", +// image: "bolkedebruin/rdpgw:latest", +// state: "exited", +// status: "Exited (0) 4 hours ago", +// ports: null, +// labels: {}, +// created: 1757359039, +// networks: { +// bridge: { +// networkId: +// "ea7f56dfc9cc476b8a3560b5b570d0fe8a6a2bc5e8343ab1ed37822086e89687" +// } +// }, +// hostname: "d0e29b08361c" +// } +// ]; + +// // Test the function +// const result = processContainerLabels(testContainers); +// console.log("Processed result:"); +// console.log(JSON.stringify(result, null, 2)); diff --git a/server/lib/blueprints/parseDotNotation.ts b/server/lib/blueprints/parseDotNotation.ts new file mode 100644 index 00000000..87509d39 --- /dev/null +++ b/server/lib/blueprints/parseDotNotation.ts @@ -0,0 +1,109 @@ +export function setNestedProperty(obj: any, path: string, value: string): void { + const keys = path.split("."); + let current = obj; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + + // Handle array notation like "targets[0]" + const arrayMatch = key.match(/^(.+)\[(\d+)\]$/); + + if (arrayMatch) { + const [, arrayKey, indexStr] = arrayMatch; + const index = parseInt(indexStr, 10); + + // Initialize array if it doesn't exist + if (!current[arrayKey]) { + current[arrayKey] = []; + } + + // Ensure array is long enough + while (current[arrayKey].length <= index) { + current[arrayKey].push({}); + } + + current = current[arrayKey][index]; + } else { + // Regular object property + if (!current[key]) { + current[key] = {}; + } + current = current[key]; + } + } + + // Set the final value + const finalKey = keys[keys.length - 1]; + const arrayMatch = finalKey.match(/^(.+)\[(\d+)\]$/); + + if (arrayMatch) { + const [, arrayKey, indexStr] = arrayMatch; + const index = parseInt(indexStr, 10); + + if (!current[arrayKey]) { + current[arrayKey] = []; + } + + // Ensure array is long enough + while (current[arrayKey].length <= index) { + current[arrayKey].push(null); + } + + current[arrayKey][index] = convertValue(value); + } else { + current[finalKey] = convertValue(value); + } +} + +// Helper function to convert string values to appropriate types +export function convertValue(value: string): any { + // Convert boolean strings + if (value === "true") return true; + if (value === "false") return false; + + // Convert numeric strings + if (/^\d+$/.test(value)) { + const num = parseInt(value, 10); + return num; + } + + if (/^\d*\.\d+$/.test(value)) { + const num = parseFloat(value); + return num; + } + + // Return as string + return value; +} + +// // Example usage: +// const dockerLabels: DockerLabels = { +// "resources.resource-nice-id.name": "this is my resource", +// "resources.resource-nice-id.protocol": "http", +// "resources.resource-nice-id.full-domain": "level1.test3.example.com", +// "resources.resource-nice-id.host-header": "example.com", +// "resources.resource-nice-id.tls-server-name": "example.com", +// "resources.resource-nice-id.auth.pincode": "123456", +// "resources.resource-nice-id.auth.password": "sadfasdfadsf", +// "resources.resource-nice-id.auth.sso-enabled": "true", +// "resources.resource-nice-id.auth.sso-roles[0]": "Member", +// "resources.resource-nice-id.auth.sso-users[0]": "owen@fossorial.io", +// "resources.resource-nice-id.auth.whitelist-users[0]": "owen@fossorial.io", +// "resources.resource-nice-id.targets[0].hostname": "localhost", +// "resources.resource-nice-id.targets[0].method": "http", +// "resources.resource-nice-id.targets[0].port": "8000", +// "resources.resource-nice-id.targets[0].healthcheck.port": "8000", +// "resources.resource-nice-id.targets[0].healthcheck.hostname": "localhost", +// "resources.resource-nice-id.targets[1].hostname": "localhost", +// "resources.resource-nice-id.targets[1].method": "http", +// "resources.resource-nice-id.targets[1].port": "8001", +// "resources.resource-nice-id2.name": "this is other resource", +// "resources.resource-nice-id2.protocol": "tcp", +// "resources.resource-nice-id2.proxy-port": "3000", +// "resources.resource-nice-id2.targets[0].hostname": "localhost", +// "resources.resource-nice-id2.targets[0].port": "3000" +// }; + +// // Parse the labels +// const parsed = parseDockerLabels(dockerLabels); +// console.log(JSON.stringify(parsed, null, 2)); diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts new file mode 100644 index 00000000..6244fefa --- /dev/null +++ b/server/lib/blueprints/proxyResources.ts @@ -0,0 +1,885 @@ +import { + domains, + orgDomains, + Resource, + resourcePincode, + resourceRules, + resourceWhitelist, + roleResources, + roles, + Target, + Transaction, + userOrgs, + userResources, + users +} from "@server/db"; +import { resources, targets, sites } from "@server/db"; +import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; +import { + Config, + ConfigSchema, + isTargetsOnlyResource, + TargetData +} from "./types"; +import logger from "@server/logger"; +import { pickPort } from "@server/routers/target/helpers"; +import { resourcePassword } from "@server/db"; +import { hashPassword } from "@server/auth/password"; +import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; + +export type ProxyResourcesResults = { + proxyResource: Resource; + targetsToUpdate: Target[]; +}[]; + +export async function updateProxyResources( + orgId: string, + config: Config, + trx: Transaction, + siteId?: number +): Promise { + const results: ProxyResourcesResults = []; + + for (const [resourceNiceId, resourceData] of Object.entries( + config["proxy-resources"] + )) { + const targetsToUpdate: Target[] = []; + let resource: Resource; + + async function createTarget( // reusable function to create a target + resourceId: number, + targetData: TargetData + ) { + const targetSiteId = targetData.site; + let site; + + if (targetSiteId) { + // Look up site by niceId + [site] = await trx + .select({ siteId: sites.siteId }) + .from(sites) + .where( + and( + eq(sites.niceId, targetSiteId), + eq(sites.orgId, orgId) + ) + ) + .limit(1); + } else if (siteId) { + // Use the provided siteId directly, but verify it belongs to the org + [site] = await trx + .select({ siteId: sites.siteId }) + .from(sites) + .where( + and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) + ) + .limit(1); + } else { + throw new Error(`Target site is required`); + } + + if (!site) { + throw new Error( + `Site not found: ${targetSiteId} in org ${orgId}` + ); + } + + let internalPortToCreate; + if (!targetData["internal-port"]) { + const { internalPort, targetIps } = await pickPort( + site.siteId!, + trx + ); + internalPortToCreate = internalPort; + } else { + internalPortToCreate = targetData["internal-port"]; + } + + // Create target + const [newTarget] = await trx + .insert(targets) + .values({ + resourceId: resourceId, + siteId: site.siteId, + ip: targetData.hostname, + method: targetData.method, + port: targetData.port, + enabled: targetData.enabled, + internalPort: internalPortToCreate, + path: targetData.path, + pathMatchType: targetData["path-match"] + }) + .returning(); + + targetsToUpdate.push(newTarget); + } + + // Find existing resource by niceId and orgId + const [existingResource] = await trx + .select() + .from(resources) + .where( + and( + eq(resources.niceId, resourceNiceId), + eq(resources.orgId, orgId) + ) + ) + .limit(1); + + const http = resourceData.protocol == "http"; + const protocol = + resourceData.protocol == "http" ? "tcp" : resourceData.protocol; + const resourceEnabled = + resourceData.enabled == undefined || resourceData.enabled == null + ? true + : resourceData.enabled; + const resourceSsl = + resourceData.ssl == undefined || resourceData.ssl == null + ? true + : resourceData.ssl; + let headers = ""; + for (const header of resourceData.headers || []) { + headers += `${header.name}: ${header.value},`; + } + // if there are headers, remove the trailing comma + if (headers.endsWith(",")) { + headers = headers.slice(0, -1); + } + + if (existingResource) { + let domain; + if (http) { + domain = await getDomain( + existingResource.resourceId, + resourceData["full-domain"]!, + orgId, + trx + ); + } + + // check if the only key in the resource is targets, if so, skip the update + if (isTargetsOnlyResource(resourceData)) { + logger.debug( + `Skipping update for resource ${existingResource.resourceId} as only targets are provided` + ); + resource = existingResource; + } else { + // Update existing resource + [resource] = await trx + .update(resources) + .set({ + name: resourceData.name || "Unnamed Resource", + protocol: protocol || "http", + http: http, + proxyPort: http ? null : resourceData["proxy-port"], + fullDomain: http ? resourceData["full-domain"] : null, + subdomain: domain ? domain.subdomain : null, + domainId: domain ? domain.domainId : null, + enabled: resourceEnabled, + sso: resourceData.auth?.["sso-enabled"] || false, + ssl: resourceSsl, + setHostHeader: resourceData["host-header"] || null, + tlsServerName: resourceData["tls-server-name"] || null, + emailWhitelistEnabled: resourceData.auth?.[ + "whitelist-users" + ] + ? resourceData.auth["whitelist-users"].length > 0 + : false, + headers: headers || null, + applyRules: + resourceData.rules && resourceData.rules.length > 0 + }) + .where( + eq(resources.resourceId, existingResource.resourceId) + ) + .returning(); + + await trx + .delete(resourcePassword) + .where( + eq( + resourcePassword.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.password) { + const passwordHash = await hashPassword( + resourceData.auth.password + ); + + await trx.insert(resourcePassword).values({ + resourceId: existingResource.resourceId, + passwordHash + }); + } + + await trx + .delete(resourcePincode) + .where( + eq( + resourcePincode.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.pincode) { + const pincodeHash = await hashPassword( + resourceData.auth.pincode.toString() + ); + + await trx.insert(resourcePincode).values({ + resourceId: existingResource.resourceId, + pincodeHash, + digitLength: 6 + }); + } + + if (resourceData.auth?.["sso-roles"]) { + const ssoRoles = resourceData.auth?.["sso-roles"]; + await syncRoleResources( + existingResource.resourceId, + ssoRoles, + orgId, + trx + ); + } + + if (resourceData.auth?.["sso-users"]) { + const ssoUsers = resourceData.auth?.["sso-users"]; + await syncUserResources( + existingResource.resourceId, + ssoUsers, + orgId, + trx + ); + } + + if (resourceData.auth?.["whitelist-users"]) { + const whitelistUsers = + resourceData.auth?.["whitelist-users"]; + await syncWhitelistUsers( + existingResource.resourceId, + whitelistUsers, + orgId, + trx + ); + } + } + + const existingResourceTargets = await trx + .select() + .from(targets) + .where(eq(targets.resourceId, existingResource.resourceId)) + .orderBy(asc(targets.targetId)); + + // Create new targets + for (const [index, targetData] of resourceData.targets.entries()) { + if ( + !targetData || + (typeof targetData === "object" && + Object.keys(targetData).length === 0) + ) { + // If targetData is null or an empty object, we can skip it + continue; + } + const existingTarget = existingResourceTargets[index]; + + if (existingTarget) { + const targetSiteId = targetData.site; + let site; + + if (targetSiteId) { + // Look up site by niceId + [site] = await trx + .select({ siteId: sites.siteId }) + .from(sites) + .where( + and( + eq(sites.niceId, targetSiteId), + eq(sites.orgId, orgId) + ) + ) + .limit(1); + } else if (siteId) { + // Use the provided siteId directly, but verify it belongs to the org + [site] = await trx + .select({ siteId: sites.siteId }) + .from(sites) + .where( + and( + eq(sites.siteId, siteId), + eq(sites.orgId, orgId) + ) + ) + .limit(1); + } else { + throw new Error(`Target site is required`); + } + + if (!site) { + throw new Error( + `Site not found: ${targetSiteId} in org ${orgId}` + ); + } + + // update this target + const [updatedTarget] = await trx + .update(targets) + .set({ + siteId: site.siteId, + ip: targetData.hostname, + method: http ? targetData.method : null, + port: targetData.port, + enabled: targetData.enabled, + path: targetData.path, + pathMatchType: targetData["path-match"] + }) + .where(eq(targets.targetId, existingTarget.targetId)) + .returning(); + + if (checkIfTargetChanged(existingTarget, updatedTarget)) { + let internalPortToUpdate; + if (!targetData["internal-port"]) { + const { internalPort, targetIps } = await pickPort( + site.siteId!, + trx + ); + internalPortToUpdate = internalPort; + } else { + internalPortToUpdate = targetData["internal-port"]; + } + + const [finalUpdatedTarget] = await trx // this double is so we can check the whole target before and after + .update(targets) + .set({ + internalPort: internalPortToUpdate + }) + .where( + eq(targets.targetId, existingTarget.targetId) + ) + .returning(); + + targetsToUpdate.push(finalUpdatedTarget); + } + } else { + await createTarget(existingResource.resourceId, targetData); + } + } + + if (existingResourceTargets.length > resourceData.targets.length) { + const targetsToDelete = existingResourceTargets.slice( + resourceData.targets.length + ); + logger.debug( + `Targets to delete: ${JSON.stringify(targetsToDelete)}` + ); + for (const target of targetsToDelete) { + if (!target) { + continue; + } + if (siteId && target.siteId !== siteId) { + logger.debug( + `Skipping target ${target.targetId} for deletion. Site ID does not match filter.` + ); + continue; // only delete targets for the specified siteId + } + logger.debug(`Deleting target ${target.targetId}`); + await trx + .delete(targets) + .where(eq(targets.targetId, target.targetId)); + } + } + + const existingRules = await trx + .select() + .from(resourceRules) + .where( + eq(resourceRules.resourceId, existingResource.resourceId) + ) + .orderBy(resourceRules.priority); + + // Sync rules + for (const [index, rule] of resourceData.rules?.entries() || []) { + const existingRule = existingRules[index]; + if (existingRule) { + if ( + existingRule.action !== getRuleAction(rule.action) || + existingRule.match !== rule.match.toUpperCase() || + existingRule.value !== rule.value + ) { + validateRule(rule); + await trx + .update(resourceRules) + .set({ + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), + value: rule.value + }) + .where( + eq(resourceRules.ruleId, existingRule.ruleId) + ); + } + } else { + validateRule(rule); + await trx.insert(resourceRules).values({ + resourceId: existingResource.resourceId, + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), + value: rule.value, + priority: index + 1 // start priorities at 1 + }); + } + } + + if (existingRules.length > (resourceData.rules?.length || 0)) { + const rulesToDelete = existingRules.slice( + resourceData.rules?.length || 0 + ); + for (const rule of rulesToDelete) { + await trx + .delete(resourceRules) + .where(eq(resourceRules.ruleId, rule.ruleId)); + } + } + + logger.debug(`Updated resource ${existingResource.resourceId}`); + } else { + // create a brand new resource + let domain; + if (http) { + domain = await getDomain( + undefined, + resourceData["full-domain"]!, + orgId, + trx + ); + } + + // Create new resource + const [newResource] = await trx + .insert(resources) + .values({ + orgId, + niceId: resourceNiceId, + name: resourceData.name || "Unnamed Resource", + protocol: resourceData.protocol || "http", + http: http, + proxyPort: http ? null : resourceData["proxy-port"], + fullDomain: http ? resourceData["full-domain"] : null, + subdomain: domain ? domain.subdomain : null, + domainId: domain ? domain.domainId : null, + enabled: resourceEnabled, + sso: resourceData.auth?.["sso-enabled"] || false, + setHostHeader: resourceData["host-header"] || null, + tlsServerName: resourceData["tls-server-name"] || null, + ssl: resourceSsl, + headers: headers || null, + applyRules: + resourceData.rules && resourceData.rules.length > 0 + }) + .returning(); + + if (resourceData.auth?.password) { + const passwordHash = await hashPassword( + resourceData.auth.password + ); + + await trx.insert(resourcePassword).values({ + resourceId: newResource.resourceId, + passwordHash + }); + } + + if (resourceData.auth?.pincode) { + const pincodeHash = await hashPassword( + resourceData.auth.pincode.toString() + ); + + await trx.insert(resourcePincode).values({ + resourceId: newResource.resourceId, + pincodeHash, + digitLength: 6 + }); + } + + resource = newResource; + + const [adminRole] = await trx + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (!adminRole) { + throw new Error(`Admin role not found`); + } + + await trx.insert(roleResources).values({ + roleId: adminRole.roleId, + resourceId: newResource.resourceId + }); + + if (resourceData.auth?.["sso-roles"]) { + const ssoRoles = resourceData.auth?.["sso-roles"]; + await syncRoleResources( + newResource.resourceId, + ssoRoles, + orgId, + trx + ); + } + + if (resourceData.auth?.["sso-users"]) { + const ssoUsers = resourceData.auth?.["sso-users"]; + await syncUserResources( + newResource.resourceId, + ssoUsers, + orgId, + trx + ); + } + + if (resourceData.auth?.["whitelist-users"]) { + const whitelistUsers = resourceData.auth?.["whitelist-users"]; + await syncWhitelistUsers( + newResource.resourceId, + whitelistUsers, + orgId, + trx + ); + } + + // Create new targets + for (const targetData of resourceData.targets) { + if (!targetData) { + // If targetData is null or an empty object, we can skip it + continue; + } + await createTarget(newResource.resourceId, targetData); + } + + for (const [index, rule] of resourceData.rules?.entries() || []) { + validateRule(rule); + await trx.insert(resourceRules).values({ + resourceId: newResource.resourceId, + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), + value: rule.value, + priority: index + 1 // start priorities at 1 + }); + } + + logger.debug(`Created resource ${newResource.resourceId}`); + } + + results.push({ + proxyResource: resource, + targetsToUpdate + }); + } + + return results; +} + +function getRuleAction(input: string) { + let action = "DROP"; + if (input == "allow") { + action = "ACCEPT"; + } else if (input == "deny") { + action = "DROP"; + } else if (input == "pass") { + action = "PASS"; + } + return action; +} + +function validateRule(rule: any) { + if (rule.match === "cidr") { + if (!isValidCIDR(rule.value)) { + throw new Error(`Invalid CIDR provided: ${rule.value}`); + } + } else if (rule.match === "ip") { + if (!isValidIP(rule.value)) { + throw new Error(`Invalid IP provided: ${rule.value}`); + } + } else if (rule.match === "path") { + if (!isValidUrlGlobPattern(rule.value)) { + throw new Error(`Invalid URL glob pattern: ${rule.value}`); + } + } +} + +async function syncRoleResources( + resourceId: number, + ssoRoles: string[], + orgId: string, + trx: Transaction +) { + const existingRoleResources = await trx + .select() + .from(roleResources) + .where(eq(roleResources.resourceId, resourceId)); + + for (const roleName of ssoRoles) { + if (roleName === "Admin") { + continue; // never add admin access + } + + const [role] = await trx + .select() + .from(roles) + .where(and(eq(roles.name, roleName), eq(roles.orgId, orgId))) + .limit(1); + + if (!role) { + throw new Error(`Role not found: ${roleName} in org ${orgId}`); + } + + const existingRoleResource = existingRoleResources.find( + (rr) => rr.roleId === role.roleId + ); + + if (!existingRoleResource) { + await trx.insert(roleResources).values({ + roleId: role.roleId, + resourceId: resourceId + }); + } + } + + for (const existingRoleResource of existingRoleResources) { + const [role] = await trx + .select() + .from(roles) + .where(eq(roles.roleId, existingRoleResource.roleId)) + .limit(1); + + if (role.isAdmin) { + continue; // never remove admin access + } + + if (role && !ssoRoles.includes(role.name)) { + await trx + .delete(roleResources) + .where( + and( + eq(roleResources.roleId, existingRoleResource.roleId), + eq(roleResources.resourceId, resourceId) + ) + ); + } + } +} + +async function syncUserResources( + resourceId: number, + ssoUsers: string[], + orgId: string, + trx: Transaction +) { + const existingUserResources = await trx + .select() + .from(userResources) + .where(eq(userResources.resourceId, resourceId)); + + for (const email of ssoUsers) { + const [user] = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where(and(eq(users.email, email), eq(userOrgs.orgId, orgId))) + .limit(1); + + if (!user) { + throw new Error(`User not found: ${email} in org ${orgId}`); + } + + const existingUserResource = existingUserResources.find( + (rr) => rr.userId === user.user.userId + ); + + if (!existingUserResource) { + await trx.insert(userResources).values({ + userId: user.user.userId, + resourceId: resourceId + }); + } + } + + for (const existingUserResource of existingUserResources) { + const [user] = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where( + and( + eq(users.userId, existingUserResource.userId), + eq(userOrgs.orgId, orgId) + ) + ) + .limit(1); + + if (user && user.user.email && !ssoUsers.includes(user.user.email)) { + await trx + .delete(userResources) + .where( + and( + eq(userResources.userId, existingUserResource.userId), + eq(userResources.resourceId, resourceId) + ) + ); + } + } +} + +async function syncWhitelistUsers( + resourceId: number, + whitelistUsers: string[], + orgId: string, + trx: Transaction +) { + const existingWhitelist = await trx + .select() + .from(resourceWhitelist) + .where(eq(resourceWhitelist.resourceId, resourceId)); + + for (const email of whitelistUsers) { + const [user] = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where(and(eq(users.email, email), eq(userOrgs.orgId, orgId))) + .limit(1); + + if (!user) { + throw new Error(`User not found: ${email} in org ${orgId}`); + } + + const existingWhitelistEntry = existingWhitelist.find( + (w) => w.email === email + ); + + if (!existingWhitelistEntry) { + await trx.insert(resourceWhitelist).values({ + email, + resourceId: resourceId + }); + } + } + + for (const existingWhitelistEntry of existingWhitelist) { + if (!whitelistUsers.includes(existingWhitelistEntry.email)) { + await trx + .delete(resourceWhitelist) + .where( + and( + eq(resourceWhitelist.resourceId, resourceId), + eq( + resourceWhitelist.email, + existingWhitelistEntry.email + ) + ) + ); + } + } +} + +function checkIfTargetChanged( + existing: Target | undefined, + incoming: Target | undefined +): boolean { + if (!existing && incoming) return true; + if (existing && !incoming) return true; + if (!existing || !incoming) return false; + + if (existing.ip !== incoming.ip) return true; + if (existing.port !== incoming.port) return true; + if (existing.siteId !== incoming.siteId) return true; + + return false; +} + +async function getDomain( + resourceId: number | undefined, + fullDomain: string, + orgId: string, + trx: Transaction +) { + const [fullDomainExists] = await trx + .select({ resourceId: resources.resourceId }) + .from(resources) + .where( + and( + eq(resources.fullDomain, fullDomain), + eq(resources.orgId, orgId), + resourceId + ? ne(resources.resourceId, resourceId) + : isNotNull(resources.resourceId) + ) + ) + .limit(1); + + if (fullDomainExists) { + throw new Error( + `Resource already exists: ${fullDomain} in org ${orgId}` + ); + } + + const domain = await getDomainId(orgId, fullDomain, trx); + + if (!domain) { + throw new Error( + `Domain not found for full-domain: ${fullDomain} in org ${orgId}` + ); + } + + return domain; +} + +async function getDomainId( + orgId: string, + fullDomain: string, + trx: Transaction +): Promise<{ subdomain: string | null; domainId: string } | null> { + const possibleDomains = await trx + .select() + .from(domains) + .innerJoin(orgDomains, eq(domains.domainId, orgDomains.domainId)) + .where(and(eq(orgDomains.orgId, orgId), eq(domains.verified, true))) + .execute(); + + if (possibleDomains.length === 0) { + return null; + } + + const validDomains = possibleDomains.filter((domain) => { + if (domain.domains.type == "ns" || domain.domains.type == "wildcard") { + return ( + fullDomain === domain.domains.baseDomain || + fullDomain.endsWith(`.${domain.domains.baseDomain}`) + ); + } else if (domain.domains.type == "cname") { + return fullDomain === domain.domains.baseDomain; + } + }); + + if (validDomains.length === 0) { + return null; + } + + const domainSelection = validDomains[0].domains; + const baseDomain = domainSelection.baseDomain; + + // remove the base domain of the domain + let subdomain = null; + if (domainSelection.type == "ns") { + if (fullDomain != baseDomain) { + subdomain = fullDomain.replace(`.${baseDomain}`, ""); + } + } + + // Return the first valid domain + return { + subdomain: subdomain, + domainId: domainSelection.domainId + }; +} diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts new file mode 100644 index 00000000..9b3a7a20 --- /dev/null +++ b/server/lib/blueprints/types.ts @@ -0,0 +1,366 @@ +import { z } from "zod"; + +export const SiteSchema = z.object({ + name: z.string().min(1).max(100), + "docker-socket-enabled": z.boolean().optional().default(true) +}); + +// Schema for individual target within a resource +export const TargetSchema = z.object({ + site: z.string().optional(), + method: z.enum(["http", "https", "h2c"]).optional(), + hostname: z.string(), + port: z.number().int().min(1).max(65535), + enabled: z.boolean().optional().default(true), + "internal-port": z.number().int().min(1).max(65535).optional(), + path: z.string().optional(), + "path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable() +}); +export type TargetData = z.infer; + +export const AuthSchema = z.object({ + // pincode has to have 6 digits + pincode: z.number().min(100000).max(999999).optional(), + password: z.string().min(1).optional(), + "sso-enabled": z.boolean().optional().default(false), + "sso-roles": z + .array(z.string()) + .optional() + .default([]) + .refine((roles) => !roles.includes("Admin"), { + message: "Admin role cannot be included in sso-roles" + }), + "sso-users": z.array(z.string().email()).optional().default([]), + "whitelist-users": z.array(z.string().email()).optional().default([]), +}); + +export const RuleSchema = z.object({ + action: z.enum(["allow", "deny", "pass"]), + match: z.enum(["cidr", "path", "ip", "country"]), + value: z.string() +}); + +export const HeaderSchema = z.object({ + name: z.string().min(1), + value: z.string().min(1) +}); + +// Schema for individual resource +export const ResourceSchema = z + .object({ + name: z.string().optional(), + protocol: z.enum(["http", "tcp", "udp"]).optional(), + ssl: z.boolean().optional(), + "full-domain": z.string().optional(), + "proxy-port": z.number().int().min(1).max(65535).optional(), + enabled: z.boolean().optional(), + targets: z.array(TargetSchema.nullable()).optional().default([]), + auth: AuthSchema.optional(), + "host-header": z.string().optional(), + "tls-server-name": z.string().optional(), + headers: z.array(HeaderSchema).optional(), + rules: z.array(RuleSchema).optional() + }) + .refine( + (resource) => { + if (isTargetsOnlyResource(resource)) { + return true; + } + + // Otherwise, require name and protocol for full resource definition + return ( + resource.name !== undefined && resource.protocol !== undefined + ); + }, + { + message: + "Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum", + path: ["name", "protocol"] + } + ) + .refine( + (resource) => { + if (isTargetsOnlyResource(resource)) { + return true; + } + + // If protocol is http, all targets must have method field + if (resource.protocol === "http") { + return resource.targets.every( + (target) => target == null || target.method !== undefined + ); + } + // If protocol is tcp or udp, no target should have method field + if (resource.protocol === "tcp" || resource.protocol === "udp") { + return resource.targets.every( + (target) => target == null || target.method === undefined + ); + } + return true; + }, + (resource) => { + if (resource.protocol === "http") { + return { + message: + "When protocol is 'http', all targets must have a 'method' field", + path: ["targets"] + }; + } + return { + message: + "When protocol is 'tcp' or 'udp', targets must not have a 'method' field", + path: ["targets"] + }; + } + ) + .refine( + (resource) => { + if (isTargetsOnlyResource(resource)) { + return true; + } + + // If protocol is http, it must have a full-domain + if (resource.protocol === "http") { + return ( + resource["full-domain"] !== undefined && + resource["full-domain"].length > 0 + ); + } + return true; + }, + { + message: + "When protocol is 'http', a 'full-domain' must be provided", + path: ["full-domain"] + } + ) + .refine( + (resource) => { + if (isTargetsOnlyResource(resource)) { + return true; + } + + // If protocol is tcp or udp, it must have both proxy-port + if (resource.protocol === "tcp" || resource.protocol === "udp") { + return resource["proxy-port"] !== undefined; + } + return true; + }, + { + message: + "When protocol is 'tcp' or 'udp', 'proxy-port' must be provided", + path: ["proxy-port", "exit-node"] + } + ) + .refine( + (resource) => { + // Skip validation for targets-only resources + if (isTargetsOnlyResource(resource)) { + return true; + } + + // If protocol is tcp or udp, it must not have auth + if (resource.protocol === "tcp" || resource.protocol === "udp") { + return resource.auth === undefined; + } + return true; + }, + { + message: + "When protocol is 'tcp' or 'udp', 'auth' must not be provided", + path: ["auth"] + } + ); + +export function isTargetsOnlyResource(resource: any): boolean { + return Object.keys(resource).length === 1 && resource.targets; +} + +export const ClientResourceSchema = z.object({ + name: z.string().min(2).max(100), + site: z.string().min(2).max(100).optional(), + protocol: z.enum(["tcp", "udp"]), + "proxy-port": z.number().min(1).max(65535), + "hostname": z.string().min(1).max(255), + "internal-port": z.number().min(1).max(65535), + enabled: z.boolean().optional().default(true) +}); + +// Schema for the entire configuration object +export const ConfigSchema = z + .object({ + "proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}), + "client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}), + sites: z.record(z.string(), SiteSchema).optional().default({}) + }) + .refine( + // Enforce the full-domain uniqueness across resources in the same stack + (config) => { + // Extract all full-domain values with their resource keys + const fullDomainMap = new Map(); + + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const fullDomain = resource["full-domain"]; + if (fullDomain) { + // Only process if full-domain is defined + if (!fullDomainMap.has(fullDomain)) { + fullDomainMap.set(fullDomain, []); + } + fullDomainMap.get(fullDomain)!.push(resourceKey); + } + } + ); + + // Find duplicates + const duplicates = Array.from(fullDomainMap.entries()).filter( + ([_, resourceKeys]) => resourceKeys.length > 1 + ); + + return duplicates.length === 0; + }, + (config) => { + // Extract duplicates for error message + const fullDomainMap = new Map(); + + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const fullDomain = resource["full-domain"]; + if (fullDomain) { + // Only process if full-domain is defined + if (!fullDomainMap.has(fullDomain)) { + fullDomainMap.set(fullDomain, []); + } + fullDomainMap.get(fullDomain)!.push(resourceKey); + } + } + ); + + const duplicates = Array.from(fullDomainMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map( + ([fullDomain, resourceKeys]) => + `'${fullDomain}' used by resources: ${resourceKeys.join(", ")}` + ) + .join("; "); + + return { + message: `Duplicate 'full-domain' values found: ${duplicates}`, + path: ["resources"] + }; + } + ) + .refine( + // Enforce proxy-port uniqueness within proxy-resources + (config) => { + const proxyPortMap = new Map(); + + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const proxyPort = resource["proxy-port"]; + if (proxyPort !== undefined) { + if (!proxyPortMap.has(proxyPort)) { + proxyPortMap.set(proxyPort, []); + } + proxyPortMap.get(proxyPort)!.push(resourceKey); + } + } + ); + + // Find duplicates + const duplicates = Array.from(proxyPortMap.entries()).filter( + ([_, resourceKeys]) => resourceKeys.length > 1 + ); + + return duplicates.length === 0; + }, + (config) => { + // Extract duplicates for error message + const proxyPortMap = new Map(); + + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const proxyPort = resource["proxy-port"]; + if (proxyPort !== undefined) { + if (!proxyPortMap.has(proxyPort)) { + proxyPortMap.set(proxyPort, []); + } + proxyPortMap.get(proxyPort)!.push(resourceKey); + } + } + ); + + const duplicates = Array.from(proxyPortMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map( + ([proxyPort, resourceKeys]) => + `port ${proxyPort} used by proxy-resources: ${resourceKeys.join(", ")}` + ) + .join("; "); + + return { + message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`, + path: ["proxy-resources"] + }; + } + ) + .refine( + // Enforce proxy-port uniqueness within client-resources + (config) => { + const proxyPortMap = new Map(); + + Object.entries(config["client-resources"]).forEach( + ([resourceKey, resource]) => { + const proxyPort = resource["proxy-port"]; + if (proxyPort !== undefined) { + if (!proxyPortMap.has(proxyPort)) { + proxyPortMap.set(proxyPort, []); + } + proxyPortMap.get(proxyPort)!.push(resourceKey); + } + } + ); + + // Find duplicates + const duplicates = Array.from(proxyPortMap.entries()).filter( + ([_, resourceKeys]) => resourceKeys.length > 1 + ); + + return duplicates.length === 0; + }, + (config) => { + // Extract duplicates for error message + const proxyPortMap = new Map(); + + Object.entries(config["client-resources"]).forEach( + ([resourceKey, resource]) => { + const proxyPort = resource["proxy-port"]; + if (proxyPort !== undefined) { + if (!proxyPortMap.has(proxyPort)) { + proxyPortMap.set(proxyPort, []); + } + proxyPortMap.get(proxyPort)!.push(resourceKey); + } + } + ); + + const duplicates = Array.from(proxyPortMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map( + ([proxyPort, resourceKeys]) => + `port ${proxyPort} used by client-resources: ${resourceKeys.join(", ")}` + ) + .join("; "); + + return { + message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`, + path: ["client-resources"] + }; + } + ); + +// Type inference from the schema +export type Site = z.infer; +export type Target = z.infer; +export type Resource = z.infer; +export type Config = z.infer; diff --git a/server/lib/consts.ts b/server/lib/consts.ts index b9afa792..506c1c8d 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.9.0"; +export const APP_VERSION = "1.10.1"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/domainUtils.ts b/server/lib/domainUtils.ts new file mode 100644 index 00000000..d043ca51 --- /dev/null +++ b/server/lib/domainUtils.ts @@ -0,0 +1,112 @@ +import { db } from "@server/db"; +import { domains, orgDomains } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import { subdomainSchema } from "@server/lib/schemas"; +import { fromError } from "zod-validation-error"; + +export type DomainValidationResult = { + success: true; + fullDomain: string; + subdomain: string | null; +} | { + success: false; + error: string; +}; + +/** + * Validates a domain and constructs the full domain based on domain type and subdomain. + * + * @param domainId - The ID of the domain to validate + * @param orgId - The organization ID to check domain access + * @param subdomain - Optional subdomain to append (for ns and wildcard domains) + * @returns DomainValidationResult with success status and either fullDomain/subdomain or error message + */ +export async function validateAndConstructDomain( + domainId: string, + orgId: string, + subdomain?: string | null +): Promise { + try { + // Query domain with organization access check + const [domainRes] = await db + .select() + .from(domains) + .where(eq(domains.domainId, domainId)) + .leftJoin( + orgDomains, + and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) + ); + + // Check if domain exists + if (!domainRes || !domainRes.domains) { + return { + success: false, + error: `Domain with ID ${domainId} not found` + }; + } + + // Check if organization has access to domain + if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) { + return { + success: false, + error: `Organization does not have access to domain with ID ${domainId}` + }; + } + + // Check if domain is verified + if (!domainRes.domains.verified) { + return { + success: false, + error: `Domain with ID ${domainId} is not verified` + }; + } + + // Construct full domain based on domain type + let fullDomain = ""; + let finalSubdomain = subdomain; + + if (domainRes.domains.type === "ns") { + if (subdomain) { + fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`; + } else { + fullDomain = domainRes.domains.baseDomain; + } + } else if (domainRes.domains.type === "cname") { + fullDomain = domainRes.domains.baseDomain; + finalSubdomain = null; // CNAME domains don't use subdomains + } else if (domainRes.domains.type === "wildcard") { + if (subdomain !== undefined && subdomain !== null) { + // Validate subdomain format for wildcard domains + const parsedSubdomain = subdomainSchema.safeParse(subdomain); + if (!parsedSubdomain.success) { + return { + success: false, + error: fromError(parsedSubdomain.error).toString() + }; + } + fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`; + } else { + fullDomain = domainRes.domains.baseDomain; + } + } + + // If the full domain equals the base domain, set subdomain to null + if (fullDomain === domainRes.domains.baseDomain) { + finalSubdomain = null; + } + + // Convert to lowercase + fullDomain = fullDomain.toLowerCase(); + + return { + success: true, + fullDomain, + subdomain: finalSubdomain ?? null + }; + } catch (error) { + return { + success: false, + error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } +} diff --git a/server/lib/exitNodeComms.ts b/server/lib/exitNodeComms.ts index f79b718f..bcfbec3e 100644 --- a/server/lib/exitNodeComms.ts +++ b/server/lib/exitNodeComms.ts @@ -3,7 +3,7 @@ import logger from "@server/logger"; import { ExitNode } from "@server/db"; interface ExitNodeRequest { - remoteType: string; + remoteType?: string; localPath: string; method?: "POST" | "DELETE" | "GET" | "PUT"; data?: any; diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts index 06539bb0..7b571682 100644 --- a/server/lib/exitNodes/exitNodes.ts +++ b/server/lib/exitNodes/exitNodes.ts @@ -30,7 +30,8 @@ export async function listExitNodes(orgId: string, filterOnline = false) { maxConnections: exitNodes.maxConnections, online: exitNodes.online, lastPing: exitNodes.lastPing, - type: exitNodes.type + type: exitNodes.type, + region: exitNodes.region }) .from(exitNodes); diff --git a/server/lib/traefikConfig.ts b/server/lib/traefikConfig.ts index e16b93d2..8b133419 100644 --- a/server/lib/traefikConfig.ts +++ b/server/lib/traefikConfig.ts @@ -15,6 +15,7 @@ import { getValidCertificatesForDomains, getValidCertificatesForDomainsHybrid } from "./remoteCertificates"; +import { sendToExitNode } from "./exitNodeComms"; export class TraefikConfigManager { private intervalId: NodeJS.Timeout | null = null; @@ -403,27 +404,11 @@ export class TraefikConfigManager { [exitNode] = await db.select().from(exitNodes).limit(1); } if (exitNode) { - try { - await axios.post( - `${exitNode.reachableAt}/update-local-snis`, - { fullDomains: Array.from(domains) }, - { headers: { "Content-Type": "application/json" } } - ); - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error updating local SNI:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error updating local SNI:", error); - } - } + await sendToExitNode(exitNode, { + localPath: "/update-local-snis", + method: "POST", + data: { fullDomains: Array.from(domains) } + }); } else { logger.error( "No exit node found. Has gerbil registered yet?" diff --git a/server/lib/validators.ts b/server/lib/validators.ts index 6c581e47..522e5018 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -129,6 +129,40 @@ export function isValidDomain(domain: string): boolean { return true; } +export function validateHeaders(headers: string): boolean { + // Validate comma-separated headers in format "Header-Name: value" + const headerPairs = headers.split(",").map((pair) => pair.trim()); + return headerPairs.every((pair) => { + // Check if the pair contains exactly one colon + const colonCount = (pair.match(/:/g) || []).length; + if (colonCount !== 1) { + return false; + } + + const colonIndex = pair.indexOf(":"); + if (colonIndex === 0 || colonIndex === pair.length - 1) { + return false; + } + + const headerName = pair.substring(0, colonIndex).trim(); + const headerValue = pair.substring(colonIndex + 1).trim(); + + // Header name should not be empty and should contain valid characters + // Header names are case-insensitive and can contain alphanumeric, hyphens + const headerNameRegex = /^[a-zA-Z0-9\-_]+$/; + if (!headerName || !headerNameRegex.test(headerName)) { + return false; + } + + // Header value should not be empty and should not contain colons + if (!headerValue || headerValue.includes(":")) { + return false; + } + + return true; + }); +} + const validTlds = [ "AAA", "AARP", diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts index 9c96e6ec..51a8f3fc 100644 --- a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts +++ b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts @@ -19,6 +19,11 @@ export async function verifyApiKeySetResourceUsers( ); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + if (!req.apiKeyOrg) { return next( createHttpError( @@ -32,11 +37,6 @@ export async function verifyApiKeySetResourceUsers( return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs")); } - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - if (userIds.length === 0) { return next(); } diff --git a/server/routers/external.ts b/server/routers/external.ts index ce44bd8a..c48a41a7 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -343,6 +343,12 @@ authenticated.get( verifyUserHasAction(ActionsEnum.getResource), resource.getResource ); +authenticated.get( + "/org/:orgId/resource/:niceId", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getResource), + resource.getResource +); authenticated.post( "/resource/:resourceId", verifyResourceAccess, @@ -582,6 +588,14 @@ authenticated.put( user.createOrgUser ); +authenticated.post( + "/org/:orgId/user/:userId", + verifyOrgAccess, + verifyUserAccess, + verifyUserHasAction(ActionsEnum.updateOrgUser), + user.updateOrgUser +); + authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); authenticated.post( @@ -932,7 +946,7 @@ authRouter.post( windowMs: 15 * 60 * 1000, max: 15, keyGenerator: (req) => - `requestEmailVerificationCode:${req.body.email || ipKeyGenerator(req.ip || "")}`, + `requestEmailVerificationCode:${req.user?.email || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts index 2a0e5809..150b9f88 100644 --- a/server/routers/idp/listIdps.ts +++ b/server/routers/idp/listIdps.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, idpOidcConfig } from "@server/db"; import { domains, idp, orgDomains, users, idpOrg } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { sql } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; @@ -33,23 +33,21 @@ async function query(limit: number, offset: number) { idpId: idp.idpId, name: idp.name, type: idp.type, - orgCount: sql`count(${idpOrg.orgId})` + variant: idpOidcConfig.variant, + orgCount: sql`count(${idpOrg.orgId})`, + autoProvision: idp.autoProvision }) .from(idp) .leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`) - .groupBy(idp.idpId) + .leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId)) + .groupBy(idp.idpId, idpOidcConfig.variant) .limit(limit) .offset(offset); return res; } export type ListIdpsResponse = { - idps: Array<{ - idpId: number; - name: string; - type: string; - orgCount: number; - }>; + idps: Awaited>; pagination: { total: number; limit: number; diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 79453732..6a43aaa7 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -24,7 +24,8 @@ import { verifyApiKeyIsRoot, verifyApiKeyClientAccess, verifyClientsEnabled, - verifyApiKeySiteResourceAccess + verifyApiKeySiteResourceAccess, + verifyOrgAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -469,6 +470,21 @@ authenticated.get( user.listUsers ); +authenticated.put( + "/org/:orgId/user", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createOrgUser), + user.createOrgUser +); + +authenticated.post( + "/org/:orgId/user/:userId", + verifyApiKeyOrgAccess, + verifyApiKeyUserAccess, + verifyApiKeyHasAction(ActionsEnum.updateOrgUser), + user.updateOrgUser +); + authenticated.delete( "/org/:orgId/user/:userId", verifyApiKeyOrgAccess, @@ -628,3 +644,10 @@ authenticated.post( verifyApiKeyHasAction(ActionsEnum.updateClient), client.updateClient ); + +authenticated.put( + "/org/:orgId/blueprint", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.applyBlueprint), + org.applyBlueprint +); \ No newline at end of file diff --git a/server/routers/newt/handleApplyBlueprintMessage.ts b/server/routers/newt/handleApplyBlueprintMessage.ts new file mode 100644 index 00000000..68158799 --- /dev/null +++ b/server/routers/newt/handleApplyBlueprintMessage.ts @@ -0,0 +1,73 @@ +import { db, newts } from "@server/db"; +import { MessageHandler } from "../ws"; +import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db"; +import { eq, and, sql, inArray } from "drizzle-orm"; +import logger from "@server/logger"; +import { applyBlueprint } from "@server/lib/blueprints/applyBlueprint"; + +export const handleApplyBlueprintMessage: MessageHandler = async (context) => { + const { message, client, sendToClient } = context; + const newt = client as Newt; + + logger.debug("Handling apply blueprint message!"); + + if (!newt) { + logger.warn("Newt not found"); + return; + } + + if (!newt.siteId) { + logger.warn("Newt has no site!"); // TODO: Maybe we create the site here? + return; + } + + // get the site + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, newt.siteId)); + + if (!site) { + logger.warn("Site not found for newt"); + return; + } + + const { blueprint } = message.data; + if (!blueprint) { + logger.warn("No blueprint provided"); + return; + } + + logger.debug(`Received blueprint: ${blueprint}`); + + try { + const blueprintParsed = JSON.parse(blueprint); + // Update the blueprint in the database + await applyBlueprint(site.orgId, blueprintParsed, site.siteId); + } catch (error) { + logger.error(`Failed to update database from config: ${error}`); + return { + message: { + type: "newt/blueprint/results", + data: { + success: false, + message: `Failed to update database from config: ${error}` + } + }, + broadcast: false, // Send to all clients + excludeSender: false // Include sender in broadcast + }; + } + + return { + message: { + type: "newt/blueprint/results", + data: { + success: true, + message: "Config updated successfully" + } + }, + broadcast: false, // Send to all clients + excludeSender: false // Include sender in broadcast + }; +}; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 3c7ecaff..eef78765 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -10,6 +10,7 @@ import { getNextAvailableClientSubnet } from "@server/lib/ip"; import { selectBestExitNode, verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; +import { fetchContainers } from "./dockerSocket"; export type ExitNodePingResult = { exitNodeId: number; @@ -76,6 +77,15 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { return; } + logger.debug(`Docker socket enabled: ${oldSite.dockerSocketEnabled}`); + + if (oldSite.dockerSocketEnabled) { + logger.debug( + "Site has docker socket enabled - requesting docker containers" + ); + fetchContainers(newt.newtId); + } + let siteSubnet = oldSite.subnet; let exitNodeIdToQuery = oldSite.exitNodeId; if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) { diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts index 01b7be60..aceca37d 100644 --- a/server/routers/newt/handleSocketMessages.ts +++ b/server/routers/newt/handleSocketMessages.ts @@ -2,6 +2,7 @@ import { MessageHandler } from "../ws"; import logger from "@server/logger"; import { dockerSocketCache } from "./dockerSocket"; import { Newt } from "@server/db"; +import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint"; export const handleDockerStatusMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; @@ -57,4 +58,15 @@ export const handleDockerContainersMessage: MessageHandler = async ( } else { logger.warn(`Newt ${newt.newtId} does not have Docker containers`); } + + if (!newt.siteId) { + logger.warn("Newt has no site!"); + return; + } + + await applyNewtDockerBlueprint( + newt.siteId, + newt.newtId, + containers + ); }; diff --git a/server/routers/newt/index.ts b/server/routers/newt/index.ts index 08f047e3..9642a637 100644 --- a/server/routers/newt/index.ts +++ b/server/routers/newt/index.ts @@ -4,4 +4,5 @@ export * from "./handleNewtRegisterMessage"; export * from "./handleReceiveBandwidthMessage"; export * from "./handleGetConfigMessage"; export * from "./handleSocketMessages"; -export * from "./handleNewtPingRequestMessage"; \ No newline at end of file +export * from "./handleNewtPingRequestMessage"; +export * from "./handleApplyBlueprintMessage"; \ No newline at end of file diff --git a/server/routers/org/applyBlueprint.ts b/server/routers/org/applyBlueprint.ts new file mode 100644 index 00000000..982258ee --- /dev/null +++ b/server/routers/org/applyBlueprint.ts @@ -0,0 +1,127 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { eq } from "drizzle-orm"; +import { + apiKeyOrg, + apiKeys, + domains, + Org, + orgDomains, + orgs, + roleActions, + roles, + userOrgs, + users, + actions +} from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import config from "@server/lib/config"; +import { fromError } from "zod-validation-error"; +import { defaultRoleAllowedActions } from "../role"; +import { OpenAPITags, registry } from "@server/openApi"; +import { isValidCIDR } from "@server/lib/validators"; +import { applyBlueprint as applyBlueprintFunc } from "@server/lib/blueprints/applyBlueprint"; + +const applyBlueprintSchema = z + .object({ + blueprint: z.string() + }) + .strict(); + +const applyBlueprintParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/blueprint", + description: "Apply a base64 encoded blueprint to an organization", + tags: [OpenAPITags.Org], + request: { + params: applyBlueprintParamsSchema, + body: { + content: { + "application/json": { + schema: applyBlueprintSchema + } + } + } + }, + responses: {} +}); + +export async function applyBlueprint( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = applyBlueprintParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const parsedBody = applyBlueprintSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { blueprint } = parsedBody.data; + + if (!blueprint) { + logger.warn("No blueprint provided"); + return; + } + + logger.debug(`Received blueprint: ${blueprint}`); + + try { + // first base64 decode the blueprint + const decoded = Buffer.from(blueprint, "base64").toString("utf-8"); + // then parse the json + const blueprintParsed = JSON.parse(decoded); + + // Update the blueprint in the database + await applyBlueprintFunc(orgId, blueprintParsed); + } catch (error) { + logger.error(`Failed to update database from config: ${error}`); + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Failed to update database from config: ${error}` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Blueprint applied successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index c9a44d8d..754def66 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -7,3 +7,4 @@ export * from "./checkId"; export * from "./getOrgOverview"; export * from "./listOrgs"; export * from "./pickOrgDefaults"; +export * from "./applyBlueprint"; \ No newline at end of file diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 5b27cb41..806a5a58 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -21,6 +21,8 @@ import { subdomainSchema } from "@server/lib/schemas"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; import { build } from "@server/build"; +import { getUniqueResourceName } from "@server/db/names"; +import { validateAndConstructDomain } from "@server/lib/domainUtils"; const createResourceParamsSchema = z .object({ @@ -193,76 +195,21 @@ async function createHttpResource( } const { name, domainId } = parsedBody.data; - let subdomain = parsedBody.data.subdomain; + const subdomain = parsedBody.data.subdomain; - const [domainRes] = await db - .select() - .from(domains) - .where(eq(domains.domainId, domainId)) - .leftJoin( - orgDomains, - and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) - ); + // Validate domain and construct full domain + const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain); - if (!domainRes || !domainRes.domains) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Domain with ID ${domainId} not found` - ) - ); - } - - if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - `Organization does not have access to domain with ID ${domainId}` - ) - ); - } - - if (!domainRes.domains.verified) { + if (!domainResult.success) { return next( createHttpError( HttpCode.BAD_REQUEST, - `Domain with ID ${domainRes.domains.domainId} is not verified` + domainResult.error ) ); } - let fullDomain = ""; - if (domainRes.domains.type == "ns") { - if (subdomain) { - fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`; - } else { - fullDomain = domainRes.domains.baseDomain; - } - } else if (domainRes.domains.type == "cname") { - fullDomain = domainRes.domains.baseDomain; - } else if (domainRes.domains.type == "wildcard") { - if (subdomain) { - // the subdomain cant have a dot in it - const parsedSubdomain = subdomainSchema.safeParse(subdomain); - if (!parsedSubdomain.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedSubdomain.error).toString() - ) - ); - } - fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`; - } else { - fullDomain = domainRes.domains.baseDomain; - } - } - - if (fullDomain === domainRes.domains.baseDomain) { - subdomain = null; - } - - fullDomain = fullDomain.toLowerCase(); + const { fullDomain, subdomain: finalSubdomain } = domainResult; logger.debug(`Full domain: ${fullDomain}`); @@ -283,15 +230,18 @@ async function createHttpResource( let resource: Resource | undefined; + const niceId = await getUniqueResourceName(orgId); + await db.transaction(async (trx) => { const newResource = await trx .insert(resources) .values({ + niceId, fullDomain, domainId, orgId, name, - subdomain, + subdomain: finalSubdomain, http: true, protocol: "tcp", ssl: true @@ -391,10 +341,13 @@ async function createRawResource( let resource: Resource | undefined; + const niceId = await getUniqueResourceName(orgId); + await db.transaction(async (trx) => { const newResource = await trx .insert(resources) .values({ + niceId, orgId, name, http, diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index a2c1c0d1..d2aebedd 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -2,32 +2,72 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { Resource, resources, sites } from "@server/db"; -import { eq } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; +import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; const getResourceSchema = z .object({ resourceId: z .string() - .transform(Number) - .pipe(z.number().int().positive()) + .optional() + .transform(stoi) + .pipe(z.number().int().positive().optional()) + .optional(), + niceId: z.string().optional(), + orgId: z.string().optional() }) .strict(); -export type GetResourceResponse = Resource; +async function query(resourceId?: number, niceId?: string, orgId?: string) { + if (resourceId) { + const [res] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + return res; + } else if (niceId && orgId) { + const [res] = await db + .select() + .from(resources) + .where(and(eq(resources.niceId, niceId), eq(resources.orgId, orgId))) + .limit(1); + return res; + } +} + +export type GetResourceResponse = NonNullable>>; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/resource/{niceId}", + description: + "Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.", + tags: [OpenAPITags.Org, OpenAPITags.Resource], + request: { + params: z.object({ + orgId: z.string(), + niceId: z.string() + }) + }, + responses: {} +}); registry.registerPath({ method: "get", path: "/resource/{resourceId}", - description: "Get a resource.", + description: "Get a resource by resourceId.", tags: [OpenAPITags.Resource], request: { - params: getResourceSchema + params: z.object({ + resourceId: z.number() + }) }, responses: {} }); @@ -48,29 +88,18 @@ export async function getResource( ); } - const { resourceId } = parsedParams.data; + const { resourceId, niceId, orgId } = parsedParams.data; - const [resp] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - const resource = resp; + const resource = await query(resourceId, niceId, orgId); if (!resource) { return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) + createHttpError(HttpCode.NOT_FOUND, "Resource not found") ); } - return response(res, { - data: { - ...resource - }, + return response(res, { + data: resource, success: true, error: false, message: "Resource retrieved successfully", diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 191221f1..c775564b 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -32,6 +32,7 @@ export type GetResourceAuthInfoResponse = { url: string; whitelist: boolean; skipToIdpId: number | null; + orgId: string; }; export async function getResourceAuthInfo( @@ -88,7 +89,8 @@ export async function getResourceAuthInfo( blockAccess: resource.blockAccess, url, whitelist: resource.emailWhitelistEnabled, - skipToIdpId: resource.skipToIdpId + skipToIdpId: resource.skipToIdpId, + orgId: resource.orgId }, success: true, error: false, diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 54878bfc..45d225ed 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -16,6 +16,7 @@ import logger from "@server/logger"; import stoi from "@server/lib/stoi"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { warn } from "console"; const listResourcesParamsSchema = z .object({ @@ -54,7 +55,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { protocol: resources.protocol, proxyPort: resources.proxyPort, enabled: resources.enabled, - domainId: resources.domainId + domainId: resources.domainId, + niceId: resources.niceId }) .from(resources) .leftJoin( diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 30acc0c1..7c0f9c63 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -20,6 +20,8 @@ import { tlsNameSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas"; import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; +import { validateAndConstructDomain } from "@server/lib/domainUtils"; +import { validateHeaders } from "@server/lib/validators"; const updateResourceParamsSchema = z .object({ @@ -44,7 +46,8 @@ const updateHttpResourceBodySchema = z stickySession: z.boolean().optional(), tlsServerName: z.string().nullable().optional(), setHostHeader: z.string().nullable().optional(), - skipToIdpId: z.number().int().positive().nullable().optional() + skipToIdpId: z.number().int().positive().nullable().optional(), + headers: z.string().nullable().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -82,6 +85,18 @@ const updateHttpResourceBodySchema = z message: "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." } + ) + .refine( + (data) => { + if (data.headers) { + return validateHeaders(data.headers); + } + return true; + }, + { + message: + "Invalid headers format. Use comma-separated format: 'Header-Name: value, Another-Header: another-value'. Header values cannot contain colons." + } ); export type UpdateResourceResponse = Resource; @@ -230,78 +245,19 @@ async function updateHttpResource( if (updateData.domainId) { const domainId = updateData.domainId; - const [domainRes] = await db - .select() - .from(domains) - .where(eq(domains.domainId, domainId)) - .leftJoin( - orgDomains, - and( - eq(orgDomains.orgId, resource.orgId), - eq(orgDomains.domainId, domainId) - ) - ); - - if (!domainRes || !domainRes.domains) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Domain with ID ${updateData.domainId} not found` - ) - ); - } - - if ( - domainRes.orgDomains && - domainRes.orgDomains.orgId !== resource.orgId - ) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - `You do not have permission to use domain with ID ${updateData.domainId}` - ) - ); - } - - if (!domainRes.domains.verified) { + // Validate domain and construct full domain + const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain); + + if (!domainResult.success) { return next( createHttpError( HttpCode.BAD_REQUEST, - `Domain with ID ${updateData.domainId} is not verified` + domainResult.error ) ); } - let fullDomain = ""; - if (domainRes.domains.type == "ns") { - if (updateData.subdomain) { - fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`; - } else { - fullDomain = domainRes.domains.baseDomain; - } - } else if (domainRes.domains.type == "cname") { - fullDomain = domainRes.domains.baseDomain; - } else if (domainRes.domains.type == "wildcard") { - if (updateData.subdomain !== undefined) { - // the subdomain cant have a dot in it - const parsedSubdomain = subdomainSchema.safeParse( - updateData.subdomain - ); - if (!parsedSubdomain.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedSubdomain.error).toString() - ) - ); - } - fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`; - } else { - fullDomain = domainRes.domains.baseDomain; - } - } - - fullDomain = fullDomain.toLowerCase(); + const { fullDomain, subdomain: finalSubdomain } = domainResult; logger.debug(`Full domain: ${fullDomain}`); @@ -332,9 +288,8 @@ async function updateHttpResource( .where(eq(resources.resourceId, resource.resourceId)); } - if (fullDomain === domainRes.domains.baseDomain) { - updateData.subdomain = null; - } + // Update the subdomain in the update data + updateData.subdomain = finalSubdomain; } const updatedResource = await db diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 2e705c56..58d44744 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -139,7 +139,7 @@ export async function pickSiteDefaults( }, success: true, error: false, - message: "Organization retrieved successfully", + message: "Site defaults chosen successfully", status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index da41c19c..ca223b04 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -10,6 +10,7 @@ import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import { addTargets } from "../client/targets"; +import { getUniqueSiteResourceName } from "@server/db/names"; const createSiteResourceParamsSchema = z .object({ @@ -121,11 +122,14 @@ export async function createSiteResource( ); } + const niceId = await getUniqueSiteResourceName(orgId); + // Create the site resource const [newSiteResource] = await db .insert(siteResources) .values({ siteId, + niceId, orgId, name, protocol, diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts index 914706cd..09c01eb0 100644 --- a/server/routers/siteResource/getSiteResource.ts +++ b/server/routers/siteResource/getSiteResource.ts @@ -12,21 +12,72 @@ import { OpenAPITags, registry } from "@server/openApi"; const getSiteResourceParamsSchema = z .object({ - siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()), + siteResourceId: z + .string() + .optional() + .transform((val) => val ? Number(val) : undefined) + .pipe(z.number().int().positive().optional()) + .optional(), siteId: z.string().transform(Number).pipe(z.number().int().positive()), + niceId: z.string().optional(), orgId: z.string() }) .strict(); -export type GetSiteResourceResponse = SiteResource; +async function query(siteResourceId?: number, siteId?: number, niceId?: string, orgId?: string) { + if (siteResourceId && siteId && orgId) { + const [siteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + return siteResource; + } else if (niceId && siteId && orgId) { + const [siteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.niceId, niceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + return siteResource; + } +} + +export type GetSiteResourceResponse = NonNullable>>; registry.registerPath({ method: "get", path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", - description: "Get a specific site resource.", + description: "Get a specific site resource by siteResourceId.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { - params: getSiteResourceParamsSchema + params: z.object({ + siteResourceId: z.number(), + siteId: z.number(), + orgId: z.string() + }) + }, + responses: {} +}); + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site/{siteId}/resource/nice/{niceId}", + description: "Get a specific site resource by niceId.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: z.object({ + niceId: z.string(), + siteId: z.number(), + orgId: z.string() + }) }, responses: {} }); @@ -47,18 +98,10 @@ export async function getSiteResource( ); } - const { siteResourceId, siteId, orgId } = parsedParams.data; + const { siteResourceId, siteId, niceId, orgId } = parsedParams.data; // Get the site resource - const [siteResource] = await db - .select() - .from(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) - .limit(1); + const siteResource = await query(siteResourceId, siteId, niceId, orgId); if (!siteResource) { return next( diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 82e2fe68..f6f71124 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -28,7 +28,7 @@ const updateSiteResourceSchema = z protocol: z.enum(["tcp", "udp"]).optional(), proxyPort: z.number().int().positive().optional(), destinationPort: z.number().int().positive().optional(), - destinationIp: z.string().ip().optional(), + destinationIp: z.string().optional(), enabled: z.boolean().optional() }) .strict(); diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 7a3acd55..fb85f566 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -30,7 +30,9 @@ const createTargetSchema = z ip: z.string().refine(isTargetValid), method: z.string().optional().nullable(), port: z.number().int().min(1).max(65535), - enabled: z.boolean().default(true) + enabled: z.boolean().default(true), + path: z.string().optional().nullable(), + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() }) .strict(); @@ -161,7 +163,7 @@ export async function createTarget( ); } - const { internalPort, targetIps } = await pickPort(site.siteId!); + const { internalPort, targetIps } = await pickPort(site.siteId!, db); if (!internalPort) { return next( diff --git a/server/routers/target/helpers.ts b/server/routers/target/helpers.ts index 4935d28a..13b2ee46 100644 --- a/server/routers/target/helpers.ts +++ b/server/routers/target/helpers.ts @@ -1,10 +1,10 @@ -import { db } from "@server/db"; +import { db, Transaction } from "@server/db"; import { resources, targets } from "@server/db"; import { eq } from "drizzle-orm"; const currentBannedPorts: number[] = []; -export async function pickPort(siteId: number): Promise<{ +export async function pickPort(siteId: number, trx: Transaction | typeof db): Promise<{ internalPort: number; targetIps: string[]; }> { @@ -12,7 +12,7 @@ export async function pickPort(siteId: number): Promise<{ const targetIps: string[] = []; const targetInternalPorts: number[] = []; - const targetsRes = await db + const targetsRes = await trx .select() .from(targets) .where(eq(targets.siteId, siteId)); diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index eab8f1c8..ca1159d2 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -44,7 +44,9 @@ function queryTargets(resourceId: number) { enabled: targets.enabled, resourceId: targets.resourceId, siteId: targets.siteId, - siteType: sites.type + siteType: sites.type, + path: targets.path, + pathMatchType: targets.pathMatchType }) .from(targets) .leftJoin(sites, eq(sites.siteId, targets.siteId)) diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 67d9a8df..928a1a55 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -26,7 +26,9 @@ const updateTargetBodySchema = z ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), port: z.number().int().min(1).max(65535).optional(), - enabled: z.boolean().optional() + enabled: z.boolean().optional(), + path: z.string().optional().nullable(), + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -153,7 +155,7 @@ export async function updateTarget( ); } - const { internalPort, targetIps } = await pickPort(site.siteId!); + const { internalPort, targetIps } = await pickPort(site.siteId!, db); if (!internalPort) { return next( diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 1a55f2bd..a1a2a7a3 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -54,7 +54,8 @@ export async function traefikConfigProvider( config.getRawConfig().traefik.site_types ); - if (traefikConfig?.http?.middlewares) { // BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING + if (traefikConfig?.http?.middlewares) { + // BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING traefikConfig.http.middlewares[badgerMiddlewareName] = { plugin: { [badgerMiddlewareName]: { @@ -104,106 +105,112 @@ export async function getTraefikConfig( }; }; - // Get all resources with related data - const allResources = await db.transaction(async (tx) => { - // Get resources with their targets and sites in a single optimized query - // Start from sites on this exit node, then join to targets and resources - const resourcesWithTargetsAndSites = await tx - .select({ - // Resource fields - resourceId: resources.resourceId, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol, - subdomain: resources.subdomain, - domainId: resources.domainId, - enabled: resources.enabled, - stickySession: resources.stickySession, - tlsServerName: resources.tlsServerName, - setHostHeader: resources.setHostHeader, - enableProxy: resources.enableProxy, - // Target fields - targetId: targets.targetId, - targetEnabled: targets.enabled, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - // Site fields - siteId: sites.siteId, - siteType: sites.type, - siteOnline: sites.online, - subnet: sites.subnet, - exitNodeId: sites.exitNodeId - }) - .from(sites) - .innerJoin(targets, eq(targets.siteId, sites.siteId)) - .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) - .where( - and( - eq(targets.enabled, true), - eq(resources.enabled, true), - or( - eq(sites.exitNodeId, exitNodeId), - isNull(sites.exitNodeId) - ), - inArray(sites.type, siteTypes), - config.getRawConfig().traefik.allow_raw_resources - ? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true - : eq(resources.http, true), - ) - ); + // Get resources with their targets and sites in a single optimized query + // Start from sites on this exit node, then join to targets and resources + const resourcesWithTargetsAndSites = await db + .select({ + // Resource fields + resourceId: resources.resourceId, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + http: resources.http, + proxyPort: resources.proxyPort, + protocol: resources.protocol, + subdomain: resources.subdomain, + domainId: resources.domainId, + enabled: resources.enabled, + stickySession: resources.stickySession, + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader, + enableProxy: resources.enableProxy, + headers: resources.headers, + // Target fields + targetId: targets.targetId, + targetEnabled: targets.enabled, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + path: targets.path, + pathMatchType: targets.pathMatchType, - // Group by resource and include targets with their unique site data - const resourcesMap = new Map(); + // Site fields + siteId: sites.siteId, + siteType: sites.type, + siteOnline: sites.online, + subnet: sites.subnet, + exitNodeId: sites.exitNodeId + }) + .from(sites) + .innerJoin(targets, eq(targets.siteId, sites.siteId)) + .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) + .where( + and( + eq(targets.enabled, true), + eq(resources.enabled, true), + or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)), + inArray(sites.type, siteTypes), + config.getRawConfig().traefik.allow_raw_resources + ? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true + : eq(resources.http, true) + ) + ); - resourcesWithTargetsAndSites.forEach((row) => { - const resourceId = row.resourceId; + // Group by resource and include targets with their unique site data + const resourcesMap = new Map(); - if (!resourcesMap.has(resourceId)) { - resourcesMap.set(resourceId, { - resourceId: row.resourceId, - fullDomain: row.fullDomain, - ssl: row.ssl, - http: row.http, - proxyPort: row.proxyPort, - protocol: row.protocol, - subdomain: row.subdomain, - domainId: row.domainId, - enabled: row.enabled, - stickySession: row.stickySession, - tlsServerName: row.tlsServerName, - setHostHeader: row.setHostHeader, - enableProxy: row.enableProxy, - targets: [] - }); - } + resourcesWithTargetsAndSites.forEach((row) => { + const resourceId = row.resourceId; + const targetPath = sanitizePath(row.path) || ""; // Handle null/undefined paths + const pathMatchType = row.pathMatchType || ""; - // Add target with its associated site data - resourcesMap.get(resourceId).targets.push({ + // Create a unique key combining resourceId and path+pathMatchType + const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-"); + const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); + + if (!resourcesMap.has(mapKey)) { + resourcesMap.set(mapKey, { resourceId: row.resourceId, - targetId: row.targetId, - ip: row.ip, - method: row.method, - port: row.port, - internalPort: row.internalPort, - enabled: row.targetEnabled, - site: { - siteId: row.siteId, - type: row.siteType, - subnet: row.subnet, - exitNodeId: row.exitNodeId, - online: row.siteOnline - } + fullDomain: row.fullDomain, + ssl: row.ssl, + http: row.http, + proxyPort: row.proxyPort, + protocol: row.protocol, + subdomain: row.subdomain, + domainId: row.domainId, + enabled: row.enabled, + stickySession: row.stickySession, + tlsServerName: row.tlsServerName, + setHostHeader: row.setHostHeader, + enableProxy: row.enableProxy, + targets: [], + headers: row.headers, + path: row.path, // the targets will all have the same path + pathMatchType: row.pathMatchType // the targets will all have the same pathMatchType }); - }); + } - return Array.from(resourcesMap.values()); + // Add target with its associated site data + resourcesMap.get(mapKey).targets.push({ + resourceId: row.resourceId, + targetId: row.targetId, + ip: row.ip, + method: row.method, + port: row.port, + internalPort: row.internalPort, + enabled: row.targetEnabled, + site: { + siteId: row.siteId, + type: row.siteType, + subnet: row.subnet, + exitNodeId: row.exitNodeId, + online: row.siteOnline + } + }); }); - if (!allResources.length) { + // make sure we have at least one resource + if (resourcesMap.size === 0) { return {}; } @@ -219,14 +226,15 @@ export async function getTraefikConfig( } }; - for (const resource of allResources) { + // get the key and the resource + for (const [key, resource] of resourcesMap.entries()) { const targets = resource.targets; - const routerName = `${resource.resourceId}-router`; - const serviceName = `${resource.resourceId}-service`; + const routerName = `${key}-router`; + const serviceName = `${key}-service`; const fullDomain = `${resource.fullDomain}`; - const transportName = `${resource.resourceId}-transport`; - const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`; + const transportName = `${key}-transport`; + const headersMiddlewareName = `${key}-headers-middleware`; if (!resource.enabled) { continue; @@ -238,9 +246,6 @@ export async function getTraefikConfig( } if (!resource.fullDomain) { - logger.error( - `Resource ${resource.resourceId} has no fullDomain` - ); continue; } @@ -296,16 +301,68 @@ export async function getTraefikConfig( const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; + const routerMiddlewares = [ + badgerMiddlewareName, + ...additionalMiddlewares + ]; + + if (resource.headers && resource.headers.length > 0) { + // if there are headers, parse them into an object + const headersObj: { [key: string]: string } = {}; + const headersArr = resource.headers.split(","); + for (const header of headersArr) { + const [key, value] = header + .split(":") + .map((s: string) => s.trim()); + if (key && value) { + headersObj[key] = value; + } + } + + if (resource.setHostHeader) { + headersObj["Host"] = resource.setHostHeader; + } + + // check if the object is not empty + if (Object.keys(headersObj).length > 0) { + // Add the headers middleware + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + config_output.http.middlewares[headersMiddlewareName] = { + headers: { + customRequestHeaders: headersObj + } + }; + + routerMiddlewares.push(headersMiddlewareName); + } + } + + let rule = `Host(\`${fullDomain}\`)`; + let priority = 100; + if (resource.path && resource.pathMatchType) { + priority += 1; + // add path to rule based on match type + if (resource.pathMatchType === "exact") { + rule += ` && Path(\`${resource.path}\`)`; + } else if (resource.pathMatchType === "prefix") { + rule += ` && PathPrefix(\`${resource.path}\`)`; + } else if (resource.pathMatchType === "regex") { + rule += ` && PathRegexp(\`${resource.path}\`)`; + } + } + config_output.http.routers![routerName] = { entryPoints: [ resource.ssl ? config.getRawConfig().traefik.https_entrypoint : config.getRawConfig().traefik.http_entrypoint ], - middlewares: [badgerMiddlewareName, ...additionalMiddlewares], + middlewares: routerMiddlewares, service: serviceName, - rule: `Host(\`${fullDomain}\`)`, - priority: 100, + rule: rule, + priority: priority, ...(resource.ssl ? { tls } : {}) }; @@ -316,8 +373,8 @@ export async function getTraefikConfig( ], middlewares: [redirectHttpsMiddlewareName], service: serviceName, - rule: `Host(\`${fullDomain}\`)`, - priority: 100 + rule: rule, + priority: priority }; } @@ -334,55 +391,64 @@ export async function getTraefikConfig( targets as TargetWithSite[] ).some((target: TargetWithSite) => target.site.online); - return (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } - - // If any sites are online, exclude offline sites - if (anySitesOnline && !target.site.online) { - return false; - } - - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - if ( - !target.ip || - !target.port || - !target.method - ) { + return ( + (targets as TargetWithSite[]) + .filter((target: TargetWithSite) => { + if (!target.enabled) { return false; } - } else if (target.site.type === "newt") { - if ( - !target.internalPort || - !target.method || - !target.site.subnet - ) { + + // If any sites are online, exclude offline sites + if (anySitesOnline && !target.site.online) { return false; } - } - return true; - }) - .map((target: TargetWithSite) => { - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - return { - url: `${target.method}://${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { - const ip = - target.site.subnet!.split("/")[0]; - return { - url: `${target.method}://${ip}:${target.internalPort}` - }; - } - }); + + if ( + target.site.type === "local" || + target.site.type === "wireguard" + ) { + if ( + !target.ip || + !target.port || + !target.method + ) { + return false; + } + } else if (target.site.type === "newt") { + if ( + !target.internalPort || + !target.method || + !target.site.subnet + ) { + return false; + } + } + return true; + }) + .map((target: TargetWithSite) => { + if ( + target.site.type === "local" || + target.site.type === "wireguard" + ) { + return { + url: `${target.method}://${target.ip}:${target.port}` + }; + } else if (target.site.type === "newt") { + const ip = + target.site.subnet!.split("/")[0]; + return { + url: `${target.method}://${ip}:${target.internalPort}` + }; + } + }) + // filter out duplicates + .filter( + (v, i, a) => + a.findIndex( + (t) => t && v && t.url === v.url + ) === i + ) + ); })(), ...(resource.stickySession ? { @@ -413,27 +479,6 @@ export async function getTraefikConfig( serviceName ].loadBalancer.serversTransport = transportName; } - - // Add the host header middleware - if (resource.setHostHeader) { - if (!config_output.http.middlewares) { - config_output.http.middlewares = {}; - } - config_output.http.middlewares[hostHeaderMiddlewareName] = { - headers: { - customRequestHeaders: { - Host: resource.setHostHeader - } - } - }; - if (!config_output.http.routers![routerName].middlewares) { - config_output.http.routers![routerName].middlewares = []; - } - config_output.http.routers![routerName].middlewares = [ - ...config_output.http.routers![routerName].middlewares, - hostHeaderMiddlewareName - ]; - } } else { // Non-HTTP (TCP/UDP) configuration if (!resource.enableProxy) { @@ -529,3 +574,13 @@ export async function getTraefikConfig( } return config_output; } + +function sanitizePath(path: string | null | undefined): string | undefined { + if (!path) return undefined; + // clean any non alphanumeric characters from the path and replace with dashes + // the path cant be too long either, so limit to 50 characters + if (path.length > 50) { + path = path.substring(0, 50); + } + return path.replace(/[^a-zA-Z0-9]/g, ""); +} diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index 4419772a..5b11c923 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -84,7 +84,14 @@ export async function createOrgUser( } const { orgId } = parsedParams.data; - const { username, email, name, type, idpId, roleId } = parsedBody.data; + const { + username, + email, + name, + type, + idpId, + roleId + } = parsedBody.data; const [role] = await db .select() @@ -141,7 +148,12 @@ export async function createOrgUser( const [existingUser] = await trx .select() .from(users) - .where(eq(users.username, username)); + .where( + and( + eq(users.username, username), + eq(users.idpId, idpId) + ) + ); if (existingUser) { const [existingOrgUser] = await trx @@ -168,7 +180,8 @@ export async function createOrgUser( .values({ orgId, userId: existingUser.userId, - roleId: role.roleId + roleId: role.roleId, + autoProvisioned: false }) .returning(); } else { @@ -184,7 +197,7 @@ export async function createOrgUser( type: "oidc", idpId, dateCreated: new Date().toISOString(), - emailVerified: true + emailVerified: true, }) .returning(); @@ -193,7 +206,8 @@ export async function createOrgUser( .values({ orgId, userId: newUser.userId, - roleId: role.roleId + roleId: role.roleId, + autoProvisioned: false }) .returning(); } @@ -204,7 +218,6 @@ export async function createOrgUser( .from(userOrgs) .where(eq(userOrgs.orgId, orgId)); }); - } else { return next( createHttpError(HttpCode.BAD_REQUEST, "User type is required") diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 38cd70a6..bdec2d12 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, idp, idpOidcConfig } from "@server/db"; import { roles, userOrgs, users } from "@server/db"; import { and, eq, sql } from "drizzle-orm"; import response from "@server/lib/response"; @@ -25,10 +25,18 @@ async function queryUser(orgId: string, userId: string) { isOwner: userOrgs.isOwner, isAdmin: roles.isAdmin, twoFactorEnabled: users.twoFactorEnabled, + autoProvisioned: userOrgs.autoProvisioned, + idpId: users.idpId, + idpName: idp.name, + idpType: idp.type, + idpVariant: idpOidcConfig.variant, + idpAutoProvision: idp.autoProvision }) .from(userOrgs) .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) .leftJoin(users, eq(userOrgs.userId, users.userId)) + .leftJoin(idp, eq(users.idpId, idp.idpId)) + .leftJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId)) .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) .limit(1); if (typeof user.roles === "string") { diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 4c5dbf86..a54ce681 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -14,3 +14,4 @@ export * from "./removeInvitation"; export * from "./createOrgUser"; export * from "./adminUpdateUser2FA"; export * from "./adminGetUser"; +export * from "./updateOrgUser"; diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index edcc5dce..0535a79d 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, idpOidcConfig } from "@server/db"; import { idp, roles, userOrgs, users } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -61,12 +61,15 @@ async function queryUsers(orgId: string, limit: number, offset: number) { isOwner: userOrgs.isOwner, idpName: idp.name, idpId: users.idpId, + idpType: idp.type, + idpVariant: idpOidcConfig.variant, twoFactorEnabled: users.twoFactorEnabled, }) .from(users) .leftJoin(userOrgs, eq(users.userId, userOrgs.userId)) .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) .leftJoin(idp, eq(users.idpId, idp.idpId)) + .leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId)) .where(eq(userOrgs.orgId, orgId)) .groupBy(users.userId) .limit(limit) diff --git a/server/routers/user/updateOrgUser.ts b/server/routers/user/updateOrgUser.ts new file mode 100644 index 00000000..fb00b59f --- /dev/null +++ b/server/routers/user/updateOrgUser.ts @@ -0,0 +1,112 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, userOrgs } 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 { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + userId: z.string(), + orgId: z.string() + }) + .strict(); + +const bodySchema = z + .object({ + autoProvisioned: z.boolean().optional() + }) + .strict() + .refine((data) => Object.keys(data).length > 0, { + message: "At least one field must be provided for update" + }); + +registry.registerPath({ + method: "post", + path: "/org/{orgId}/user/{userId}", + description: "Update a user in an org.", + tags: [OpenAPITags.Org, OpenAPITags.User], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function updateOrgUser( + req: Request, + res: Response, + next: NextFunction +): Promise { + 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 { userId, orgId } = parsedParams.data; + + 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" + ) + ); + } + + const updateData = parsedBody.data; + + const [updatedUser] = await db + .update(userOrgs) + .set({ + ...updateData + }) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) + .returning(); + + return response(res, { + data: updatedUser, + success: true, + error: false, + message: "Org user updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index a30daf43..8ca33b8a 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -4,7 +4,8 @@ import { handleGetConfigMessage, handleDockerStatusMessage, handleDockerContainersMessage, - handleNewtPingRequestMessage + handleNewtPingRequestMessage, + handleApplyBlueprintMessage } from "../newt"; import { handleOlmRegisterMessage, @@ -23,7 +24,8 @@ export const messageHandlers: Record = { "olm/ping": handleOlmPingMessage, "newt/socket/status": handleDockerStatusMessage, "newt/socket/containers": handleDockerContainersMessage, - "newt/ping/request": handleNewtPingRequestMessage + "newt/ping/request": handleNewtPingRequestMessage, + "newt/blueprint/apply": handleApplyBlueprintMessage, }; startOlmOfflineChecker(); // this is to handle the offline check for olms diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 6b3f20b9..c5950e1d 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -9,6 +9,7 @@ import m1 from "./scriptsPg/1.6.0"; import m2 from "./scriptsPg/1.7.0"; import m3 from "./scriptsPg/1.8.0"; import m4 from "./scriptsPg/1.9.0"; +import m5 from "./scriptsPg/1.10.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -18,7 +19,8 @@ const migrations = [ { version: "1.6.0", run: m1 }, { version: "1.7.0", run: m2 }, { version: "1.8.0", run: m3 }, - { version: "1.9.0", run: m4 } + { version: "1.9.0", run: m4 }, + { version: "1.10.0", run: m5 }, // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 5b0850c8..b8fa64f0 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -26,6 +26,8 @@ import m21 from "./scriptsSqlite/1.6.0"; import m22 from "./scriptsSqlite/1.7.0"; import m23 from "./scriptsSqlite/1.8.0"; import m24 from "./scriptsSqlite/1.9.0"; +import m25 from "./scriptsSqlite/1.10.0"; +import m26 from "./scriptsSqlite/1.10.1"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -51,6 +53,8 @@ const migrations = [ { version: "1.7.0", run: m22 }, { version: "1.8.0", run: m23 }, { version: "1.9.0", run: m24 }, + { version: "1.10.0", run: m25 }, + { version: "1.10.1", run: m26 }, // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.10.0.ts b/server/setup/scriptsPg/1.10.0.ts new file mode 100644 index 00000000..3be2f697 --- /dev/null +++ b/server/setup/scriptsPg/1.10.0.ts @@ -0,0 +1,147 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; +import { __DIRNAME, APP_PATH } from "@server/lib/consts"; +import { readFileSync } from "fs"; +import path, { join } from "path"; + +const version = "1.10.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + const resources = await db.execute(sql` + SELECT "resourceId" FROM "resources" + `); + + const siteResources = await db.execute(sql` + SELECT "siteResourceId" FROM "siteResources" + `); + + await db.execute(sql`BEGIN`); + + await db.execute( + sql`ALTER TABLE "exitNodes" ADD COLUMN "region" text;` + ); + + await db.execute( + sql`ALTER TABLE "idpOidcConfig" ADD COLUMN "variant" text DEFAULT 'oidc' NOT NULL;` + ); + + await db.execute( + sql`ALTER TABLE "resources" ADD COLUMN "niceId" text DEFAULT '' NOT NULL;` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "niceId" text DEFAULT '' NOT NULL;` + ); + + await db.execute( + sql`ALTER TABLE "userOrgs" ADD COLUMN "autoProvisioned" boolean DEFAULT false;` + ); + + await db.execute( + sql`ALTER TABLE "targets" ADD COLUMN "pathMatchType" text;` + ); + + await db.execute(sql`ALTER TABLE "targets" ADD COLUMN "path" text;`); + + await db.execute( + sql`ALTER TABLE "resources" ADD COLUMN "headers" text;` + ); + + const usedNiceIds: string[] = []; + + for (const resource of resources.rows) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + await db.execute(sql` + UPDATE "resources" SET "niceId" = ${niceId} WHERE "resourceId" = ${resource.resourceId} + `); + } + + for (const resource of siteResources.rows) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + await db.execute(sql` + UPDATE "siteResources" SET "niceId" = ${niceId} WHERE "siteResourceId" = ${resource.siteResourceId} + `); + } + + // Handle auto-provisioned users for identity providers + const autoProvisionIdps = await db.execute(sql` + SELECT "idpId" FROM "idp" WHERE "autoProvision" = true + `); + + for (const idp of autoProvisionIdps.rows) { + // Get all users with this identity provider + const usersWithIdp = await db.execute(sql` + SELECT "id" FROM "user" WHERE "idpId" = ${idp.idpId} + `); + + // Update userOrgs to set autoProvisioned to true for these users + for (const user of usersWithIdp.rows) { + await db.execute(sql` + UPDATE "userOrgs" SET "autoProvisioned" = true WHERE "userId" = ${user.id} + `); + } + } + + await db.execute(sql`COMMIT`); + console.log(`Migrated database`); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Failed to migrate db:", e); + throw e; + } +} + +const dev = process.env.ENVIRONMENT !== "prod"; +let file; +if (!dev) { + file = join(__DIRNAME, "names.json"); +} else { + file = join("server/db/names.json"); +} +export const names = JSON.parse(readFileSync(file, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // clean out any non-alphanumeric characters except for dashes + return name.replace(/[^a-z0-9-]/g, ""); +} diff --git a/server/setup/scriptsSqlite/1.10.0.ts b/server/setup/scriptsSqlite/1.10.0.ts new file mode 100644 index 00000000..3065a664 --- /dev/null +++ b/server/setup/scriptsSqlite/1.10.0.ts @@ -0,0 +1,136 @@ +import { __DIRNAME, APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import { readFileSync } from "fs"; +import path, { join } from "path"; + +const version = "1.10.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + const resources = db + .prepare( + "SELECT resourceId FROM resources" + ) + .all() as Array<{ resourceId: number }>; + + const siteResources = db + .prepare( + "SELECT siteResourceId FROM siteResources" + ) + .all() as Array<{ siteResourceId: number }>; + + db.transaction(() => { + db.exec(` + ALTER TABLE 'exitNodes' ADD 'region' text; + ALTER TABLE 'idpOidcConfig' ADD 'variant' text DEFAULT 'oidc' NOT NULL; + ALTER TABLE 'resources' ADD 'niceId' text DEFAULT '' NOT NULL; + ALTER TABLE 'siteResources' ADD 'niceId' text DEFAULT '' NOT NULL; + ALTER TABLE 'userOrgs' ADD 'autoProvisioned' integer DEFAULT false; + ALTER TABLE 'targets' ADD 'pathMatchType' text; + ALTER TABLE 'targets' ADD 'path' text; + ALTER TABLE 'resources' ADD 'headers' text; + `); // this diverges from the schema a bit because the schema does not have a default on niceId but was required for the migration and I dont think it will effect much down the line... + + const usedNiceIds: string[] = []; + + for (const resourceId of resources) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + db.prepare( + `UPDATE resources SET niceId = ? WHERE resourceId = ?` + ).run(niceId, resourceId.resourceId); + } + + for (const resourceId of siteResources) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + db.prepare( + `UPDATE siteResources SET niceId = ? WHERE siteResourceId = ?` + ).run(niceId, resourceId.siteResourceId); + } + + // Handle auto-provisioned users for identity providers + const autoProvisionIdps = db + .prepare( + "SELECT idpId FROM idp WHERE autoProvision = 1" + ) + .all() as Array<{ idpId: number }>; + + for (const idp of autoProvisionIdps) { + // Get all users with this identity provider + const usersWithIdp = db + .prepare( + "SELECT id FROM user WHERE idpId = ?" + ) + .all(idp.idpId) as Array<{ id: string }>; + + // Update userOrgs to set autoProvisioned to true for these users + for (const user of usersWithIdp) { + db.prepare( + "UPDATE userOrgs SET autoProvisioned = 1 WHERE userId = ?" + ).run(user.id); + } + } + })(); + + console.log(`Migrated database`); + } catch (e) { + console.log("Failed to migrate db:", e); + throw e; + } +} + +const dev = process.env.ENVIRONMENT !== "prod"; +let file; +if (!dev) { + file = join(__DIRNAME, "names.json"); +} else { + file = join("server/db/names.json"); +} +export const names = JSON.parse(readFileSync(file, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // clean out any non-alphanumeric characters except for dashes + return name.replace(/[^a-z0-9-]/g, ""); +} diff --git a/server/setup/scriptsSqlite/1.10.1.ts b/server/setup/scriptsSqlite/1.10.1.ts new file mode 100644 index 00000000..3608e92e --- /dev/null +++ b/server/setup/scriptsSqlite/1.10.1.ts @@ -0,0 +1,69 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.10.1"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + db.pragma("foreign_keys = OFF"); + + db.transaction(() => { + db.exec(`ALTER TABLE "targets" RENAME TO "targets_old"; +--> statement-breakpoint +CREATE TABLE "targets" ( + "targetId" INTEGER PRIMARY KEY AUTOINCREMENT, + "resourceId" INTEGER NOT NULL, + "siteId" INTEGER NOT NULL, + "ip" TEXT NOT NULL, + "method" TEXT, + "port" INTEGER NOT NULL, + "internalPort" INTEGER, + "enabled" INTEGER NOT NULL DEFAULT 1, + "path" TEXT, + "pathMatchType" TEXT, + FOREIGN KEY ("resourceId") REFERENCES "resources"("resourceId") ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ("siteId") REFERENCES "sites"("siteId") ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO "targets" ( + "targetId", + "resourceId", + "siteId", + "ip", + "method", + "port", + "internalPort", + "enabled", + "path", + "pathMatchType" +) +SELECT + targetId, + resourceId, + siteId, + ip, + method, + port, + internalPort, + enabled, + path, + pathMatchType +FROM "targets_old"; +--> statement-breakpoint +DROP TABLE "targets_old";`); + })(); + + db.pragma("foreign_keys = ON"); + + console.log(`Migrated database`); + } catch (e) { + console.log("Failed to migrate db:", e); + throw e; + } +} diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 4740198b..4c3ac07b 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -1,8 +1,8 @@ import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; import { cache } from "react"; -import OrganizationLandingCard from "./OrganizationLandingCard"; -import MemberResourcesPortal from "./MemberResourcesPortal"; +import OrganizationLandingCard from "../../components/OrganizationLandingCard"; +import MemberResourcesPortal from "../../components/MemberResourcesPortal"; import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; diff --git a/src/app/[orgId]/settings/access/invitations/page.tsx b/src/app/[orgId]/settings/access/invitations/page.tsx index 665b9a43..d7fee322 100644 --- a/src/app/[orgId]/settings/access/invitations/page.tsx +++ b/src/app/[orgId]/settings/access/invitations/page.tsx @@ -1,13 +1,13 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; -import InvitationsTable, { InvitationRow } from "./InvitationsTable"; +import InvitationsTable, { InvitationRow } from "../../../../../components/InvitationsTable"; import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; -import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav"; +import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from 'next-intl/server'; diff --git a/src/app/[orgId]/settings/access/layout.tsx b/src/app/[orgId]/settings/access/layout.tsx index 2dd20177..51be3b5e 100644 --- a/src/app/[orgId]/settings/access/layout.tsx +++ b/src/app/[orgId]/settings/access/layout.tsx @@ -1,7 +1,6 @@ interface AccessLayoutProps { children: React.ReactNode; params: Promise<{ - resourceId: number | string; orgId: string; }>; } diff --git a/src/app/[orgId]/settings/access/roles/page.tsx b/src/app/[orgId]/settings/access/roles/page.tsx index 8faedbf8..cffe4ed9 100644 --- a/src/app/[orgId]/settings/access/roles/page.tsx +++ b/src/app/[orgId]/settings/access/roles/page.tsx @@ -5,7 +5,7 @@ import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import { ListRolesResponse } from "@server/routers/role"; -import RolesTable, { RoleRow } from "./RolesTable"; +import RolesTable, { RoleRow } from "../../../../../components/RolesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from 'next-intl/server'; diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index c56c45cc..fdce89eb 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -8,6 +8,7 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; +import { Checkbox } from "@app/components/ui/checkbox"; import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { SetUserRolesResponse } from "@server/routers/user"; @@ -34,6 +35,8 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { useTranslations } from "next-intl"; +import IdpTypeBadge from "@app/components/IdpTypeBadge"; +import { UserType } from "@server/types/UserTypes"; export default function AccessControlsPage() { const { orgUser: user } = userOrgUserContext(); @@ -61,14 +64,16 @@ export default function AccessControlsPage() { text: z.string() }) ) - .min(1, { message: t('accessRoleSelectPlease') }) + .min(1, { message: t('accessRoleSelectPlease') }), + autoProvisioned: z.boolean() }); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { username: user.username!, - roles: [] + roles: [], + autoProvisioned: user.autoProvisioned || false } }); @@ -80,10 +85,10 @@ export default function AccessControlsPage() { console.error(e); toast({ variant: "destructive", - title: t('accessRoleErrorFetch'), + title: t("accessRoleErrorFetch"), description: formatAxiosError( e, - t('accessRoleErrorFetchDescription') + t("accessRoleErrorFetchDescription") ) }); }); @@ -107,31 +112,38 @@ export default function AccessControlsPage() { text: i.name })) ); + form.setValue("autoProvisioned", user.autoProvisioned || false); }, []); async function onSubmit(values: z.infer) { setLoading(true); - const res = await api - .post< - AxiosResponse - >(`/org/${user.orgId}/user/${user.userId}/roles`, { roleIds: values.roles.map((r) => parseInt(r.id)) }) - .catch((e) => { - toast({ - variant: "destructive", - title: t('accessRoleErrorAdd'), - description: formatAxiosError( - e, - t('accessRoleErrorAddDescription') - ) - }); - }); + try { + // Execute both API calls simultaneously + const [roleRes, userRes] = await Promise.all([ + api.post>(`/org/${user.orgId}/user/${user.userId}/roles`, { + roleIds: values.roles.map((r) => parseInt(r.id)) } + ), + api.post(`/org/${orgId}/user/${user.userId}`, { + autoProvisioned: values.autoProvisioned + }) + ]); - if (res && res.status === 200) { + if (roleRes.status === 200 && userRes.status === 200) { + toast({ + variant: "default", + title: t("userSaved"), + description: t("userSavedDescription") + }); + } + } catch (e) { toast({ - variant: "default", - title: t('userSaved'), - description: t('userSavedDescription') + variant: "destructive", + title: t("accessRoleErrorAdd"), + description: formatAxiosError( + e, + t("accessRoleErrorAddDescription") + ) }); } @@ -142,9 +154,11 @@ export default function AccessControlsPage() { - {t('accessControls')} + + {t("accessControls")} + - {t('accessControlsDescription')} + {t("accessControlsDescription")} @@ -156,12 +170,29 @@ export default function AccessControlsPage() { className="space-y-4" id="access-controls-form" > + {/* IDP Type Display */} + {user.type !== UserType.Internal && + user.idpType && ( +
+ + {t("idp")}: + + +
+ )} + ( - Roles + {t('roles')} )} /> + + {user.idpAutoProvision && ( + ( + + + + +
+ + {t("autoProvisioned")} + +

+ {t( + "autoProvisionedDescription" + )} +

+
+
+ )} + /> + )} @@ -212,7 +276,7 @@ export default function AccessControlsPage() { disabled={loading} form="access-controls-form" > - {t('accessControlsSubmit')} + {t("accessControlsSubmit")}
diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index 6ae00b61..47bff1e1 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -46,6 +46,7 @@ import { Checkbox } from "@app/components/ui/checkbox"; import { ListIdpsResponse } from "@server/routers/idp"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; +import Image from "next/image"; type UserType = "internal" | "oidc"; @@ -53,6 +54,17 @@ interface IdpOption { idpId: number; name: string; type: string; + variant: string | null; +} + +interface UserOption { + id: string; + title: string; + description: string; + disabled: boolean; + icon?: React.ReactNode; + idpId?: number; + variant?: string | null; } export default function Page() { @@ -62,14 +74,14 @@ export default function Page() { const api = createApiClient({ env }); const t = useTranslations(); - const [userType, setUserType] = useState("internal"); + const [selectedOption, setSelectedOption] = useState("internal"); const [inviteLink, setInviteLink] = useState(null); const [loading, setLoading] = useState(false); const [expiresInDays, setExpiresInDays] = useState(1); const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); const [idps, setIdps] = useState([]); const [sendEmail, setSendEmail] = useState(env.email.emailEnabled); - const [selectedIdp, setSelectedIdp] = useState(null); + const [userOptions, setUserOptions] = useState([]); const [dataLoaded, setDataLoaded] = useState(false); const internalFormSchema = z.object({ @@ -80,7 +92,13 @@ export default function Page() { roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }) }); - const externalFormSchema = z.object({ + const googleAzureFormSchema = z.object({ + email: z.string().email({ message: t("emailInvalid") }), + name: z.string().optional(), + roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }) + }); + + const genericOidcFormSchema = z.object({ username: z.string().min(1, { message: t("usernameRequired") }), email: z .string() @@ -88,19 +106,51 @@ export default function Page() { .optional() .or(z.literal("")), name: z.string().optional(), - roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }), - idpId: z.string().min(1, { message: t("idpSelectPlease") }) + roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }) }); const formatIdpType = (type: string) => { switch (type.toLowerCase()) { case "oidc": return t("idpGenericOidc"); + case "google": + return t("idpGoogleDescription"); + case "azure": + return t("idpAzureDescription"); default: return type; } }; + const getIdpIcon = (variant: string | null) => { + if (!variant) return null; + + switch (variant.toLowerCase()) { + case "google": + return ( + {t("idpGoogleAlt")} + ); + case "azure": + return ( + {t("idpAzureAlt")} + ); + default: + return null; + } + }; + const validFor = [ { hours: 24, name: t("day", { count: 1 }) }, { hours: 48, name: t("day", { count: 2 }) }, @@ -120,45 +170,39 @@ export default function Page() { } }); - const externalForm = useForm>({ - resolver: zodResolver(externalFormSchema), + const googleAzureForm = useForm>({ + resolver: zodResolver(googleAzureFormSchema), + defaultValues: { + email: "", + name: "", + roleId: "" + } + }); + + const genericOidcForm = useForm>({ + resolver: zodResolver(genericOidcFormSchema), defaultValues: { username: "", email: "", name: "", - roleId: "", - idpId: "" + roleId: "" } }); useEffect(() => { - if (userType === "internal") { + if (selectedOption === "internal") { setSendEmail(env.email.emailEnabled); internalForm.reset(); setInviteLink(null); setExpiresInDays(1); - } else if (userType === "oidc") { - externalForm.reset(); + } else if (selectedOption && selectedOption !== "internal") { + googleAzureForm.reset(); + genericOidcForm.reset(); } - }, [userType, env.email.emailEnabled, internalForm, externalForm]); - - const [userTypes, setUserTypes] = useState[]>([ - { - id: "internal", - title: t("userTypeInternal"), - description: t("userTypeInternalDescription"), - disabled: false - }, - { - id: "oidc", - title: t("userTypeExternal"), - description: t("userTypeExternalDescription"), - disabled: true - } - ]); + }, [selectedOption, env.email.emailEnabled, internalForm, googleAzureForm, genericOidcForm]); useEffect(() => { - if (!userType) { + if (!selectedOption) { return; } @@ -199,20 +243,6 @@ export default function Page() { if (res?.status === 200) { setIdps(res.data.data.idps); - - if (res.data.data.idps.length) { - setUserTypes((prev) => - prev.map((type) => { - if (type.id === "oidc") { - return { - ...type, - disabled: false - }; - } - return type; - }) - ); - } } } @@ -226,6 +256,33 @@ export default function Page() { fetchInitialData(); }, []); + // Build user options when IDPs are loaded + useEffect(() => { + const options: UserOption[] = [ + { + id: "internal", + title: t("userTypeInternal"), + description: t("userTypeInternalDescription"), + disabled: false + } + ]; + + // Add IDP options + idps.forEach((idp) => { + options.push({ + id: `idp-${idp.idpId}`, + title: idp.name, + description: formatIdpType(idp.variant || idp.type), + disabled: false, + icon: getIdpIcon(idp.variant), + idpId: idp.idpId, + variant: idp.variant + }); + }); + + setUserOptions(options); + }, [idps, t]); + async function onSubmitInternal( values: z.infer ) { @@ -274,9 +331,52 @@ export default function Page() { setLoading(false); } - async function onSubmitExternal( - values: z.infer + async function onSubmitGoogleAzure( + values: z.infer ) { + const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + if (!selectedUserOption?.idpId) return; + + setLoading(true); + + const res = await api + .put(`/org/${orgId}/user`, { + username: values.email, // Use email as username for Google/Azure + email: values.email, + name: values.name, + type: "oidc", + idpId: selectedUserOption.idpId, + roleId: parseInt(values.roleId) + }) + .catch((e) => { + toast({ + variant: "destructive", + title: t("userErrorCreate"), + description: formatAxiosError( + e, + t("userErrorCreateDescription") + ) + }); + }); + + if (res && res.status === 201) { + toast({ + variant: "default", + title: t("userCreated"), + description: t("userCreatedDescription") + }); + router.push(`/${orgId}/settings/access/users`); + } + + setLoading(false); + } + + async function onSubmitGenericOidc( + values: z.infer + ) { + const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + if (!selectedUserOption?.idpId) return; + setLoading(true); const res = await api @@ -285,7 +385,7 @@ export default function Page() { email: values.email, name: values.name, type: "oidc", - idpId: parseInt(values.idpId), + idpId: selectedUserOption.idpId, roleId: parseInt(values.roleId) }) .catch((e) => { @@ -330,7 +430,7 @@ export default function Page() {
- {!inviteLink && build !== "saas" ? ( + {!inviteLink && build !== "saas" && dataLoaded ? ( @@ -342,15 +442,15 @@ export default function Page() { { - setUserType(value as UserType); + setSelectedOption(value); if (value === "internal") { internalForm.reset(); - } else if (value === "oidc") { - externalForm.reset(); - setSelectedIdp(null); + } else { + googleAzureForm.reset(); + genericOidcForm.reset(); } }} cols={2} @@ -359,7 +459,7 @@ export default function Page() { ) : null} - {userType === "internal" && dataLoaded && ( + {selectedOption === "internal" && dataLoaded && ( <> {!inviteLink ? ( @@ -564,71 +664,7 @@ export default function Page() { )} - {userType !== "internal" && dataLoaded && ( - <> - - - - {t("idpTitle")} - - - {t("idpSelect")} - - - - {idps.length === 0 ? ( -

- {t("idpNotConfigured")} -

- ) : ( -
- ( - - ({ - id: idp.idpId.toString(), - title: idp.name, - description: - formatIdpType( - idp.type - ) - }) - )} - defaultValue={ - field.value - } - onChange={( - value - ) => { - field.onChange( - value - ); - const idp = - idps.find( - (idp) => - idp.idpId.toString() === - value - ); - setSelectedIdp( - idp || null - ); - }} - cols={2} - /> - - - )} - /> - - )} -
-
- - {idps.length > 0 && ( + {selectedOption && selectedOption !== "internal" && dataLoaded && ( @@ -640,144 +676,206 @@ export default function Page() { -
- - ( - - - {t( - "username" - )} - - - - -

- {t( - "usernameUniq" - )} -

- -
+ {/* Google/Azure Form */} + {(() => { + const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + return selectedUserOption?.variant === "google" || selectedUserOption?.variant === "azure"; + })() && ( + + - - ( - - - {t( - "emailOptional" - )} - - - - - - - )} - /> - - ( - - - {t( - "nameOptional" - )} - - - - - - - )} - /> - - ( - - - {t("role")} - - - - {roles.map( - ( - role - ) => ( + + + )} + /> + + ( + + + {t("nameOptional")} + + + + + + + )} + /> + + ( + + + {t("role")} + + - - + ))} + + + + + )} + /> + + + )} + + {/* Generic OIDC Form */} + {(() => { + const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + return selectedUserOption?.variant !== "google" && selectedUserOption?.variant !== "azure"; + })() && ( +
+ -
- + className="space-y-4" + id="create-user-form" + > + ( + + + {t("username")} + + + + +

+ {t("usernameUniq")} +

+ +
+ )} + /> + + ( + + + {t("emailOptional")} + + + + + + + )} + /> + + ( + + + {t("nameOptional")} + + + + + + + )} + /> + + ( + + + {t("role")} + + + + + )} + /> + + + )}
- )} - )}
- {userType && dataLoaded && ( + {selectedOption && dataLoaded && ( + ); + } + + return ( +
+ + { + const value = e.target.value.trim(); + if (!value) { + setShowPathInput(false); + updateTarget(row.original.targetId, { + ...row.original, + path: null, + pathMatchType: null + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + path: value + }); + } + }} + /> + + + +
+ ); + } + }, { accessorKey: "siteId", header: t("site"), @@ -546,7 +693,7 @@ export default function ReverseProxyTargets(props: { className={cn( "justify-between flex-1", !row.original.siteId && - "text-muted-foreground" + "text-muted-foreground" )} > {row.original.siteId @@ -597,49 +744,59 @@ export default function ReverseProxyTargets(props: { - {selectedSite && selectedSite.type === "newt" && (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })()} + {selectedSite && + selectedSite.type === "newt" && + (() => { + const dockerState = getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })()}
); } }, ...(resource.http ? [ - { - accessorKey: "method", - header: t("method"), - cell: ({ row }: { row: Row }) => ( - - ) - } - ] + { + accessorKey: "method", + header: t("method"), + cell: ({ row }: { row: Row }) => ( + + ) + } + ] : []), { accessorKey: "ip", @@ -658,9 +815,13 @@ export default function ReverseProxyTargets(props: { if (parsed) { updateTarget(row.original.targetId, { ...row.original, - method: hasProtocol ? parsed.protocol : row.original.method, + method: hasProtocol + ? parsed.protocol + : row.original.method, ip: parsed.host, - port: hasPort ? parsed.port : row.original.port + port: hasPort + ? parsed.port + : row.original.port }); } else { updateTarget(row.original.targetId, { @@ -807,21 +968,21 @@ export default function ReverseProxyTargets(props: { className={cn( "justify-between flex-1", !field.value && - "text-muted-foreground" + "text-muted-foreground" )} > {field.value ? sites.find( - ( - site - ) => - site.siteId === - field.value - ) - ?.name + ( + site + ) => + site.siteId === + field.value + ) + ?.name : t( - "siteSelect" - )} + "siteSelect" + )} @@ -887,18 +1048,35 @@ export default function ReverseProxyTargets(props: { ); return selectedSite && selectedSite.type === - "newt" ? (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })() : null; + "newt" + ? (() => { + const dockerState = + getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })() + : null; })()}
@@ -964,25 +1142,59 @@ export default function ReverseProxyTargets(props: { name="ip" render={({ field }) => ( - {t("targetAddr")} + + {t("targetAddr")} + { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); + const input = + e.target.value.trim(); + const hasProtocol = + /^(https?|h2c):\/\//.test( + input + ); + const hasPort = + /:\d+(?:\/|$)/.test( + input + ); - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); + if ( + hasProtocol || + hasPort + ) { + const parsed = + parseHostTarget( + input + ); if (parsed) { - if (hasProtocol || !addTargetForm.getValues("method")) { - addTargetForm.setValue("method", parsed.protocol); + if ( + hasProtocol || + !addTargetForm.getValues( + "method" + ) + ) { + addTargetForm.setValue( + "method", + parsed.protocol + ); } - addTargetForm.setValue("ip", parsed.host); - if (hasPort || !addTargetForm.getValues("port")) { - addTargetForm.setValue("port", parsed.port); + addTargetForm.setValue( + "ip", + parsed.host + ); + if ( + hasPort || + !addTargetForm.getValues( + "port" + ) + ) { + addTargetForm.setValue( + "port", + parsed.port + ); } } } else { @@ -1091,12 +1303,12 @@ export default function ReverseProxyTargets(props: { {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} @@ -1256,6 +1468,31 @@ export default function ReverseProxyTargets(props: { )} /> + ( + + + {t("customHeaders")} + + + { + field.onChange( + value + ); + }} + rows={4} + /> + + + + )} + /> diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx similarity index 98% rename from src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx rename to src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx index 424d7973..8b5e4709 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx @@ -128,7 +128,7 @@ export default function ResourceRules(props: { try { const res = await api.get< AxiosResponse - >(`/resource/${params.resourceId}/rules`); + >(`/resource/${resource.resourceId}/rules`); if (res.status === 200) { setRules(res.data.data.rules); } @@ -251,7 +251,7 @@ export default function ResourceRules(props: { // Save rules enabled state const res = await api - .post(`/resource/${params.resourceId}`, { + .post(`/resource/${resource.resourceId}`, { applyRules: rulesEnabled }) .catch((err) => { @@ -336,13 +336,13 @@ export default function ResourceRules(props: { if (rule.new) { const res = await api.put( - `/resource/${params.resourceId}/rule`, + `/resource/${resource.resourceId}/rule`, data ); rule.ruleId = res.data.data.ruleId; } else if (rule.updated) { await api.post( - `/resource/${params.resourceId}/rule/${rule.ruleId}`, + `/resource/${resource.resourceId}/rule/${rule.ruleId}`, data ); } @@ -361,7 +361,7 @@ export default function ResourceRules(props: { for (const ruleId of rulesToRemove) { await api.delete( - `/resource/${params.resourceId}/rule/${ruleId}` + `/resource/${resource.resourceId}/rule/${ruleId}` ); setRules(rules.filter((r) => r.ruleId !== ruleId)); } diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index 782b3135..71628ce7 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -58,7 +58,7 @@ import { } from "@app/components/ui/popover"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; -import { SquareArrowOutUpRight } from "lucide-react"; +import { ArrowRight, MoveRight, SquareArrowOutUpRight } from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; import Link from "next/link"; import { useTranslations } from "next-intl"; @@ -90,7 +90,9 @@ import { ListTargetsResponse } from "@server/routers/target"; import { DockerManager, DockerState } from "@app/lib/docker"; import { parseHostTarget } from "@app/lib/parseHostTarget"; import { toASCII, toUnicode } from 'punycode'; -import { DomainRow } from "../../domains/DomainsTable"; +import { DomainRow } from "../../../../../components/DomainsTable"; +import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; + const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), @@ -112,8 +114,42 @@ const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), port: z.coerce.number().int().positive(), - siteId: z.number().int().positive() -}); + siteId: z.number().int().positive(), + path: z.string().optional().nullable(), + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() +}).refine( + (data) => { + // If path is provided, pathMatchType must be provided + if (data.path && !data.pathMatchType) { + return false; + } + // If pathMatchType is provided, path must be provided + if (data.pathMatchType && !data.path) { + return false; + } + // Validate path based on pathMatchType + if (data.path && data.pathMatchType) { + switch (data.pathMatchType) { + case "exact": + case "prefix": + // Path should start with / + return data.path.startsWith("/"); + case "regex": + // Validate regex + try { + new RegExp(data.path); + return true; + } catch { + return false; + } + } + } + return true; + }, + { + message: "Invalid path configuration" + } +); type BaseResourceFormValues = z.infer; type HttpResourceFormValues = z.infer; @@ -202,7 +238,9 @@ export default function Page() { defaultValues: { ip: "", method: baseForm.watch("http") ? "http" : null, - port: "" as any as number + port: "" as any as number, + path: null, + pathMatchType: null } as z.infer }); @@ -273,6 +311,8 @@ export default function Page() { const newTarget: LocalTarget = { ...data, + path: data.path || null, + pathMatchType: data.pathMatchType || null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), @@ -284,7 +324,9 @@ export default function Page() { addTargetForm.reset({ ip: "", method: baseForm.watch("http") ? "http" : null, - port: "" as any as number + port: "" as any as number, + path: null, + pathMatchType: null }); } @@ -326,10 +368,17 @@ export default function Page() { http: baseData.http }; + let sanitizedSubdomain: string | undefined; + if (isHttp) { const httpData = httpForm.getValues(); + + sanitizedSubdomain = httpData.subdomain + ? finalizeSubdomainSanitize(httpData.subdomain) + : undefined; + Object.assign(payload, { - subdomain: httpData.subdomain ? toASCII(httpData.subdomain) : undefined, + subdomain: sanitizedSubdomain ? toASCII(sanitizedSubdomain) : undefined, domainId: httpData.domainId, protocol: "tcp" }); @@ -359,6 +408,7 @@ export default function Page() { if (res && res.status === 201) { const id = res.data.data.resourceId; + const niceId = res.data.data.niceId; setResourceId(id); // Create targets if any exist @@ -370,7 +420,9 @@ export default function Page() { port: target.port, method: target.method, enabled: target.enabled, - siteId: target.siteId + siteId: target.siteId, + path: target.path, + pathMatchType: target.pathMatchType }; await api.put(`/resource/${id}/target`, data); @@ -389,7 +441,7 @@ export default function Page() { } if (isHttp) { - router.push(`/${orgId}/settings/resources/${id}`); + router.push(`/${orgId}/settings/resources/${niceId}`); } else { const tcpUdpData = tcpUdpForm.getValues(); // Only show config snippets if enableProxy is explicitly true @@ -493,6 +545,98 @@ export default function Page() { }, []); const columns: ColumnDef[] = [ + { + accessorKey: "path", + header: t("matchPath"), + cell: ({ row }) => { + const [showPathInput, setShowPathInput] = useState( + !!(row.original.path || row.original.pathMatchType) + ); + + if (!showPathInput) { + return ( + + ); + } + + return ( +
+ + { + const value = e.target.value.trim(); + if (!value) { + setShowPathInput(false); + updateTarget(row.original.targetId, { + ...row.original, + path: null, + pathMatchType: null + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + path: value + }); + } + }} + /> + + + +
+ ); + } + }, { accessorKey: "siteId", header: t("site"), @@ -630,19 +774,29 @@ export default function Page() { defaultValue={row.original.ip} className="min-w-[150px]" onBlur={(e) => { - const parsed = parseHostTarget(e.target.value); + const input = e.target.value.trim(); + const hasProtocol = /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); - if (parsed) { - updateTarget(row.original.targetId, { - ...row.original, - method: parsed.protocol, - ip: parsed.host, - port: parsed.port ? Number(parsed.port) : undefined, - }); + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + updateTarget(row.original.targetId, { + ...row.original, + method: hasProtocol ? parsed.protocol : row.original.method, + ip: parsed.host, + port: hasPort ? parsed.port : row.original.port + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } } else { updateTarget(row.original.targetId, { ...row.original, - ip: e.target.value, + ip: input }); } }} @@ -744,6 +898,11 @@ export default function Page() {
{ + if (e.key === "Enter") { + e.preventDefault(); // block default enter refresh + } + }} className="space-y-4" id="base-resource-form" > @@ -856,6 +1015,11 @@ export default function Page() { { + if (e.key === "Enter") { + e.preventDefault(); // block default enter refresh + } + }} className="space-y-4" id="tcp-udp-settings-form" > @@ -1204,11 +1368,21 @@ export default function Page() { id="ip" {...field} onBlur={(e) => { - const parsed = parseHostTarget(e.target.value); - if (parsed) { - addTargetForm.setValue("method", parsed.protocol); - addTargetForm.setValue("ip", parsed.host); - addTargetForm.setValue("port", parsed.port); + const input = e.target.value.trim(); + const hasProtocol = /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + if (hasProtocol || !addTargetForm.getValues("method")) { + addTargetForm.setValue("method", parsed.protocol); + } + addTargetForm.setValue("ip", parsed.host); + if (hasPort || !addTargetForm.getValues("port")) { + addTargetForm.setValue("port", parsed.port); + } + } } else { field.onBlur(); } @@ -1420,6 +1594,9 @@ export default function Page() {

{t("resourceAddEntrypoints")}

+

+ {t("resourceAddEntrypointsEditFile")} +

{t("resourceExposePorts")} +

+ {t("resourceExposePortsEditFile")} +

; windows: Record; docker: Record; + kubernetes: Record; podman: Record; nixos: Record; }; @@ -83,6 +87,7 @@ type Commands = { const platforms = [ "linux", "docker", + "kubernetes", "podman", "mac", "windows", @@ -277,6 +282,18 @@ PersistentKeepalive = 5`; `docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] }, + kubernetes: { + "Helm Chart": [ + `helm repo add fossorial https://charts.fossorial.io`, + `helm repo update fossorial`, + `helm install newt fossorial/newt \\ + --create-namespace \\ + --set newtInstances[0].name="main-tunnel" \\ + --set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\ + --set-string newtInstances[0].auth.keys.idKey="${id}" \\ + --set-string newtInstances[0].auth.keys.secretKey="${secret}"` + ] + }, podman: { "Podman Quadlet": [ `[Unit] @@ -324,6 +341,8 @@ WantedBy=default.target` return ["x64"]; case "docker": return ["Docker Compose", "Docker Run"]; + case "kubernetes": + return ["Helm Chart"]; case "podman": return ["Podman Quadlet", "Podman Run"]; case "freebsd": @@ -345,6 +364,8 @@ WantedBy=default.target` return "macOS"; case "docker": return "Docker"; + case "kubernetes": + return "Kubernetes"; case "podman": return "Podman"; case "freebsd": @@ -391,6 +412,8 @@ WantedBy=default.target` return ; case "docker": return ; + case "kubernetes": + return ; case "podman": return ; case "freebsd": @@ -620,6 +643,11 @@ WantedBy=default.target` { + if (e.key === "Enter") { + e.preventDefault(); // block default enter refresh + } + }} className="space-y-4" id="create-site-form" > diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 10bcad53..a854083c 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -2,9 +2,9 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListSitesResponse } from "@server/routers/site"; import { AxiosResponse } from "axios"; -import SitesTable, { SiteRow } from "./SitesTable"; +import SitesTable, { SiteRow } from "../../../../components/SitesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import SitesSplashCard from "./SitesSplashCard"; +import SitesSplashCard from "../../../../components/SitesSplashCard"; import { getTranslations } from "next-intl/server"; type SitesPageProps = { diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx index 2f95c7fd..b5a61306 100644 --- a/src/app/admin/api-keys/create/page.tsx +++ b/src/app/admin/api-keys/create/page.tsx @@ -200,6 +200,11 @@ export default function Page() { { + if (e.key === "Enter") { + e.preventDefault(); // block default enter refresh + } + }} className="space-y-4" id="create-site-form" > diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx index 22607f2f..e518911f 100644 --- a/src/app/admin/api-keys/page.tsx +++ b/src/app/admin/api-keys/page.tsx @@ -3,7 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { ListRootApiKeysResponse } from "@server/routers/apiKeys"; -import ApiKeysTable, { ApiKeyRow } from "./ApiKeysTable"; +import ApiKeysTable, { ApiKeyRow } from "../../../components/ApiKeysTable"; import { getTranslations } from "next-intl/server"; type ApiKeyPageProps = {}; diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index aadd6eb8..01b186bf 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -31,7 +31,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react"; -import PolicyTable, { PolicyRow } from "./PolicyTable"; +import PolicyTable, { PolicyRow } from "../../../../../components/PolicyTable"; import { AxiosResponse } from "axios"; import { ListOrgsResponse } from "@server/routers/org"; import { diff --git a/src/app/admin/idp/page.tsx b/src/app/admin/idp/page.tsx index 4db77785..fef0990c 100644 --- a/src/app/admin/idp/page.tsx +++ b/src/app/admin/idp/page.tsx @@ -2,7 +2,7 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import IdpTable, { IdpRow } from "./AdminIdpTable"; +import IdpTable, { IdpRow } from "../../../components/AdminIdpTable"; import { getTranslations } from "next-intl/server"; export default async function IdpPage() { @@ -16,7 +16,7 @@ export default async function IdpPage() { } catch (e) { console.error(e); } - + const t = await getTranslations(); return ( diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index 6c5e4613..8e75ff24 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { UsersDataTable } from "./AdminUsersDataTable"; +import { UsersDataTable } from "@app/components/AdminUsersDataTable"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import { useRouter } from "next/navigation"; diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index e9673374..bf6547a3 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -3,7 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { AdminListUsersResponse } from "@server/routers/user/adminListUsers"; -import UsersTable, { GlobalUserRow } from "./AdminUsersTable"; +import UsersTable, { GlobalUserRow } from "../../../components/AdminUsersTable"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx index 1c0f8125..2ff8d09a 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx @@ -1,5 +1,5 @@ import { cookies } from "next/headers"; -import ValidateOidcToken from "./ValidateOidcToken"; +import ValidateOidcToken from "@app/components/ValidateOidcToken"; import { cache } from "react"; import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index cad0ce58..1068c4f7 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -2,7 +2,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import Link from "next/link"; import { redirect } from "next/navigation"; import { cache } from "react"; -import DashboardLoginForm from "./DashboardLoginForm"; +import DashboardLoginForm from "@app/components/DashboardLoginForm"; import { Mail } from "lucide-react"; import { pullEnv } from "@app/lib/pullEnv"; import { cleanRedirect } from "@app/lib/cleanRedirect"; @@ -42,7 +42,8 @@ export default async function Page(props: { )(); const loginIdps = idpsRes.data.data.idps.map((idp) => ({ idpId: idp.idpId, - name: idp.name + name: idp.name, + variant: idp.variant })) as LoginFormIDP[]; const t = await getTranslations(); diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index 596afb99..3d456bd9 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -35,7 +35,7 @@ import { ResetPasswordResponse } from "@server/routers/auth"; import { Loader2 } from "lucide-react"; -import { Alert, AlertDescription } from "../../../components/ui/alert"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; import { toast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/api"; @@ -210,7 +210,7 @@ export default function ResetPasswordForm({ } catch (verificationError) { console.error("Failed to send verification code:", verificationError); } - + if (redirect) { router.push(`/auth/verify-email?redirect=${redirect}`); } else { @@ -254,8 +254,8 @@ export default function ResetPasswordForm({ {quickstart ? t('completeAccountSetup') : t('passwordReset')} - {quickstart - ? t('completeAccountSetupDescription') + {quickstart + ? t('completeAccountSetupDescription') : t('passwordResetDescription') } @@ -282,8 +282,8 @@ export default function ResetPasswordForm({ - {quickstart - ? t('accountSetupSent') + {quickstart + ? t('accountSetupSent') : t('passwordResetSent') } @@ -325,8 +325,8 @@ export default function ResetPasswordForm({ render={({ field }) => ( - {quickstart - ? t('accountSetupCode') + {quickstart + ? t('accountSetupCode') : t('passwordResetCode') } @@ -338,8 +338,8 @@ export default function ResetPasswordForm({ - {quickstart - ? t('accountSetupCodeDescription') + {quickstart + ? t('accountSetupCodeDescription') : t('passwordResetCodeDescription') } @@ -354,8 +354,8 @@ export default function ResetPasswordForm({ render={({ field }) => ( - {quickstart - ? t('passwordCreate') + {quickstart + ? t('passwordCreate') : t('passwordNew') } @@ -375,8 +375,8 @@ export default function ResetPasswordForm({ render={({ field }) => ( - {quickstart - ? t('passwordCreateConfirm') + {quickstart + ? t('passwordCreateConfirm') : t('passwordNewConfirm') } @@ -490,8 +490,8 @@ export default function ResetPasswordForm({ {isSubmitting && ( )} - {quickstart - ? t('accountSetupSubmit') + {quickstart + ? t('accountSetupSubmit') : t('passwordResetSubmit') } diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx index f06c7c4c..490f89f7 100644 --- a/src/app/auth/reset-password/page.tsx +++ b/src/app/auth/reset-password/page.tsx @@ -1,7 +1,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { cache } from "react"; -import ResetPasswordForm from "./ResetPasswordForm"; +import ResetPasswordForm from "@app/components/ResetPasswordForm"; import Link from "next/link"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 347d3586..25580ee7 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -2,20 +2,20 @@ import { GetResourceAuthInfoResponse, GetExchangeTokenResponse } from "@server/routers/resource"; -import ResourceAuthPortal from "./ResourceAuthPortal"; +import ResourceAuthPortal from "@app/components/ResourceAuthPortal"; import { internal, priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; import { cache } from "react"; import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; -import ResourceNotFound from "./ResourceNotFound"; -import ResourceAccessDenied from "./ResourceAccessDenied"; -import AccessToken from "./AccessToken"; +import ResourceNotFound from "@app/components/ResourceNotFound"; +import ResourceAccessDenied from "@app/components/ResourceAccessDenied"; +import AccessToken from "@app/components/AccessToken"; import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; import { ListIdpsResponse } from "@server/routers/idp"; -import AutoLoginHandler from "./AutoLoginHandler"; +import AutoLoginHandler from "@app/components/AutoLoginHandler"; export const dynamic = "force-dynamic"; diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index 673e69bf..b4f4fddd 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -1,4 +1,4 @@ -import SignupForm from "@app/app/auth/signup/SignupForm"; +import SignupForm from "@app/components/SignupForm"; import { verifySession } from "@app/lib/auth/verifySession"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; @@ -11,7 +11,7 @@ import { getTranslations } from "next-intl/server"; export const dynamic = "force-dynamic"; export default async function Page(props: { - searchParams: Promise<{ + searchParams: Promise<{ redirect: string | undefined; email: string | undefined; }>; diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx index 10ad809f..c549abf0 100644 --- a/src/app/auth/verify-email/page.tsx +++ b/src/app/auth/verify-email/page.tsx @@ -1,4 +1,4 @@ -import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm"; +import VerifyEmailForm from "@app/components/VerifyEmailForm"; import { verifySession } from "@app/lib/auth/verifySession"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 2e0c11e2..49c5a2c5 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -4,7 +4,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { AcceptInviteResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import InviteStatusCard from "./InviteStatusCard"; +import InviteStatusCard from "../../components/InviteStatusCard"; import { formatAxiosError } from "@app/lib/api"; import { getTranslations } from "next-intl/server"; @@ -72,14 +72,14 @@ export default async function InvitePage(props: { const type = cardType(); if (!user && type === "user_does_not_exist") { - const redirectUrl = emailParam + const redirectUrl = emailParam ? `/auth/signup?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` : `/auth/signup?redirect=/invite?token=${params.token}`; redirect(redirectUrl); } if (!user && type === "not_logged_in") { - const redirectUrl = emailParam + const redirectUrl = emailParam ? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` : `/auth/login?redirect=/invite?token=${params.token}`; redirect(redirectUrl); diff --git a/src/app/s/[accessToken]/page.tsx b/src/app/s/[accessToken]/page.tsx index d28ff7be..61299366 100644 --- a/src/app/s/[accessToken]/page.tsx +++ b/src/app/s/[accessToken]/page.tsx @@ -1,4 +1,4 @@ -import AccessToken from "@app/app/auth/resource/[resourceId]/AccessToken"; +import AccessToken from "@app/components/AccessToken"; export default async function ResourceAuthPage(props: { params: Promise<{ accessToken: string }>; diff --git a/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx b/src/components/AccessPageHeaderAndNav.tsx similarity index 100% rename from src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx rename to src/components/AccessPageHeaderAndNav.tsx diff --git a/src/app/auth/resource/[resourceId]/AccessToken.tsx b/src/components/AccessToken.tsx similarity index 100% rename from src/app/auth/resource/[resourceId]/AccessToken.tsx rename to src/components/AccessToken.tsx diff --git a/src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx b/src/components/AccessTokenUsage.tsx similarity index 100% rename from src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx rename to src/components/AccessTokenUsage.tsx diff --git a/src/app/admin/idp/AdminIdpDataTable.tsx b/src/components/AdminIdpDataTable.tsx similarity index 100% rename from src/app/admin/idp/AdminIdpDataTable.tsx rename to src/components/AdminIdpDataTable.tsx diff --git a/src/app/admin/idp/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx similarity index 94% rename from src/app/admin/idp/AdminIdpTable.tsx rename to src/components/AdminIdpTable.tsx index fa7de6da..8849ba25 100644 --- a/src/app/admin/idp/AdminIdpTable.tsx +++ b/src/components/AdminIdpTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { IdpDataTable } from "./AdminIdpDataTable"; +import { IdpDataTable } from "@app/components/AdminIdpDataTable"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import { useState } from "react"; @@ -20,12 +20,14 @@ import { } from "@app/components/ui/dropdown-menu"; import Link from "next/link"; import { useTranslations } from "next-intl"; +import IdpTypeBadge from "./IdpTypeBadge"; export type IdpRow = { idpId: number; name: string; type: string; orgCount: number; + variant?: string; }; type Props = { @@ -57,15 +59,6 @@ export default function IdpTable({ idps }: Props) { } }; - const getTypeDisplay = (type: string) => { - switch (type) { - case "oidc": - return "OAuth2/OIDC"; - default: - return type; - } - }; - const columns: ColumnDef[] = [ { accessorKey: "idpId", @@ -116,9 +109,8 @@ export default function IdpTable({ idps }: Props) { }, cell: ({ row }) => { const type = row.original.type; - return ( - {getTypeDisplay(type)} - ); + const variant = row.original.variant; + return ; } }, { diff --git a/src/app/admin/users/AdminUsersDataTable.tsx b/src/components/AdminUsersDataTable.tsx similarity index 100% rename from src/app/admin/users/AdminUsersDataTable.tsx rename to src/components/AdminUsersDataTable.tsx diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx new file mode 100644 index 00000000..8e75ff24 --- /dev/null +++ b/src/components/AdminUsersTable.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { UsersDataTable } from "@app/components/AdminUsersDataTable"; +import { Button } from "@app/components/ui/button"; +import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; + +export type GlobalUserRow = { + id: string; + name: string | null; + username: string; + email: string | null; + type: string; + idpId: number | null; + idpName: string; + dateCreated: string; + twoFactorEnabled: boolean | null; + twoFactorSetupRequested: boolean | null; +}; + +type Props = { + users: GlobalUserRow[]; +}; + +export default function UsersTable({ users }: Props) { + const router = useRouter(); + const t = useTranslations(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selected, setSelected] = useState(null); + const [rows, setRows] = useState(users); + + const api = createApiClient(useEnvContext()); + + const deleteUser = (id: string) => { + api.delete(`/user/${id}`) + .catch((e) => { + console.error(t("userErrorDelete"), e); + toast({ + variant: "destructive", + title: t("userErrorDelete"), + description: formatAxiosError(e, t("userErrorDelete")) + }); + }) + .then(() => { + router.refresh(); + setIsDeleteModalOpen(false); + + const newRows = rows.filter((row) => row.id !== id); + + setRows(newRows); + }); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "username", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "email", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "idpName", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "twoFactorEnabled", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const userRow = row.original; + + return ( +
+ + {userRow.twoFactorEnabled || + userRow.twoFactorSetupRequested ? ( + + {t("enabled")} + + ) : ( + {t("disabled")} + )} + +
+ ); + } + }, + { + id: "actions", + cell: ({ row }) => { + const r = row.original; + return ( + <> +
+ + + + + + { + setSelected(r); + setIsDeleteModalOpen(true); + }} + > + {t("delete")} + + + + +
+ + ); + } + } + ]; + + return ( + <> + {selected && ( + { + setIsDeleteModalOpen(val); + setSelected(null); + }} + dialog={ +
+

+ {t("userQuestionRemove", { + selectedUser: + selected?.email || + selected?.name || + selected?.username + })} +

+ +

+ {t("userMessageRemove")} +

+ +

{t("userMessageConfirm")}

+
+ } + buttonText={t("userDeleteConfirm")} + onConfirm={async () => deleteUser(selected!.id)} + string={ + selected.email || selected.name || selected.username + } + title={t("userDeleteServer")} + /> + )} + + + + ); +} diff --git a/src/app/admin/api-keys/ApiKeysDataTable.tsx b/src/components/ApiKeysDataTable.tsx similarity index 100% rename from src/app/admin/api-keys/ApiKeysDataTable.tsx rename to src/components/ApiKeysDataTable.tsx diff --git a/src/app/admin/api-keys/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx similarity index 98% rename from src/app/admin/api-keys/ApiKeysTable.tsx rename to src/components/ApiKeysTable.tsx index 02aead9e..99094651 100644 --- a/src/app/admin/api-keys/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -18,7 +18,7 @@ import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import moment from "moment"; -import { ApiKeysDataTable } from "./ApiKeysDataTable"; +import { ApiKeysDataTable } from "@app/components/ApiKeysDataTable"; import { useTranslations } from "next-intl"; export type ApiKeyRow = { diff --git a/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx b/src/components/AutoLoginHandler.tsx similarity index 100% rename from src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx rename to src/components/AutoLoginHandler.tsx diff --git a/src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx similarity index 100% rename from src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx rename to src/components/ClientInfoCard.tsx diff --git a/src/app/[orgId]/settings/clients/ClientsDataTable.tsx b/src/components/ClientsDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/clients/ClientsDataTable.tsx rename to src/components/ClientsDataTable.tsx diff --git a/src/app/[orgId]/settings/clients/ClientsTable.tsx b/src/components/ClientsTable.tsx similarity index 99% rename from src/app/[orgId]/settings/clients/ClientsTable.tsx rename to src/components/ClientsTable.tsx index 7fa81622..fc7c7c84 100644 --- a/src/app/[orgId]/settings/clients/ClientsTable.tsx +++ b/src/components/ClientsTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { ClientsDataTable } from "./ClientsDataTable"; +import { ClientsDataTable } from "@app/components/ClientsDataTable"; import { DropdownMenu, DropdownMenuContent, diff --git a/src/app/[orgId]/settings/domains/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx similarity index 99% rename from src/app/[orgId]/settings/domains/CreateDomainForm.tsx rename to src/components/CreateDomainForm.tsx index e609a8ac..77fdea9c 100644 --- a/src/app/[orgId]/settings/domains/CreateDomainForm.tsx +++ b/src/components/CreateDomainForm.tsx @@ -238,7 +238,7 @@ export default function CreateDomainForm({ {t("domain")} @@ -604,4 +604,4 @@ export default function CreateDomainForm({ ); -} \ No newline at end of file +} diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index ccfddcd8..63dfc11d 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -352,7 +352,7 @@ export default function CreateInternalResourceDialog({ render={({ field }) => ( - {t("createInternalResourceDialogDestinationIP")} + {t("targetAddr")} ( - {t("createInternalResourceDialogDestinationPort")} + {t("targetPort")} void; cols?: number; + hideFreeDomain?: boolean; } export default function DomainPicker2({ orgId, onDomainChange, - cols = 2 + cols = 2, + hideFreeDomain = false }: DomainPicker2Props) { const { env } = useEnvContext(); const api = createApiClient({ env }); @@ -153,12 +155,12 @@ export default function DomainPicker2({ fullDomain: firstOrgDomain.baseDomain, baseDomain: firstOrgDomain.baseDomain }); - } else if (build === "saas" || build === "enterprise") { + } else if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { // If no organization domains, select the provided domain option const domainOptionText = build === "enterprise" - ? "Provided Domain" - : "Free Provided Domain"; + ? t("domainPickerProvidedDomain") + : t("domainPickerFreeProvidedDomain"); const freeDomainOption: DomainOption = { id: "provided-search", domain: domainOptionText, @@ -171,8 +173,8 @@ export default function DomainPicker2({ console.error("Failed to load organization domains:", error); toast({ variant: "destructive", - title: "Error", - description: "Failed to load organization domains" + title: t("domainPickerError"), + description: t("domainPickerErrorLoadDomains") }); } finally { setLoadingDomains(false); @@ -180,7 +182,7 @@ export default function DomainPicker2({ }; loadOrganizationDomains(); - }, [orgId, api]); + }, [orgId, api, hideFreeDomain]); const checkAvailability = useCallback( async (input: string) => { @@ -202,8 +204,8 @@ export default function DomainPicker2({ setAvailableOptions([]); toast({ variant: "destructive", - title: "Error", - description: "Failed to check domain availability" + title: t("domainPickerError"), + description: t("domainPickerErrorCheckAvailability") }); } finally { setIsChecking(false); @@ -246,11 +248,11 @@ export default function DomainPicker2({ }); }); - if (build === "saas" || build === "enterprise") { + if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { const domainOptionText = build === "enterprise" - ? "Provided Domain" - : "Free Provided Domain"; + ? t("domainPickerProvidedDomain") + : t("domainPickerFreeProvidedDomain"); options.push({ id: "provided-search", domain: domainOptionText, @@ -269,8 +271,8 @@ export default function DomainPicker2({ if (!sanitized) { toast({ variant: "destructive", - title: "Invalid subdomain", - description: `The input "${sub}" was removed because it's not valid.`, + title: t("domainPickerInvalidSubdomain"), + description: t("domainPickerInvalidSubdomainRemoved", { sub }), }); return ""; } @@ -283,16 +285,16 @@ export default function DomainPicker2({ if (!ok) { toast({ variant: "destructive", - title: "Invalid subdomain", - description: `"${sub}" could not be made valid for ${base.domain}.`, + title: t("domainPickerInvalidSubdomain"), + description: t("domainPickerInvalidSubdomainCannotMakeValid", { sub, domain: base.domain }), }); return ""; } if (sub !== sanitized) { toast({ - title: "Subdomain sanitized", - description: `"${sub}" was corrected to "${sanitized}"`, + title: t("domainPickerSubdomainSanitized"), + description: t("domainPickerSubdomainCorrected", { sub, sanitized }), }); } @@ -453,7 +455,7 @@ export default function DomainPicker2({ /> {showSubdomainInput && subdomainInput && !isValidSubdomainStructure(subdomainInput) && (

- This subdomain contains invalid characters or structure. It will be sanitized automatically when you save. + {t("domainPickerInvalidSubdomainStructure")}

)} {showSubdomainInput && !subdomainInput && ( @@ -555,8 +557,8 @@ export default function DomainPicker2({ {orgDomain.type.toUpperCase()}{" "} •{" "} {orgDomain.verified - ? "Verified" - : "Unverified"} + ? t("domainPickerVerified") + : t("domainPickerUnverified")} {(build === "saas" || - build === "enterprise") && ( + build === "enterprise") && !hideFreeDomain && ( )} )} {(build === "saas" || - build === "enterprise") && ( + build === "enterprise") && !hideFreeDomain && ( {build === "enterprise" - ? "Provided Domain" - : "Free Provided Domain"} + ? t("domainPickerProvidedDomain") + : t("domainPickerFreeProvidedDomain")} {t( @@ -771,4 +773,4 @@ function debounce any>( func(...args); }, wait); }; -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/domains/DomainsDataTable.tsx b/src/components/DomainsDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/domains/DomainsDataTable.tsx rename to src/components/DomainsDataTable.tsx diff --git a/src/app/[orgId]/settings/domains/DomainsTable.tsx b/src/components/DomainsTable.tsx similarity index 98% rename from src/app/[orgId]/settings/domains/DomainsTable.tsx rename to src/components/DomainsTable.tsx index 84bc8bc6..5bafe935 100644 --- a/src/app/[orgId]/settings/domains/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { DomainsDataTable } from "./DomainsDataTable"; +import { DomainsDataTable } from "@app/components/DomainsDataTable"; import { Button } from "@app/components/ui/button"; import { ArrowUpDown } from "lucide-react"; import { useState } from "react"; @@ -12,7 +12,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { Badge } from "@app/components/ui/badge"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import CreateDomainForm from "./CreateDomainForm"; +import CreateDomainForm from "@app/components/CreateDomainForm"; import { useToast } from "@app/hooks/useToast"; import { useOrgContext } from "@app/hooks/useOrgContext"; diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index adfed1b7..d09f0b6c 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -221,7 +221,7 @@ export default function EditInternalResourceDialog({ name="destinationIp" render={({ field }) => ( - {t("editInternalResourceDialogDestinationIP")} + {t("targetAddr")} @@ -235,7 +235,7 @@ export default function EditInternalResourceDialog({ name="destinationPort" render={({ field }) => ( - {t("editInternalResourceDialogDestinationPort")} + {t("targetPort")} void; + placeholder?: string; + rows?: number; + className?: string; +} + +export function HeadersInput({ + value = "", + onChange, + placeholder = `X-Example-Header: example-value +X-Another-Header: another-value`, + rows = 4, + className +}: HeadersInputProps) { + const [internalValue, setInternalValue] = useState(""); + + // Convert comma-separated to newline-separated for display + const convertToNewlineSeparated = (commaSeparated: string): string => { + if (!commaSeparated || commaSeparated.trim() === "") return ""; + + return commaSeparated + .split(',') + .map(header => header.trim()) + .filter(header => header.length > 0) + .join('\n'); + }; + + // Convert newline-separated to comma-separated for output + const convertToCommaSeparated = (newlineSeparated: string): string => { + if (!newlineSeparated || newlineSeparated.trim() === "") return ""; + + return newlineSeparated + .split('\n') + .map(header => header.trim()) + .filter(header => header.length > 0) + .join(', '); + }; + + // Update internal value when external value changes + useEffect(() => { + setInternalValue(convertToNewlineSeparated(value)); + }, [value]); + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInternalValue(newValue); + + // Convert back to comma-separated format for the parent + const commaSeparatedValue = convertToCommaSeparated(newValue); + onChange(commaSeparatedValue); + }; + + return ( +