diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b0627cc..41d43bd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,12 @@ on: jobs: test: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - name: Install Node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: '22' @@ -57,8 +58,26 @@ jobs: echo "App failed to start" exit 1 + build-sqlite: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml + - name: Build Docker image sqlite - run: make build-sqlite + run: make dev-build-sqlite + + build-postgres: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml - name: Build Docker image pg - run: make build-pg + run: make dev-build-pg diff --git a/Dockerfile b/Dockerfile index fa2d71c0..c59490b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,23 +43,25 @@ RUN test -f dist/server.mjs RUN npm run build:cli +# Prune dev dependencies and clean up to prepare for copy to runner +RUN npm prune --omit=dev && npm cache clean --force + FROM node:24-alpine AS runner WORKDIR /app -# Curl used for the health checks -# Python and build tools needed for better-sqlite3 native compilation -RUN apk add --no-cache curl tzdata python3 make g++ +# Only curl and tzdata needed at runtime - no build tools! +RUN apk add --no-cache curl tzdata -# COPY package.json package-lock.json ./ -COPY package*.json ./ - -RUN npm ci --omit=dev && npm cache clean --force +# Copy pre-built node_modules from builder (already pruned to production only) +# This includes the compiled native modules like better-sqlite3 +COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/dist ./dist COPY --from=builder /app/init ./dist/init +COPY --from=builder /app/package.json ./package.json COPY ./cli/wrapper.sh /usr/local/bin/pangctl RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs diff --git a/cli/commands/clearExitNodes.ts b/cli/commands/clearExitNodes.ts new file mode 100644 index 00000000..092e9761 --- /dev/null +++ b/cli/commands/clearExitNodes.ts @@ -0,0 +1,36 @@ +import { CommandModule } from "yargs"; +import { db, exitNodes } from "@server/db"; +import { eq } from "drizzle-orm"; + +type ClearExitNodesArgs = { }; + +export const clearExitNodes: CommandModule< + {}, + ClearExitNodesArgs +> = { + command: "clear-exit-nodes", + describe: + "Clear all exit nodes from the database", + // no args + builder: (yargs) => { + return yargs; + }, + handler: async (argv: {}) => { + try { + + console.log(`Clearing all exit nodes from the database`); + + // Delete all exit nodes + const deletedCount = await db + .delete(exitNodes) + .where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)); // delete all + + console.log(`Deleted ${deletedCount.changes} exit node(s) from the database`); + + process.exit(0); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } + } +}; diff --git a/cli/commands/rotateServerSecret.ts b/cli/commands/rotateServerSecret.ts new file mode 100644 index 00000000..1d954af2 --- /dev/null +++ b/cli/commands/rotateServerSecret.ts @@ -0,0 +1,284 @@ +import { CommandModule } from "yargs"; +import { db, idpOidcConfig, licenseKey } from "@server/db"; +import { encrypt, decrypt } from "@server/lib/crypto"; +import { configFilePath1, configFilePath2 } from "@server/lib/consts"; +import { eq } from "drizzle-orm"; +import fs from "fs"; +import yaml from "js-yaml"; + +type RotateServerSecretArgs = { + oldSecret: string; + newSecret: string; + force?: boolean; +}; + +export const rotateServerSecret: CommandModule< + {}, + RotateServerSecretArgs +> = { + command: "rotate-server-secret", + describe: + "Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret", + builder: (yargs) => { + return yargs + .option("oldSecret", { + type: "string", + demandOption: true, + describe: "The current server secret (for verification)" + }) + .option("newSecret", { + type: "string", + demandOption: true, + describe: "The new server secret to use" + }) + .option("force", { + type: "boolean", + default: false, + describe: + "Force rotation even if the old secret doesn't match the config file. " + + "Use this if you know the old secret is correct but the config file is out of sync. " + + "WARNING: This will attempt to decrypt all values with the provided old secret. " + + "If the old secret is incorrect, the rotation will fail or corrupt data." + }); + }, + handler: async (argv: { + oldSecret: string; + newSecret: string; + force?: boolean; + }) => { + try { + // Determine which config file exists + const configPath = fs.existsSync(configFilePath1) + ? configFilePath1 + : fs.existsSync(configFilePath2) + ? configFilePath2 + : null; + + if (!configPath) { + console.error( + "Error: Config file not found. Expected config.yml or config.yaml in the config directory." + ); + process.exit(1); + } + + // Read current config + const configContent = fs.readFileSync(configPath, "utf8"); + const config = yaml.load(configContent) as any; + + if (!config?.server?.secret) { + console.error( + "Error: No server secret found in config file. Cannot rotate." + ); + process.exit(1); + } + + const configSecret = config.server.secret; + const oldSecret = argv.oldSecret; + const newSecret = argv.newSecret; + const force = argv.force || false; + + // Verify that the provided old secret matches the one in config + if (configSecret !== oldSecret) { + if (!force) { + console.error( + "Error: The provided old secret does not match the secret in the config file." + ); + console.error( + "\nIf you are certain the old secret is correct and the config file is out of sync," + ); + console.error( + "you can use the --force flag to bypass this check." + ); + console.error( + "\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail" + ); + console.error( + "or corrupt encrypted data. Only use --force if you are absolutely certain." + ); + process.exit(1); + } else { + console.warn( + "\nWARNING: Using --force flag. Bypassing old secret verification." + ); + console.warn( + "The provided old secret does not match the config file, but proceeding anyway." + ); + console.warn( + "If the old secret is incorrect, this operation will fail or corrupt data.\n" + ); + } + } + + // Validate new secret + if (newSecret.length < 8) { + console.error( + "Error: New secret must be at least 8 characters long" + ); + process.exit(1); + } + + if (oldSecret === newSecret) { + console.error("Error: New secret must be different from old secret"); + process.exit(1); + } + + console.log("Starting server secret rotation..."); + console.log("This will decrypt and re-encrypt all encrypted values in the database."); + + // Read all data first + console.log("\nReading encrypted data from database..."); + const idpConfigs = await db.select().from(idpOidcConfig); + const licenseKeys = await db.select().from(licenseKey); + + console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`); + console.log(`Found ${licenseKeys.length} license key(s)`); + + // Prepare all decrypted and re-encrypted values + console.log("\nDecrypting and re-encrypting values..."); + + type IdpUpdate = { + idpOauthConfigId: number; + encryptedClientId: string; + encryptedClientSecret: string; + }; + + type LicenseKeyUpdate = { + oldLicenseKeyId: string; + newLicenseKeyId: string; + encryptedToken: string; + encryptedInstanceId: string; + }; + + const idpUpdates: IdpUpdate[] = []; + const licenseKeyUpdates: LicenseKeyUpdate[] = []; + + // Process idpOidcConfig entries + for (const idpConfig of idpConfigs) { + try { + // Decrypt with old secret + const decryptedClientId = decrypt(idpConfig.clientId, oldSecret); + const decryptedClientSecret = decrypt( + idpConfig.clientSecret, + oldSecret + ); + + // Re-encrypt with new secret + const encryptedClientId = encrypt(decryptedClientId, newSecret); + const encryptedClientSecret = encrypt( + decryptedClientSecret, + newSecret + ); + + idpUpdates.push({ + idpOauthConfigId: idpConfig.idpOauthConfigId, + encryptedClientId, + encryptedClientSecret + }); + } catch (error) { + console.error( + `Error processing IdP config ${idpConfig.idpOauthConfigId}:`, + error + ); + throw error; + } + } + + // Process licenseKey entries + for (const key of licenseKeys) { + try { + // Decrypt with old secret + const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret); + const decryptedToken = decrypt(key.token, oldSecret); + const decryptedInstanceId = decrypt(key.instanceId, oldSecret); + + // Re-encrypt with new secret + const encryptedLicenseKeyId = encrypt( + decryptedLicenseKeyId, + newSecret + ); + const encryptedToken = encrypt(decryptedToken, newSecret); + const encryptedInstanceId = encrypt( + decryptedInstanceId, + newSecret + ); + + licenseKeyUpdates.push({ + oldLicenseKeyId: key.licenseKeyId, + newLicenseKeyId: encryptedLicenseKeyId, + encryptedToken, + encryptedInstanceId + }); + } catch (error) { + console.error( + `Error processing license key ${key.licenseKeyId}:`, + error + ); + throw error; + } + } + + // Perform all database updates in a single transaction + console.log("\nUpdating database in transaction..."); + await db.transaction(async (trx) => { + // Update idpOidcConfig entries + for (const update of idpUpdates) { + await trx + .update(idpOidcConfig) + .set({ + clientId: update.encryptedClientId, + clientSecret: update.encryptedClientSecret + }) + .where( + eq( + idpOidcConfig.idpOauthConfigId, + update.idpOauthConfigId + ) + ); + } + + // Update licenseKey entries (delete old, insert new) + for (const update of licenseKeyUpdates) { + // Delete old entry + await trx + .delete(licenseKey) + .where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId)); + + // Insert new entry with re-encrypted values + await trx.insert(licenseKey).values({ + licenseKeyId: update.newLicenseKeyId, + token: update.encryptedToken, + instanceId: update.encryptedInstanceId + }); + } + }); + + console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`); + console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`); + + // Update config file with new secret + console.log("\nUpdating config file..."); + config.server.secret = newSecret; + const newConfigContent = yaml.dump(config, { + indent: 2, + lineWidth: -1 + }); + fs.writeFileSync(configPath, newConfigContent, "utf8"); + + console.log(`Updated config file: ${configPath}`); + + console.log("\nServer secret rotation completed successfully!"); + console.log(`\nSummary:`); + console.log(` - OIDC IdP configurations: ${idpUpdates.length}`); + console.log(` - License keys: ${licenseKeyUpdates.length}`); + console.log( + `\n IMPORTANT: Restart the server for the new secret to take effect.` + ); + + process.exit(0); + } catch (error) { + console.error("Error rotating server secret:", error); + process.exit(1); + } + } +}; + diff --git a/cli/index.ts b/cli/index.ts index f9e884cc..f44f41ba 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -4,10 +4,14 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { setAdminCredentials } from "@cli/commands/setAdminCredentials"; import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys"; +import { clearExitNodes } from "./commands/clearExitNodes"; +import { rotateServerSecret } from "./commands/rotateServerSecret"; yargs(hideBin(process.argv)) .scriptName("pangctl") .command(setAdminCredentials) .command(resetUserSecurityKeys) + .command(clearExitNodes) + .command(rotateServerSecret) .demandCommand() .help().argv; diff --git a/messages/en-US.json b/messages/en-US.json index ee1a7c20..a3aca4e1 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -100,6 +100,7 @@ "siteTunnelDescription": "Determine how you want to connect to the site", "siteNewtCredentials": "Credentials", "siteNewtCredentialsDescription": "This is how the site will authenticate with the server", + "remoteNodeCredentialsDescription": "This is how the remote node will authenticate with the server", "siteCredentialsSave": "Save the Credentials", "siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", "siteInfo": "Site Information", @@ -419,7 +420,7 @@ "userErrorExistsDescription": "This user is already a member of the organization.", "inviteError": "Failed to invite user", "inviteErrorDescription": "An error occurred while inviting the user", - "userInvited": "User invited", + "userInvited": "User Invited", "userInvitedDescription": "The user has been successfully invited.", "userErrorCreate": "Failed to create user", "userErrorCreateDescription": "An error occurred while creating the user", @@ -1035,6 +1036,7 @@ "updateOrgUser": "Update Org User", "createOrgUser": "Create Org User", "actionUpdateOrg": "Update Organization", + "actionRemoveInvitation": "Remove Invitation", "actionUpdateUser": "Update User", "actionGetUser": "Get User", "actionGetOrgUser": "Get Organization User", @@ -1673,7 +1675,7 @@ "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", "remoteExitNodeManageRemoteExitNodes": "Remote Nodes", - "remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud", + "remoteExitNodeDescription": "Self-host your own remote relay and proxy server nodes", "remoteExitNodes": "Nodes", "searchRemoteExitNodes": "Search nodes...", "remoteExitNodeAdd": "Add Node", @@ -1683,20 +1685,22 @@ "remoteExitNodeConfirmDelete": "Confirm Delete Node", "remoteExitNodeDelete": "Delete Node", "sidebarRemoteExitNodes": "Remote Nodes", + "remoteExitNodeId": "ID", + "remoteExitNodeSecretKey": "Secret", "remoteExitNodeCreate": { - "title": "Create Node", - "description": "Create a new node to extend network connectivity", + "title": "Create Remote Node", + "description": "Create a new self-hosted remote relay and proxy server node", "viewAllButton": "View All Nodes", "strategy": { "title": "Creation Strategy", - "description": "Choose this to manually configure the node or generate new credentials.", + "description": "Select how you want to create the remote node", "adopt": { "title": "Adopt Node", "description": "Choose this if you already have the credentials for the node." }, "generate": { "title": "Generate Keys", - "description": "Choose this if you want to generate new keys for the node" + "description": "Choose this if you want to generate new keys for the node." } }, "adopt": { @@ -1809,9 +1813,30 @@ "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Subnet", "subnetDescription": "The subnet for this organization's network configuration.", - "authPage": "Auth Page", - "authPageDescription": "Configure the auth page for the organization", + "customDomain": "Custom Domain", + "authPage": "Authentication Pages", + "authPageDescription": "Set a custom domain for the organization's authentication pages", "authPageDomain": "Auth Page Domain", + "authPageBranding": "Custom Branding", + "authPageBrandingDescription": "Configure the branding that appears on authentication pages for this organization", + "authPageBrandingUpdated": "Auth page Branding updated successfully", + "authPageBrandingRemoved": "Auth page Branding removed successfully", + "authPageBrandingRemoveTitle": "Remove Auth Page Branding", + "authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?", + "authPageBrandingDeleteConfirm": "Confirm Delete Branding", + "brandingLogoURL": "Logo URL", + "brandingPrimaryColor": "Primary Color", + "brandingLogoWidth": "Width (px)", + "brandingLogoHeight": "Height (px)", + "brandingOrgTitle": "Title for Organization Auth Page", + "brandingOrgDescription": "{orgName} will be replaced with the organization's name", + "brandingOrgSubtitle": "Subtitle for Organization Auth Page", + "brandingResourceTitle": "Title for Resource Auth Page", + "brandingResourceSubtitle": "Subtitle for Resource Auth Page", + "brandingResourceDescription": "{resourceName} will be replaced with the organization's name", + "saveAuthPageDomain": "Save Domain", + "saveAuthPageBranding": "Save Branding", + "removeAuthPageBranding": "Remove Branding", "noDomainSet": "No domain set", "changeDomain": "Change Domain", "selectDomain": "Select Domain", @@ -1820,7 +1845,7 @@ "setAuthPageDomain": "Set Auth Page Domain", "failedToFetchCertificate": "Failed to fetch certificate", "failedToRestartCertificate": "Failed to restart certificate", - "addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for the organization", + "addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.", "selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page", "domainPickerProvidedDomain": "Provided Domain", "domainPickerFreeProvidedDomain": "Free Provided Domain", @@ -1892,7 +1917,7 @@ "securityPolicyChangeWarningText": "This will affect all users in the organization", "authPageErrorUpdateMessage": "An error occurred while updating the auth page settings", "authPageErrorUpdate": "Unable to update auth page", - "authPageUpdated": "Auth page updated successfully", + "authPageDomainUpdated": "Auth page Domain updated successfully", "healthCheckNotAvailable": "Local", "rewritePath": "Rewrite Path", "rewritePathDescription": "Optionally rewrite the path before forwarding to the target.", @@ -2280,5 +2305,17 @@ "agent": "Agent", "personalUseOnly": "Personal Use Only", "loginPageLicenseWatermark": "This instance is licensed for personal use only.", - "instanceIsUnlicensed": "This instance is unlicensed." + "instanceIsUnlicensed": "This instance is unlicensed.", + "portRestrictions": "Port Restrictions", + "allPorts": "All", + "custom": "Custom", + "allPortsAllowed": "All Ports Allowed", + "allPortsBlocked": "All Ports Blocked", + "tcpPortsDescription": "Specify which TCP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 80,443,8000-9000).", + "udpPortsDescription": "Specify which UDP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 53,123,500-600).", + "organizationLoginPageTitle": "Organization Login Page", + "organizationLoginPageDescription": "Customize the login page for this organization", + "resourceLoginPageTitle": "Resource Login Page", + "resourceLoginPageDescription": "Customize the login page for individual resources", + "enterConfirmation": "Enter confirmation" } diff --git a/package-lock.json b/package-lock.json index b3a18c31..cdf2f6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -461,14 +461,36 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.946.0.tgz", + "integrity": "sha512-JYj3BPqgyRXgBjZ3Xvo4Abd+vLxcsHe4gb0TvwiSM/k7e6MRgBZoYwDOnwbNDs/62X1sn7MPHqqB3miuO4nR5g==", + "dev": true, +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { "version": "3.948.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", +>>>>>>> dev "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", +<<<<<<< HEAD + "@aws-sdk/core": "3.946.0", + "@aws-sdk/credential-provider-node": "3.946.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.946.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/signature-v4-multi-region": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.946.0", +======= "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", @@ -479,6 +501,7 @@ "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", +>>>>>>> dev "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", @@ -510,10 +533,68 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.946.0.tgz", + "integrity": "sha512-kGAs5iIVyUz4p6TX3pzG5q3cNxXnVpC4pwRC6DCSaSv9ozyPjc2d74FsK4fZ+J+ejtvCdJk72uiuQtWJc86Wuw==", + "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.946.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.946.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.946.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.946.0.tgz", + "integrity": "sha512-u2BkbLLVbMFrEiXrko2+S6ih5sUZPlbVyRPtXOqMHlCyzr70sE8kIiD6ba223rQeIFPcYfW/wHc6k4ihW2xxVg==", + "dev": true, +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { "version": "3.947.0", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", +>>>>>>> dev "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -534,6 +615,16 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.946.0.tgz", + "integrity": "sha512-P4l+K6wX1tf8LmWUvZofdQ+BgCNyk6Tb9u1H10npvqpuCD+dCM4pXIBq3PQcv/juUBOvLGGREo+Govuh3lfD0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { "version": "3.947.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", @@ -541,6 +632,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.947.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", @@ -550,6 +642,16 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.946.0.tgz", + "integrity": "sha512-/zeOJ6E7dGZQ/l2k7KytEoPJX0APIhwt0A79hPf/bUpMF4dDs2P6JmchDrotk0a0Y/MIdNF8sBQ/MEOPnBiYoQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { "version": "3.947.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", @@ -557,6 +659,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.947.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", @@ -571,6 +674,23 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.946.0.tgz", + "integrity": "sha512-Pdgcra3RivWj/TuZmfFaHbqsvvgnSKO0CxlRUMMr0PgBiCnUhyl+zBktdNOeGsOPH2fUzQpYhcUjYUgVSdcSDQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", + "@aws-sdk/credential-provider-env": "3.946.0", + "@aws-sdk/credential-provider-http": "3.946.0", + "@aws-sdk/credential-provider-login": "3.946.0", + "@aws-sdk/credential-provider-process": "3.946.0", + "@aws-sdk/credential-provider-sso": "3.946.0", + "@aws-sdk/credential-provider-web-identity": "3.946.0", + "@aws-sdk/nested-clients": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.948.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz", @@ -585,6 +705,7 @@ "@aws-sdk/credential-provider-sso": "3.948.0", "@aws-sdk/credential-provider-web-identity": "3.948.0", "@aws-sdk/nested-clients": "3.948.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", @@ -596,6 +717,21 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.946.0.tgz", + "integrity": "sha512-I7URUqnBPng1a5y81OImxrwERysZqMBREG6svhhGeZgxmqcpAZ8z5ywILeQXdEOCuuES8phUp/ojzxFjPXp/eA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.946.0", + "@aws-sdk/credential-provider-http": "3.946.0", + "@aws-sdk/credential-provider-ini": "3.946.0", + "@aws-sdk/credential-provider-process": "3.946.0", + "@aws-sdk/credential-provider-sso": "3.946.0", + "@aws-sdk/credential-provider-web-identity": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-login": { "version": "3.948.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz", @@ -627,6 +763,7 @@ "@aws-sdk/credential-provider-process": "3.947.0", "@aws-sdk/credential-provider-sso": "3.948.0", "@aws-sdk/credential-provider-web-identity": "3.948.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", @@ -638,6 +775,16 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.946.0.tgz", + "integrity": "sha512-GtGHX7OGqIeVQ3DlVm5RRF43Qmf3S1+PLJv9svrdvAhAdy2bUb044FdXXqrtSsIfpzTKlHgQUiRo5MWLd35Ntw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { "version": "3.947.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", @@ -645,6 +792,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.947.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -655,6 +803,18 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.946.0.tgz", + "integrity": "sha512-LeGSSt2V5iwYey1ENGY75RmoDP3bA2iE/py8QBKW8EDA8hn74XBLkprhrK5iccOvU3UGWY8WrEKFAFGNjJOL9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.946.0", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/token-providers": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.948.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz", @@ -664,6 +824,7 @@ "@aws-sdk/client-sso": "3.948.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/token-providers": "3.948.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -674,6 +835,17 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.946.0.tgz", + "integrity": "sha512-ocBCvjWfkbjxElBI1QUxOnHldsNhoU0uOICFvuRDAZAoxvypJHN3m5BJkqb7gqorBbcv3LRgmBdEnWXOAvq+7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", + "@aws-sdk/nested-clients": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.948.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz", @@ -682,6 +854,7 @@ "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.948.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -714,7 +887,43 @@ "integrity": "sha512-DS2tm5YBKhPW2PthrRBDr6eufChbwXe0NjtTZcYDfUCXf0OR+W6cIqyKguwHMJ+IyYdey30AfVw9/Lb5KB8U8A==", "license": "Apache-2.0", "dependencies": { +<<<<<<< HEAD + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", + "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.946.0.tgz", + "integrity": "sha512-0UTFmFd8PX2k/jLu/DBmR+mmLQWAtUGHYps9Rjx3dcXNwaMLaa/39NoV3qn7Dwzfpqc6JZlZzBk+NDOCJIHW9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", +======= "@aws-sdk/core": "3.947.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/core": "^3.18.7", @@ -733,6 +942,16 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.946.0.tgz", + "integrity": "sha512-7QcljCraeaWQNuqmOoAyZs8KpZcuhPiqdeeKoRd397jVGNRehLFsZbIMOvwaluUDFY11oMyXOkQEERe1Zo2fCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.947.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", @@ -740,6 +959,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.947.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.7", @@ -751,24 +971,44 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.946.0.tgz", + "integrity": "sha512-rjAtEguukeW8mlyEQMQI56vxFoyWlaNwowmz1p1rav948SUjtrzjHAp4TOQWhibb7AR7BUTHBCgIcyCRjBEf4g==", + "dev": true, +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { "version": "3.948.0", "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", +>>>>>>> dev "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", +<<<<<<< HEAD + "@aws-sdk/core": "3.946.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.946.0", +======= "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.948.0", "@aws-sdk/middleware-user-agent": "3.947.0", +>>>>>>> dev "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", +<<<<<<< HEAD + "@aws-sdk/util-user-agent-node": "3.946.0", +======= "@aws-sdk/util-user-agent-node": "3.947.0", +>>>>>>> dev "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", @@ -806,7 +1046,28 @@ "integrity": "sha512-UaYmzoxf9q3mabIA2hc4T6x5YSFUG2BpNjAZ207EA1bnQMiK+d6vZvb83t7dIWL/U1de1sGV19c1C81Jf14rrA==", "license": "Apache-2.0", "dependencies": { +<<<<<<< HEAD + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.946.0.tgz", + "integrity": "sha512-61FZ685lKiJuQ06g6U7K3PL9EwKCxNm51wNlxyKV57nnl1GrLD0NC8O3/hDNkCQLNBArT9y3IXl2H7TtIxP8Jg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.946.0", +======= "@aws-sdk/middleware-sdk-s3": "3.947.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", @@ -817,6 +1078,17 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.946.0.tgz", + "integrity": "sha512-a5c+rM6CUPX2ExmUZ3DlbLlS5rQr4tbdoGcgBsjnAHiYx8MuMNAI+8M7wfjF13i2yvUQj5WEIddvLpayfEZj9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", + "@aws-sdk/nested-clients": "3.946.0", +======= "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { "version": "3.948.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz", @@ -825,6 +1097,7 @@ "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.948.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -841,7 +1114,55 @@ "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", "license": "Apache-2.0", "dependencies": { +<<<<<<< HEAD + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.946.0.tgz", + "integrity": "sha512-a2UwwvzbK5AxHKUBupfg4s7VnkqRAHjYsuezHnKCniczmT4HZfP1NnfwwvLKEH8qaTrwenxjKSfq4UWmWkvG+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.946.0", +======= "@aws-sdk/middleware-user-agent": "3.947.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", @@ -1071,10 +1392,17 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.946.0.tgz", + "integrity": "sha512-u2BkbLLVbMFrEiXrko2+S6ih5sUZPlbVyRPtXOqMHlCyzr70sE8kIiD6ba223rQeIFPcYfW/wHc6k4ihW2xxVg==", +======= "node_modules/@aws-sdk/credential-provider-node": { "version": "3.946.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.946.0.tgz", "integrity": "sha512-I7URUqnBPng1a5y81OImxrwERysZqMBREG6svhhGeZgxmqcpAZ8z5ywILeQXdEOCuuES8phUp/ojzxFjPXp/eA==", +>>>>>>> dev "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1293,14 +1621,23 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.946.0.tgz", + "integrity": "sha512-7QcljCraeaWQNuqmOoAyZs8KpZcuhPiqdeeKoRd397jVGNRehLFsZbIMOvwaluUDFY11oMyXOkQEERe1Zo2fCw==", +======= "node_modules/@aws-sdk/middleware-sdk-s3": { "version": "3.946.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.946.0.tgz", "integrity": "sha512-0UTFmFd8PX2k/jLu/DBmR+mmLQWAtUGHYps9Rjx3dcXNwaMLaa/39NoV3qn7Dwzfpqc6JZlZzBk+NDOCJIHW9g==", +>>>>>>> dev "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.946.0", +<<<<<<< HEAD +======= "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/core": "^3.18.7", @@ -1341,6 +1678,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.946.0", +>>>>>>> dev "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.7", @@ -1352,7 +1690,11 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { +======= "node_modules/@aws-sdk/nested-clients": { +>>>>>>> dev "version": "3.946.0", "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.946.0.tgz", "integrity": "sha512-rjAtEguukeW8mlyEQMQI56vxFoyWlaNwowmz1p1rav948SUjtrzjHAp4TOQWhibb7AR7BUTHBCgIcyCRjBEf4g==", @@ -1418,6 +1760,418 @@ "node": ">=18.0.0" } }, +<<<<<<< HEAD + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.946.0.tgz", + "integrity": "sha512-a2UwwvzbK5AxHKUBupfg4s7VnkqRAHjYsuezHnKCniczmT4HZfP1NnfwwvLKEH8qaTrwenxjKSfq4UWmWkvG+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.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/credential-provider-login/node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.922.0.tgz", + "integrity": "sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.922.0.tgz", + "integrity": "sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.922.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.922.0.tgz", + "integrity": "sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.922.0.tgz", + "integrity": "sha512-Dpr2YeOaLFqt3q1hocwBesynE3x8/dXZqXZRuzSX/9/VQcwYBFChHAm4mTAl4zuvArtDbLrwzWSxmOWYZGtq5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.922.0.tgz", + "integrity": "sha512-xmnLWMtmHJHJBupSWMUEW1gyxuRIeQ1Ov2xa8Tqq77fPr4Ft2AluEwiDMaZIMHoAvpxWKEEt9Si59Li7GIA+bQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.922.0.tgz", + "integrity": "sha512-G363np7YcJhf+gBucskdv8cOTbs2TRwocEzRupuqDIooGDlLBlfJrvwehdgtWR8l53yjJR3zcHvGrVPTe2h8Nw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.922.0.tgz", + "integrity": "sha512-T4iqd7WQ2DDjCH/0s50mnhdoX+IJns83ZE+3zj9IDlpU0N2aq8R91IG890qTfYkUEdP9yRm0xir/CNed+v6Dew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.922.0.tgz", + "integrity": "sha512-ygg8lME1oFAbsH42ed2wtGqfHLoT5irgx6VC4X98j79fV1qXEwwwbqMsAiMQ/HJehpjqAFRVsHox3MHLN48Z5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.922.0.tgz", + "integrity": "sha512-eHvSJZTSRJO+/tjjGD6ocnPc8q9o3m26+qbwQTu/4V6yOJQ1q+xkDZNqwJQphL+CodYaQ7uljp8g1Ji/AN3D9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.922.0.tgz", + "integrity": "sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.922.0.tgz", + "integrity": "sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, +======= +>>>>>>> dev "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.946.0", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.946.0.tgz", @@ -3291,6 +4045,10 @@ "cpu": [ "arm64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0", "optional": true, "os": [ @@ -3310,6 +4068,134 @@ "version": "0.34.4", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", +<<<<<<< HEAD + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", +======= +>>>>>>> dev "cpu": [ "x64" ], @@ -3376,6 +4262,8 @@ "url": "https://opencollective.com/libvips" } }, +<<<<<<< HEAD +======= "node_modules/@img/sharp-libvips-linux-arm64": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", @@ -3440,6 +4328,7 @@ "url": "https://opencollective.com/libvips" } }, +>>>>>>> dev "node_modules/@img/sharp-libvips-linuxmusl-arm64": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", @@ -3447,6 +4336,10 @@ "cpu": [ "arm64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3463,6 +4356,10 @@ "cpu": [ "x64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3479,6 +4376,10 @@ "cpu": [ "arm" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0", "optional": true, "os": [ @@ -3501,6 +4402,10 @@ "cpu": [ "arm64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0", "optional": true, "os": [ @@ -3523,6 +4428,10 @@ "cpu": [ "ppc64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0", "optional": true, "os": [ @@ -3545,6 +4454,10 @@ "cpu": [ "s390x" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0", "optional": true, "os": [ @@ -3589,6 +4502,10 @@ "cpu": [ "arm64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0", "optional": true, "os": [ @@ -3611,6 +4528,10 @@ "cpu": [ "x64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0", "optional": true, "os": [ @@ -3633,6 +4554,10 @@ "cpu": [ "wasm32" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -3652,6 +4577,10 @@ "cpu": [ "arm64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3671,6 +4600,10 @@ "cpu": [ "ia32" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3690,6 +4623,10 @@ "cpu": [ "x64" ], +<<<<<<< HEAD + "dev": true, +======= +>>>>>>> dev "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -7262,6 +8199,1050 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, +<<<<<<< HEAD + "node_modules/@react-email/preview-server": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-4.3.2.tgz", + "integrity": "sha512-rBm2AJhOhfi8Fd8MAFN4DQ0FQtsqq38JjJIWvbHA0EYwbjNwODmtzRZCkbdp+8o6GL5PKiRcikF0FDzbOYAJ+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.26.10", + "@babel/parser": "7.27.0", + "@babel/traverse": "7.27.0", + "@lottiefiles/dotlottie-react": "0.13.3", + "@radix-ui/colors": "3.0.0", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@types/node": "22.14.1", + "@types/normalize-path": "3.0.2", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@types/webpack": "5.28.5", + "autoprefixer": "10.4.21", + "clsx": "2.1.1", + "esbuild": "0.25.10", + "framer-motion": "12.23.22", + "json5": "2.2.3", + "log-symbols": "4.1.0", + "module-punycode": "npm:punycode@2.3.1", + "next": "15.5.2", + "node-html-parser": "7.0.1", + "ora": "5.4.1", + "pretty-bytes": "6.1.1", + "prism-react-renderer": "2.4.1", + "react": "19.0.0", + "react-dom": "19.0.0", + "sharp": "0.34.4", + "socket.io-client": "4.8.1", + "sonner": "2.0.3", + "source-map-js": "1.2.1", + "spamc": "0.0.5", + "stacktrace-parser": "0.1.11", + "tailwind-merge": "3.2.0", + "tailwindcss": "3.4.0", + "use-debounce": "10.0.4", + "zod": "3.24.3" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/aix-ppc64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ia32": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-loong64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-mips64el": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-s390x": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/openharmony-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-ia32": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/env": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", + "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-darwin-arm64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", + "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-darwin-x64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", + "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", + "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", + "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", + "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", + "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", + "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", + "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-email/preview-server/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@react-email/preview-server/node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@react-email/preview-server/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@react-email/preview-server/node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@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/@react-email/preview-server/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/@react-email/preview-server/node_modules/next": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", + "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/env": "15.5.2", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.2", + "@next/swc-darwin-x64": "15.5.2", + "@next/swc-linux-arm64-gnu": "15.5.2", + "@next/swc-linux-arm64-musl": "15.5.2", + "@next/swc-linux-x64-gnu": "15.5.2", + "@next/swc-linux-x64-musl": "15.5.2", + "@next/swc-win32-arm64-msvc": "15.5.2", + "@next/swc-win32-x64-msvc": "15.5.2", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/@react-email/preview-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@react-email/preview-server/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@react-email/preview-server/node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-email/preview-server/node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/@react-email/preview-server/node_modules/tailwindcss": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/@react-email/preview-server/node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/@react-email/preview-server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-email/preview-server/node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, +======= +>>>>>>> dev "node_modules/@react-email/render": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.0.tgz", @@ -9497,6 +11478,21 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", "optional": true +<<<<<<< HEAD + }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } +======= +>>>>>>> dev }, "node_modules/@types/ws": { "version": "8.18.1", @@ -9958,6 +11954,75 @@ "cpu": [ "x64" ], +<<<<<<< HEAD + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, +======= +>>>>>>> dev "license": "MIT", "optional": true, "os": [ @@ -10510,9 +12575,15 @@ } }, "node_modules/baseline-browser-mapping": { +<<<<<<< HEAD + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz", + "integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==", +======= "version": "2.9.4", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz", "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==", +>>>>>>> dev "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -14511,7 +16582,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-bigints": { @@ -15290,7 +17360,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -15384,6 +17453,15 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "license": "MIT" }, +<<<<<<< HEAD + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, +======= +>>>>>>> dev "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -15756,6 +17834,69 @@ "cpu": [ "x64" ], +<<<<<<< HEAD + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", +======= +>>>>>>> dev "dev": true, "license": "MPL-2.0", "optional": true, @@ -19392,6 +21533,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -21099,6 +23241,67 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, +<<<<<<< HEAD + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, +======= +>>>>>>> dev "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", @@ -22175,6 +24378,70 @@ "node": ">=6" } }, +<<<<<<< HEAD + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz", + "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, +======= +>>>>>>> dev "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -22913,7 +25180,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" diff --git a/package.json b/package.json index 2aebc439..267c78eb 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts", "db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts", "db:clear-migrations": "rm -rf server/migrations", - "set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json", - "set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json", - "set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", + "set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json", + "set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json", + "set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", "set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts", "set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts", "next:build": "next build", diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 9456effb..2ee34da6 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -6,28 +6,28 @@ import { withReplicas } from "drizzle-orm/pg-core"; function createDb() { const config = readConfigFile(); - if (!config.postgres) { - // check the environment variables for postgres config - if (process.env.POSTGRES_CONNECTION_STRING) { - config.postgres = { - connection_string: process.env.POSTGRES_CONNECTION_STRING - }; - if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { - const replicas = - process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split( - "," - ).map((conn) => ({ + // check the environment variables for postgres config first before the config file + if (process.env.POSTGRES_CONNECTION_STRING) { + config.postgres = { + connection_string: process.env.POSTGRES_CONNECTION_STRING + }; + if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { + const replicas = + process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map( + (conn) => ({ connection_string: conn.trim() - })); - config.postgres.replicas = replicas; - } - } else { - throw new Error( - "Postgres configuration is missing in the configuration file." - ); + }) + ); + config.postgres.replicas = replicas; } } + if (!config.postgres) { + throw new Error( + "Postgres configuration is missing in the configuration file." + ); + } + const connectionString = config.postgres?.connection_string; const replicaConnections = config.postgres?.replicas || []; diff --git a/server/db/pg/migrate.ts b/server/db/pg/migrate.ts index 8bbcceb7..2d2abca3 100644 --- a/server/db/pg/migrate.ts +++ b/server/db/pg/migrate.ts @@ -10,7 +10,7 @@ const runMigrations = async () => { await migrate(db as any, { migrationsFolder: migrationsFolder }); - console.log("Migrations completed successfully."); + console.log("Migrations completed successfully. ✅"); process.exit(0); } catch (error) { console.error("Error running migrations:", error); diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index cb809b71..1f30dbf5 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -204,6 +204,29 @@ export const loginPageOrg = pgTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = pgTable("loginPageBranding", { + loginPageBrandingId: serial("loginPageBrandingId").primaryKey(), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + primaryColor: text("primaryColor"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") +}); + +export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = pgTable("sessionTransferToken", { token: varchar("token").primaryKey(), sessionId: varchar("sessionId") @@ -283,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; export type AccessAuditLog = InferSelectModel; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 71877f2f..c689a35a 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -7,7 +7,8 @@ import { bigint, real, text, - index + index, + uniqueIndex } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; import { randomUUID } from "crypto"; @@ -213,7 +214,10 @@ export const siteResources = pgTable("siteResources", { destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode enabled: boolean("enabled").notNull().default(true), alias: varchar("alias"), - aliasAddress: varchar("aliasAddress") + aliasAddress: varchar("aliasAddress"), + tcpPortRangeString: varchar("tcpPortRangeString"), + udpPortRangeString: varchar("udpPortRangeString"), + disableIcmp: boolean("disableIcmp").notNull().default(false) }); export const clientSiteResources = pgTable("clientSiteResources", { diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 975a949b..af7d021d 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -1,13 +1,12 @@ -import { - sqliteTable, - integer, - text, - real, - index -} from "drizzle-orm/sqlite-core"; import { InferSelectModel } from "drizzle-orm"; -import { domains, orgs, targets, users, exitNodes, sessions } from "./schema"; -import { metadata } from "@app/app/[orgId]/settings/layout"; +import { + index, + integer, + real, + sqliteTable, + text +} from "drizzle-orm/sqlite-core"; +import { domains, exitNodes, orgs, sessions, users } from "./schema"; export const certificates = sqliteTable("certificates", { certId: integer("certId").primaryKey({ autoIncrement: true }), @@ -203,6 +202,31 @@ export const loginPageOrg = sqliteTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = sqliteTable("loginPageBranding", { + loginPageBrandingId: integer("loginPageBrandingId").primaryKey({ + autoIncrement: true + }), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + primaryColor: text("primaryColor"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") +}); + +export const loginPageBrandingOrg = sqliteTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = sqliteTable("sessionTransferToken", { token: text("token").primaryKey(), sessionId: text("sessionId") @@ -282,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; export type AccessAuditLog = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 6e17cac4..848289ee 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1,6 +1,12 @@ import { randomUUID } from "crypto"; import { InferSelectModel } from "drizzle-orm"; -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; +import { + sqliteTable, + text, + integer, + index, + uniqueIndex +} from "drizzle-orm/sqlite-core"; import { no } from "zod/v4/locales"; export const domains = sqliteTable("domains", { @@ -234,7 +240,10 @@ export const siteResources = sqliteTable("siteResources", { destination: text("destination").notNull(), // ip, cidr, hostname enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), alias: text("alias"), - aliasAddress: text("aliasAddress") + aliasAddress: text("aliasAddress"), + tcpPortRangeString: text("tcpPortRangeString"), + udpPortRangeString: text("udpPortRangeString"), + disableIcmp: integer("disableIcmp", { mode: "boolean" }) }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index c8a0b077..32a5fb47 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -10,6 +10,7 @@ export async function sendEmail( from: string | undefined; to: string | undefined; subject: string; + replyTo?: string; } ) { if (!emailClient) { @@ -32,6 +33,7 @@ export async function sendEmail( address: opts.from }, to: opts.to, + replyTo: opts.replyTo, subject: opts.subject, html: emailHtml }); diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts index ab65336d..ecbd8b27 100644 --- a/server/lib/blueprints/clientResources.ts +++ b/server/lib/blueprints/clientResources.ts @@ -90,7 +90,10 @@ export async function updateClientResources( destination: resourceData.destination, enabled: true, // hardcoded for now // enabled: resourceData.enabled ?? true, - alias: resourceData.alias || null + alias: resourceData.alias || null, + disableIcmp: resourceData["disable-icmp"], + tcpPortRangeString: resourceData["tcp-ports"], + udpPortRangeString: resourceData["udp-ports"] }) .where( eq( @@ -217,7 +220,10 @@ export async function updateClientResources( destination: resourceData.destination, enabled: true, // hardcoded for now // enabled: resourceData.enabled ?? true, - alias: resourceData.alias || null + alias: resourceData.alias || null, + disableIcmp: resourceData["disable-icmp"], + tcpPortRangeString: resourceData["tcp-ports"], + udpPortRangeString: resourceData["udp-ports"] }) .returning(); diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 23e2176f..0f0edded 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { portRangeStringSchema } from "@server/lib/ip"; export const SiteSchema = z.object({ name: z.string().min(1).max(100), @@ -222,6 +223,9 @@ export const ClientResourceSchema = z // destinationPort: z.int().positive().optional(), destination: z.string().min(1), // enabled: z.boolean().default(true), + "tcp-ports": portRangeStringSchema.optional().default("*"), + "udp-ports": portRangeStringSchema.optional().default("*"), + "disable-icmp": z.boolean().optional().default(false), alias: z .string() .regex( diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 9c412801..21c148ac 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,10 +1,4 @@ -import { - clientSitesAssociationsCache, - db, - SiteResource, - siteResources, - Transaction -} from "@server/db"; +import { db, SiteResource, siteResources, Transaction } from "@server/db"; import { clients, orgs, sites } from "@server/db"; import { and, eq, isNotNull } from "drizzle-orm"; import config from "@server/lib/config"; @@ -472,10 +466,12 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] { export type SubnetProxyTarget = { sourcePrefix: string; // must be a cidr destPrefix: string; // must be a cidr + disableIcmp?: boolean; rewriteTo?: string; // must be a cidr portRange?: { min: number; max: number; + protocol: "tcp" | "udp"; }[]; }; @@ -505,6 +501,11 @@ export function generateSubnetProxyTargets( } const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; + const portRange = [ + ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), + ...parsePortRangeString(siteResource.udpPortRangeString, "udp") + ]; + const disableIcmp = siteResource.disableIcmp ?? false; if (siteResource.mode == "host") { let destination = siteResource.destination; @@ -515,7 +516,9 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, - destPrefix: destination + destPrefix: destination, + portRange, + disableIcmp }); } @@ -524,13 +527,17 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, destPrefix: `${siteResource.aliasAddress}/32`, - rewriteTo: destination + rewriteTo: destination, + portRange, + disableIcmp }); } } else if (siteResource.mode == "cidr") { targets.push({ sourcePrefix: clientPrefix, - destPrefix: siteResource.destination + destPrefix: siteResource.destination, + portRange, + disableIcmp }); } } @@ -542,3 +549,117 @@ export function generateSubnetProxyTargets( return targets; } + +// Custom schema for validating port range strings +// Format: "80,443,8000-9000" or "*" for all ports, or empty string +export const portRangeStringSchema = z + .string() + .optional() + .refine( + (val) => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + // Split by comma and validate each part + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; // empty parts not allowed + } + + // Check if it's a range (contains dash) + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + + // Both parts must be present + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + // Must be valid numbers + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + // Must be valid port range (1-65535) + if ( + startPort < 1 || + startPort > 65535 || + endPort < 1 || + endPort > 65535 + ) { + return false; + } + + // Start must be <= end + if (startPort > endPort) { + return false; + } + } else { + // Single port + const port = parseInt(part, 10); + + // Must be a valid number + if (isNaN(port)) { + return false; + } + + // Must be valid port range (1-65535) + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; + }, + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.' + } + ); + +/** + * Parses a port range string into an array of port range objects + * @param portRangeStr - Port range string (e.g., "80,443,8000-9000", "*", or "") + * @param protocol - Protocol to use for all ranges (default: "tcp") + * @returns Array of port range objects with min, max, and protocol fields + */ +export function parsePortRangeString( + portRangeStr: string | undefined | null, + protocol: "tcp" | "udp" = "tcp" +): { min: number; max: number; protocol: "tcp" | "udp" }[] { + // Handle undefined or empty string - insert dummy value with port 0 + if (!portRangeStr || portRangeStr.trim() === "") { + return [{ min: 0, max: 0, protocol }]; + } + + // Handle wildcard - return empty array (all ports allowed) + if (portRangeStr.trim() === "*") { + return []; + } + + const result: { min: number; max: number; protocol: "tcp" | "udp" }[] = []; + const parts = portRangeStr.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part.includes("-")) { + // Range + const [start, end] = part.split("-").map((p) => p.trim()); + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + result.push({ min: startPort, max: endPort, protocol }); + } else { + // Single port + const port = parseInt(part, 10); + result.push({ min: port, max: port, protocol }); + } + } + + return result; +} diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 568f2b35..d9608e21 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -311,6 +311,33 @@ authenticated.get( loginPage.getLoginPage ); +authenticated.get( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getLoginPage), + logActionAudit(ActionsEnum.getLoginPage), + loginPage.getLoginPageBranding +); + +authenticated.put( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateLoginPage), + logActionAudit(ActionsEnum.updateLoginPage), + loginPage.upsertLoginPageBranding +); + +authenticated.delete( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.deleteLoginPage), + logActionAudit(ActionsEnum.deleteLoginPage), + loginPage.deleteLoginPageBranding +); + authRouter.post( "/remoteExitNode/get-token", verifyValidLicense, diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts index b393b884..49596a1f 100644 --- a/server/private/routers/internal.ts +++ b/server/private/routers/internal.ts @@ -28,6 +28,7 @@ internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps); internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier); internalRouter.get("/login-page", loginPage.loadLoginPage); +internalRouter.get("/login-page-branding", loginPage.loadLoginPageBranding); internalRouter.post( "/get-session-transfer-token", diff --git a/server/private/routers/loginPage/deleteLoginPageBranding.ts b/server/private/routers/loginPage/deleteLoginPageBranding.ts new file mode 100644 index 00000000..1fb243b0 --- /dev/null +++ b/server/private/routers/loginPage/deleteLoginPageBranding.ts @@ -0,0 +1,113 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function deleteLoginPageBranding( + 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 { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + await db + .delete(loginPageBranding) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ); + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/getLoginPageBranding.ts b/server/private/routers/loginPage/getLoginPageBranding.ts new file mode 100644 index 00000000..262e9ce8 --- /dev/null +++ b/server/private/routers/loginPage/getLoginPageBranding.ts @@ -0,0 +1,103 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function getLoginPageBranding( + 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 { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/index.ts b/server/private/routers/loginPage/index.ts index 2372ddfa..1bfe6e16 100644 --- a/server/private/routers/loginPage/index.ts +++ b/server/private/routers/loginPage/index.ts @@ -17,3 +17,7 @@ export * from "./getLoginPage"; export * from "./loadLoginPage"; export * from "./updateLoginPage"; export * from "./deleteLoginPage"; +export * from "./upsertLoginPageBranding"; +export * from "./deleteLoginPageBranding"; +export * from "./getLoginPageBranding"; +export * from "./loadLoginPageBranding"; diff --git a/server/private/routers/loginPage/loadLoginPageBranding.ts b/server/private/routers/loginPage/loadLoginPageBranding.ts new file mode 100644 index 00000000..823f75a6 --- /dev/null +++ b/server/private/routers/loginPage/loadLoginPageBranding.ts @@ -0,0 +1,100 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, loginPageBranding, loginPageBrandingOrg, orgs } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import type { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types"; + +const querySchema = z.object({ + orgId: z.string().min(1) +}); + +async function query(orgId: string) { + const [orgLink] = await db + .select() + .from(loginPageBrandingOrg) + .where(eq(loginPageBrandingOrg.orgId, orgId)) + .innerJoin(orgs, eq(loginPageBrandingOrg.orgId, orgs.orgId)); + if (!orgLink) { + return null; + } + + const [res] = await db + .select() + .from(loginPageBranding) + .where( + and( + eq( + loginPageBranding.loginPageBrandingId, + orgLink.loginPageBrandingOrg.loginPageBrandingId + ) + ) + ) + .limit(1); + return { + ...res, + orgId: orgLink.orgs.orgId, + orgName: orgLink.orgs.name + }; +} + +export async function loadLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { orgId } = parsedQuery.data; + + const branding = await query(orgId); + + if (!branding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Branding for Login page not found" + ) + ); + } + + return response(res, { + data: branding, + success: true, + error: false, + message: "Login page branding retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts new file mode 100644 index 00000000..f9f9d08c --- /dev/null +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -0,0 +1,162 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { fromError } from "zod-validation-error"; +import { eq, InferInsertModel } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z.strictObject({ + orgId: z.string() +}); + +const bodySchema = z.strictObject({ + logoUrl: z.url(), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional(), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() +}); + +export type UpdateLoginPageBrandingBody = z.infer; + +export async function upsertLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + let updateData = parsedBody.data satisfies InferInsertModel< + typeof loginPageBranding + >; + + if (build !== "saas") { + // org branding settings are only considered in the saas build + const { orgTitle, orgSubtitle, ...rest } = updateData; + updateData = rest; + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + let updatedLoginPageBranding: LoginPageBranding; + + if (existingLoginPageBranding) { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .update(loginPageBranding) + .set({ ...updateData }) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ) + .returning(); + return branding; + }); + } else { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .insert(loginPageBranding) + .values({ ...updateData }) + .returning(); + + await tx.insert(loginPageBrandingOrg).values({ + loginPageBrandingId: branding.loginPageBrandingId, + orgId: orgId + }); + return branding; + }); + } + + return response(res, { + data: updatedLoginPageBranding, + success: true, + error: false, + message: existingLoginPageBranding + ? "Login page branding updated successfully" + : "Login page branding created successfully", + status: existingLoginPageBranding ? HttpCode.OK : HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/misc/sendSupportEmail.ts b/server/private/routers/misc/sendSupportEmail.ts index 404a2501..cd37560d 100644 --- a/server/private/routers/misc/sendSupportEmail.ts +++ b/server/private/routers/misc/sendSupportEmail.ts @@ -66,6 +66,7 @@ export async function sendSupportEmail( { name: req.user?.email || "Support User", to: "support@pangolin.net", + replyTo: req.user?.email || undefined, from: config.getNoReplyEmail(), subject: `Support Request: ${subject}` } diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index b7b91925..653a2578 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -4,21 +4,48 @@ import { Alias, SubnetProxyTarget } from "@server/lib/ip"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; +const BATCH_SIZE = 50; +const BATCH_DELAY_MS = 50; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function chunkArray(array: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +} + export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { - await sendToClient(newtId, { - type: `newt/wg/targets/add`, - data: targets - }); + const batches = chunkArray(targets, BATCH_SIZE); + for (let i = 0; i < batches.length; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/add`, + data: batches[i] + }); + } } export async function removeTargets( newtId: string, targets: SubnetProxyTarget[] ) { - await sendToClient(newtId, { - type: `newt/wg/targets/remove`, - data: targets - }); + const batches = chunkArray(targets, BATCH_SIZE); + for (let i = 0; i < batches.length; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/remove`, + data: batches[i] + }); + } } export async function updateTargets( @@ -28,12 +55,24 @@ export async function updateTargets( newTargets: SubnetProxyTarget[]; } ) { - await sendToClient(newtId, { - type: `newt/wg/targets/update`, - data: targets - }).catch((error) => { - logger.warn(`Error sending message:`, error); - }); + const oldBatches = chunkArray(targets.oldTargets, BATCH_SIZE); + const newBatches = chunkArray(targets.newTargets, BATCH_SIZE); + const maxBatches = Math.max(oldBatches.length, newBatches.length); + + for (let i = 0; i < maxBatches; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/update`, + data: { + oldTargets: oldBatches[i] || [], + newTargets: newBatches[i] || [] + } + }).catch((error) => { + logger.warn(`Error sending message:`, error); + }); + } } export async function addPeerData( diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index f6b21ff6..bd633707 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -192,11 +192,71 @@ export async function validateOidcCallback( state }); - const tokens = await client.validateAuthorizationCode( - ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), - code, - codeVerifier - ); + let tokens: arctic.OAuth2Tokens; + try { + tokens = await client.validateAuthorizationCode( + ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), + code, + codeVerifier + ); + } catch (err: unknown) { + if (err instanceof arctic.OAuth2RequestError) { + logger.warn("OIDC provider rejected the authorization code", { + error: err.code, + description: err.description, + uri: err.uri, + state: err.state + }); + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + err.description || + `OIDC provider rejected the request (${err.code})` + ) + ); + } + + if (err instanceof arctic.UnexpectedResponseError) { + logger.error( + "OIDC provider returned an unexpected response during token exchange", + { status: err.status } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Received an unexpected response from the identity provider while exchanging the authorization code." + ) + ); + } + + if (err instanceof arctic.UnexpectedErrorResponseBodyError) { + logger.error( + "OIDC provider returned an unexpected error payload during token exchange", + { status: err.status, data: err.data } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Identity provider returned an unexpected error payload while exchanging the authorization code." + ) + ); + } + + if (err instanceof arctic.ArcticFetchError) { + logger.error( + "Failed to reach OIDC provider while exchanging authorization code", + { error: err.message } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Unable to reach the identity provider while exchanging the authorization code. Please try again." + ) + ); + } + + throw err; + } const idToken = tokens.idToken(); logger.debug("ID token", { idToken }); diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 878d61fa..6301bb6d 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -352,6 +352,14 @@ authenticated.post( user.inviteUser ); +authenticated.delete( + "/org/:orgId/invitations/:inviteId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.removeInvitation), + logActionAudit(ActionsEnum.removeInvitation), + user.removeInvitation +); + authenticated.get( "/resource/:resourceId/roles", verifyApiKeyResourceAccess, diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts index a68dd7d4..8a253d07 100644 --- a/server/routers/loginPage/types.ts +++ b/server/routers/loginPage/types.ts @@ -1,4 +1,4 @@ -import { LoginPage } from "@server/db"; +import type { LoginPage, LoginPageBranding } from "@server/db"; export type CreateLoginPageResponse = LoginPage; @@ -9,3 +9,10 @@ export type GetLoginPageResponse = LoginPage; export type UpdateLoginPageResponse = LoginPage; export type LoadLoginPageResponse = LoginPage & { orgId: string }; + +export type LoadLoginPageBrandingResponse = LoginPageBranding & { + orgId: string; + orgName: string; +}; + +export type GetLoginPageBrandingResponse = LoginPageBranding; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 77e49a20..c7f2131e 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -346,6 +346,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { type: "newt/wg/connect", data: { endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`, + relayPort: config.getRawConfig().gerbil.clients_start_port, publicKey: exitNode.publicKey, serverIP: exitNode.address.split("/")[0], tunnelIP: siteSubnet.split("/")[0], diff --git a/server/routers/olm/getOlmToken.ts b/server/routers/olm/getOlmToken.ts index 3852b00e..b6dc8148 100644 --- a/server/routers/olm/getOlmToken.ts +++ b/server/routers/olm/getOlmToken.ts @@ -197,6 +197,7 @@ export async function getOlmToken( const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => { return { publicKey: exitNode.publicKey, + relayPort: config.getRawConfig().gerbil.clients_start_port, endpoint: exitNode.endpoint }; }); diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index 595b35ba..88886cd1 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -4,6 +4,7 @@ import { clients, clientSitesAssociationsCache, Olm } from "@server/db"; import { and, eq } from "drizzle-orm"; import { updatePeer as newtUpdatePeer } from "../newt/peers"; import logger from "@server/logger"; +import config from "@server/lib/config"; export const handleOlmRelayMessage: MessageHandler = async (context) => { const { message, client: c, sendToClient } = context; @@ -88,7 +89,8 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { type: "olm/wg/peer/relay", data: { siteId: siteId, - relayEndpoint: exitNode.endpoint + relayEndpoint: exitNode.endpoint, + relayPort: config.getRawConfig().gerbil.clients_start_port } }, broadcast: false, diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 4aa8edd7..e164b257 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -1,5 +1,6 @@ import { sendToClient } from "#dynamic/routers/ws"; import { db, olms } from "@server/db"; +import config from "@server/lib/config"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { Alias } from "yaml"; @@ -156,6 +157,7 @@ export async function initPeerAddHandshake( siteId: peer.siteId, exitNode: { publicKey: peer.exitNode.publicKey, + relayPort: config.getRawConfig().gerbil.clients_start_port, endpoint: peer.exitNode.endpoint } } diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index fe0a38c8..61479a4d 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -89,7 +89,6 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) - .leftJoin( resourceHeaderAuth, eq( diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 37ca8fe4..e7a3bb37 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -1,5 +1,6 @@ import { db, exitNodes, newts } from "@server/db"; import { orgs, roleSites, sites, userSites } from "@server/db"; +import { remoteExitNodes } from "@server/db"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -104,12 +105,17 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { newtVersion: newts.version, exitNodeId: sites.exitNodeId, exitNodeName: exitNodes.name, - exitNodeEndpoint: exitNodes.endpoint + exitNodeEndpoint: exitNodes.endpoint, + remoteExitNodeId: remoteExitNodes.remoteExitNodeId }) .from(sites) .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .leftJoin(newts, eq(newts.siteId, sites.siteId)) .leftJoin(exitNodes, eq(exitNodes.exitNodeId, sites.exitNodeId)) + .leftJoin( + remoteExitNodes, + eq(remoteExitNodes.exitNodeId, sites.exitNodeId) + ) .where( and( inArray(sites.siteId, accessibleSiteIds), diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index e9ce8e04..c103b09e 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -10,7 +10,7 @@ import { userSiteResources } from "@server/db"; import { getUniqueSiteResourceName } from "@server/db/names"; -import { getNextAvailableAliasAddress } from "@server/lib/ip"; +import { getNextAvailableAliasAddress, portRangeStringSchema } from "@server/lib/ip"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; import response from "@server/lib/response"; import logger from "@server/logger"; @@ -39,13 +39,16 @@ const createSiteResourceSchema = z alias: z .string() .regex( - /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, - "Alias must be a fully qualified domain name (e.g., example.com)" + /^(?:[a-zA-Z0-9*?](?:[a-zA-Z0-9*?-]{0,61}[a-zA-Z0-9*?])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name with optional wildcards (e.g., example.com, *.example.com, host-0?.example.internal)" ) .optional(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional() }) .strict() .refine( @@ -65,7 +68,7 @@ const createSiteResourceSchema = z const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; const isValidDomain = domainRegex.test(data.destination); - const isValidAlias = data.alias && domainRegex.test(data.alias); + const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== ""; return isValidDomain && isValidAlias; // require the alias to be set in the case of domain } @@ -154,7 +157,10 @@ export async function createSiteResource( alias, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString, + disableIcmp } = parsedBody.data; // Verify the site exists and belongs to the org @@ -239,7 +245,10 @@ export async function createSiteResource( destination, enabled, alias, - aliasAddress + aliasAddress, + tcpPortRangeString, + udpPortRangeString, + disableIcmp }) .returning(); diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index f6975cd2..7b2e0233 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -97,6 +97,9 @@ export async function listAllSiteResourcesByOrg( destination: siteResources.destination, enabled: siteResources.enabled, alias: siteResources.alias, + tcpPortRangeString: siteResources.tcpPortRangeString, + udpPortRangeString: siteResources.udpPortRangeString, + disableIcmp: siteResources.disableIcmp, siteName: sites.name, siteNiceId: sites.niceId, siteAddress: sites.address diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 92704adb..c3360e6f 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -23,7 +23,8 @@ import { updatePeerData, updateTargets } from "@server/routers/client/targets"; import { generateAliasConfig, generateRemoteSubnets, - generateSubnetProxyTargets + generateSubnetProxyTargets, + portRangeStringSchema } from "@server/lib/ip"; import { getClientSiteResourceAccess, @@ -49,13 +50,16 @@ const updateSiteResourceSchema = z alias: z .string() .regex( - /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, - "Alias must be a fully qualified domain name (e.g., example.internal)" + /^(?:[a-zA-Z0-9*?](?:[a-zA-Z0-9*?-]{0,61}[a-zA-Z0-9*?])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name with optional wildcards (e.g., example.internal, *.example.internal, host-0?.example.internal)" ) .nullish(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional() }) .strict() .refine( @@ -74,7 +78,7 @@ const updateSiteResourceSchema = z const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; const isValidDomain = domainRegex.test(data.destination); - const isValidAlias = data.alias && domainRegex.test(data.alias); + const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== ""; return isValidDomain && isValidAlias; // require the alias to be set in the case of domain } @@ -160,7 +164,10 @@ export async function updateSiteResource( enabled, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString, + disableIcmp } = parsedBody.data; const [site] = await db @@ -226,7 +233,10 @@ export async function updateSiteResource( mode: mode, destination: destination, enabled: enabled, - alias: alias && alias.trim() ? alias : null + alias: alias && alias.trim() ? alias : null, + tcpPortRangeString: tcpPortRangeString, + udpPortRangeString: udpPortRangeString, + disableIcmp: disableIcmp }) .where( and( @@ -348,10 +358,18 @@ export async function handleMessagingForUpdatedSiteResource( const aliasChanged = existingSiteResource && existingSiteResource.alias !== updatedSiteResource.alias; + const portRangesChanged = + existingSiteResource && + (existingSiteResource.tcpPortRangeString !== + updatedSiteResource.tcpPortRangeString || + existingSiteResource.udpPortRangeString !== + updatedSiteResource.udpPortRangeString || + existingSiteResource.disableIcmp !== + updatedSiteResource.disableIcmp); // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all - if (destinationChanged || aliasChanged) { + if (destinationChanged || aliasChanged || portRangesChanged) { const [newt] = await trx .select() .from(newts) @@ -365,7 +383,7 @@ export async function handleMessagingForUpdatedSiteResource( } // Only update targets on newt if destination changed - if (destinationChanged) { + if (destinationChanged || portRangesChanged) { const oldTargets = generateSubnetProxyTargets( existingSiteResource, mergedAllClients diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index 6a000afc..ab6a96d2 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -8,12 +8,24 @@ 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 removeInvitationParamsSchema = z.strictObject({ orgId: z.string(), inviteId: z.string() }); +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/invitations/{inviteId}", + description: "Remove an open invitation from an organization", + tags: [OpenAPITags.Org], + request: { + params: removeInvitationParamsSchema + }, + responses: {} +}); + export async function removeInvitation( req: Request, res: Response, diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 64298029..87b86321 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -16,11 +16,23 @@ function generateToken(): string { return generateRandomString(random, alphabet, 32); } +function validateToken(token: string): boolean { + const tokenRegex = /^[a-z0-9]{32}$/; + return tokenRegex.test(token); +} + function generateId(length: number): string { const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; return generateRandomString(random, alphabet, length); } +function showSetupToken(token: string, source: string): void { + console.log(`=== SETUP TOKEN ${source} ===`); + console.log("Token:", token); + console.log("Use this token on the initial setup page"); + console.log("================================"); +} + export async function ensureSetupToken() { try { // Check if a server admin already exists @@ -38,17 +50,48 @@ export async function ensureSetupToken() { } // Check if a setup token already exists - const existingTokens = await db + const [existingToken] = await db .select() .from(setupTokens) .where(eq(setupTokens.used, false)); + const envSetupToken = process.env.PANGOLIN_SETUP_TOKEN; + console.debug("PANGOLIN_SETUP_TOKEN:", envSetupToken); + if (envSetupToken) { + if (!validateToken(envSetupToken)) { + throw new Error( + "invalid token format for PANGOLIN_SETUP_TOKEN" + ); + } + + if (existingToken?.token !== envSetupToken) { + console.warn( + "Overwriting existing token in DB since PANGOLIN_SETUP_TOKEN is set" + ); + + await db + .update(setupTokens) + .set({ token: envSetupToken }) + .where(eq(setupTokens.tokenId, existingToken.tokenId)); + } else { + const tokenId = generateId(15); + + await db.insert(setupTokens).values({ + tokenId: tokenId, + token: envSetupToken, + used: false, + dateCreated: moment().toISOString(), + dateUsed: null + }); + } + + showSetupToken(envSetupToken, "FROM ENVIRONMENT"); + return; + } + // If unused token exists, display it instead of creating a new one - if (existingTokens.length > 0) { - console.log("=== SETUP TOKEN EXISTS ==="); - console.log("Token:", existingTokens[0].token); - console.log("Use this token on the initial setup page"); - console.log("================================"); + if (existingToken) { + showSetupToken(existingToken.token, "EXISTS"); return; } @@ -64,10 +107,7 @@ export async function ensureSetupToken() { dateUsed: null }); - console.log("=== SETUP TOKEN GENERATED ==="); - console.log("Token:", token); - console.log("Use this token on the initial setup page"); - console.log("================================"); + showSetupToken(token, "GENERATED"); } catch (error) { console.error("Failed to ensure setup token:", error); throw error; diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index e52f19ed..c4048bcc 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -1,16 +1,11 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; import { getTranslations } from "next-intl/server"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; type BillingSettingsProps = { children: React.ReactNode; @@ -23,8 +18,7 @@ export default async function BillingSettingsPage({ }: BillingSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +26,7 @@ export default async function BillingSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +34,7 @@ export default async function BillingSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx index 7cdea07a..6cdbf23c 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx @@ -3,7 +3,7 @@ import { GetIdpResponse as GetOrgIdpResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; @@ -28,7 +28,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect(`/${params.orgId}/settings/idp`); } - const navItems: HorizontalTabs = [ + const navItems: TabItem[] = [ { title: t("general"), href: `/${params.orgId}/settings/idp/${params.idpId}/general` diff --git a/src/app/[orgId]/settings/(private)/idp/create/page.tsx b/src/app/[orgId]/settings/(private)/idp/create/page.tsx index a899a2aa..682b23f1 100644 --- a/src/app/[orgId]/settings/(private)/idp/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/create/page.tsx @@ -331,29 +331,24 @@ export default function Page() { - - - - - - {t("idpType")} - - - {t("idpTypeDescription")} - - - - { - handleProviderChange( - value as "oidc" | "google" | "azure" - ); - }} - cols={3} - /> +
+
+ + {t("idpType")} + +
+ { + handleProviderChange( + value as "oidc" | "google" | "azure" + ); + }} + cols={3} + /> +
diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx index 4f66b7f8..77707e16 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx @@ -26,7 +26,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { build } from "@server/build"; -import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; import { InfoSection, InfoSectionContent, @@ -36,6 +35,7 @@ import { import CopyToClipboard from "@app/components/CopyToClipboard"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -131,19 +131,19 @@ export default function CredentialsPage() { - {t("generatedcredentials")} + {t("credentials")} - {t("regenerateCredentials")} + {t("remoteNodeCredentialsDescription")} - + - {t("endpoint") || "Endpoint"} + {t("endpoint")} - {t("remoteExitNodeId") || - "Remote Exit Node ID"} + {t("remoteExitNodeId")} {displayRemoteExitNodeId ? ( @@ -168,7 +167,7 @@ export default function CredentialsPage() { - {t("secretKey") || "Secret Key"} + {t("remoteExitNodeSecretKey")} {displaySecret ? ( diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx index 98af49a6..ec1dea83 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx @@ -43,7 +43,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <> diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx index 632dc0ad..2da0e0da 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx @@ -2,7 +2,9 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { AxiosResponse } from "axios"; -import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable"; +import ExitNodesTable, { + RemoteExitNodeRow +} from "@app/components/ExitNodesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx index e75aa3eb..d212ab4a 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx @@ -22,7 +22,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { build } from "@server/build"; -import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; import { InfoSection, InfoSectionContent, @@ -32,6 +31,7 @@ import { import CopyToClipboard from "@app/components/CopyToClipboard"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -127,7 +127,7 @@ export default function CredentialsPage() { - + diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx new file mode 100644 index 00000000..0944c9f7 --- /dev/null +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -0,0 +1,56 @@ +import AuthPageBrandingForm from "@app/components/AuthPageBrandingForm"; +import AuthPageSettings from "@app/components/private/AuthPageSettings"; +import { SettingsContainer } from "@app/components/Settings"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { build } from "@server/build"; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; +import { + GetLoginPageBrandingResponse, + GetLoginPageResponse +} from "@server/routers/loginPage/types"; +import { AxiosResponse } from "axios"; + +export interface AuthPageProps { + params: Promise<{ orgId: string }>; +} + +export default async function AuthPage(props: AuthPageProps) { + const orgId = (await props.params).orgId; + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + + let loginPage: GetLoginPageResponse | null = null; + try { + if (build === "saas") { + const res = await internal.get>( + `/org/${orgId}/login-page`, + await authCookieHeader() + ); + if (res.status === 200) { + loginPage = res.data.data; + } + } + } catch (error) {} + + let loginPageBranding: GetLoginPageBrandingResponse | null = null; + try { + const res = await internal.get< + AxiosResponse + >(`/org/${orgId}/login-page-branding`, await authCookieHeader()); + if (res.status === 200) { + loginPageBranding = res.data.data; + } + } catch (error) {} + + return ( + + {build === "saas" && } + + + ); +} diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 8c8efa59..273d4b7e 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -1,16 +1,14 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; + import { redirect } from "next/navigation"; -import { cache } from "react"; import { getTranslations } from "next-intl/server"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { build } from "@server/build"; type GeneralSettingsProps = { children: React.ReactNode; @@ -23,8 +21,7 @@ export default async function GeneralSettingsPage({ }: GeneralSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +29,7 @@ export default async function GeneralSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +37,7 @@ export default async function GeneralSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); @@ -60,12 +45,19 @@ export default async function GeneralSettingsPage({ const t = await getTranslations(); - const navItems = [ + const navItems: TabItem[] = [ { title: t("general"), - href: `/{orgId}/settings/general` + href: `/{orgId}/settings/general`, + exact: true } ]; + if (build !== "oss") { + navItems.push({ + title: t("authPage"), + href: `/{orgId}/settings/general/auth-page` + }); + } return ( <> diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 6bfb3013..9134ae9e 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -43,14 +43,13 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionForm, - SettingsSectionFooter + SettingsSectionForm } from "@app/components/Settings"; import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { SwitchInput } from "@app/components/SwitchInput"; -import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; @@ -113,29 +112,18 @@ const LOG_RETENTION_OPTIONS = [ export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const { orgUser } = userOrgUserContext(); const router = useRouter(); const { org } = useOrgContext(); const api = createApiClient(useEnvContext()); const { user } = useUserContext(); const t = useTranslations(); const { env } = useEnvContext(); - const { licenseStatus, isUnlocked } = useLicenseStatusContext(); - const subscription = useSubscriptionStatusContext(); - - // Check if security features are disabled due to licensing/subscription - const isSecurityFeatureDisabled = () => { - const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); - const isSaasNotSubscribed = - build === "saas" && !subscription?.isSubscribed(); - return isEnterpriseNotLicensed || isSaasNotSubscribed; - }; + const { isPaidUser, hasSaasSubscription } = usePaidStatus(); const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] = useState(false); - const authPageSettingsRef = useRef(null); const form = useForm({ resolver: zodResolver(GeneralFormSchema), @@ -258,14 +246,6 @@ export default function GeneralPage() { // Update organization await api.post(`/org/${org?.org.orgId}`, reqData); - // Also save auth page settings if they have unsaved changes - if ( - build === "saas" && - authPageSettingsRef.current?.hasUnsavedChanges() - ) { - await authPageSettingsRef.current.saveAuthSettings(); - } - toast({ title: t("orgUpdated"), description: t("orgUpdatedDescription") @@ -410,9 +390,7 @@ export default function GeneralPage() { {LOG_RETENTION_OPTIONS.filter( (option) => { if ( - build == - "saas" && - !subscription?.subscribed && + hasSaasSubscription && option.value > 30 ) { @@ -440,19 +418,15 @@ export default function GeneralPage() { )} /> - {build != "oss" && ( + {build !== "oss" && ( <> - + { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); + const isDisabled = !isPaidUser; return ( @@ -518,11 +492,7 @@ export default function GeneralPage() { control={form.control} name="settingsLogRetentionDaysAction" render={({ field }) => { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); + const isDisabled = !isPaidUser; return ( @@ -590,8 +560,7 @@ export default function GeneralPage() { {build !== "oss" && ( - <> -
+ {t("securitySettings")} @@ -601,14 +570,13 @@ export default function GeneralPage() { - - + + { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -655,8 +623,7 @@ export default function GeneralPage() { control={form.control} name="maxSessionLengthHours" render={({ field }) => { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -744,8 +711,7 @@ export default function GeneralPage() { control={form.control} name="passwordExpiryDays" render={({ field }) => { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -831,7 +797,7 @@ export default function GeneralPage() { /> - + )}
@@ -848,8 +814,6 @@ export default function GeneralPage() { - {build === "saas" && } - {build !== "saas" && ( diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx index 49ccb97f..eacab1d2 100644 --- a/src/app/[orgId]/settings/resources/client/page.tsx +++ b/src/app/[orgId]/settings/resources/client/page.tsx @@ -67,7 +67,10 @@ export default async function ClientResourcesPage( // destinationPort: siteResource.destinationPort, alias: siteResource.alias || null, siteNiceId: siteResource.siteNiceId, - niceId: siteResource.niceId + niceId: siteResource.niceId, + tcpPortRangeString: siteResource.tcpPortRangeString || null, + udpPortRangeString: siteResource.udpPortRangeString || null, + disableIcmp: siteResource.disableIcmp || false, }; } ); diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 865f50e8..547c8f6a 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -12,10 +12,6 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { useResourceContext } from "@app/hooks/useResourceContext"; -import { formatAxiosError } from "@app/lib/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - import { Credenza, CredenzaBody, @@ -41,7 +37,7 @@ import { SwitchInput } from "@app/components/SwitchInput"; import { Label } from "@app/components/ui/label"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; -import { createApiClient } from "@app/lib/api"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { UpdateResourceResponse } from "@server/routers/resource"; import { AxiosResponse } from "axios"; @@ -51,6 +47,8 @@ import { useParams, useRouter } from "next/navigation"; import { toASCII, toUnicode } from "punycode"; import { useActionState, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import z from "zod"; export default function GeneralForm() { const params = useParams(); @@ -69,28 +67,14 @@ export default function GeneralForm() { `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}` ); - console.log({ resource }); - - const [defaultSubdomain, defaultBaseDomain] = useMemo(() => { - const resourceUrl = new URL(resourceFullDomain); - const domain = resourceUrl.hostname; - - const allDomainParts = domain.split("."); - let sub = undefined; - let base = domain; - - if (allDomainParts.length >= 3) { - // 3 parts: [subdomain, domain, tld] - const [first, ...rest] = allDomainParts; - sub = first; - base = rest.join("."); - } - - return [sub, base]; + const resourceFullDomainName = useMemo(() => { + const url = new URL(resourceFullDomain); + return url.hostname; }, [resourceFullDomain]); const [selectedDomain, setSelectedDomain] = useState<{ domainId: string; + domainNamespaceId?: string; subdomain?: string; fullDomain: string; baseDomain: string; @@ -177,7 +161,11 @@ export default function GeneralForm() { niceId: data.niceId, subdomain: data.subdomain, fullDomain: updated.fullDomain, - proxyPort: data.proxyPort + proxyPort: data.proxyPort, + domainId: data.domainId + // ...(!resource.http && { + // enableProxy: data.enableProxy + // }) }); toast({ @@ -359,9 +347,6 @@ export default function GeneralForm() { - {r.type !== "internal" && ( + {r.type === "internal" && ( { generatePasswordResetCode(r.id); diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx new file mode 100644 index 00000000..67e2232f --- /dev/null +++ b/src/components/AuthPageBrandingForm.tsx @@ -0,0 +1,432 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useActionState, useState } from "react"; +import { useForm } from "react-hook-form"; +import z from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "./Settings"; +import { useTranslations } from "next-intl"; + +import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; +import { Input } from "./ui/input"; +import { ExternalLink, InfoIcon, XIcon } from "lucide-react"; +import { Button } from "./ui/button"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useRouter } from "next/navigation"; +import { toast } from "@app/hooks/useToast"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { build } from "@server/build"; +import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; +import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; + +export type AuthPageCustomizationProps = { + orgId: string; + branding: GetLoginPageBrandingResponse | null; +}; + +const AuthPageFormSchema = z.object({ + logoUrl: z.url().refine( + async (url) => { + try { + const response = await fetch(url); + return ( + response.status === 200 && + (response.headers.get("content-type") ?? "").startsWith( + "image/" + ) + ); + } catch (error) { + return false; + } + }, + { + error: "Invalid logo URL, must be a valid image URL" + } + ), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() +}); + +export default function AuthPageBrandingForm({ + orgId, + branding +}: AuthPageCustomizationProps) { + const env = useEnvContext(); + const api = createApiClient(env); + const { isPaidUser } = usePaidStatus(); + + const router = useRouter(); + + const [, updateFormAction, isUpdatingBranding] = useActionState( + updateBranding, + null + ); + const [, deleteFormAction, isDeletingBranding] = useActionState( + deleteBranding, + null + ); + const [setIsDeleteModalOpen] = useState(false); + + const t = useTranslations(); + + const form = useForm({ + resolver: zodResolver(AuthPageFormSchema), + defaultValues: { + logoUrl: branding?.logoUrl ?? "", + logoWidth: branding?.logoWidth ?? 100, + logoHeight: branding?.logoHeight ?? 100, + orgTitle: branding?.orgTitle ?? `Log in to {{orgName}}`, + orgSubtitle: branding?.orgSubtitle ?? `Log in to {{orgName}}`, + resourceTitle: + branding?.resourceTitle ?? + `Authenticate to access {{resourceName}}`, + resourceSubtitle: + branding?.resourceSubtitle ?? + `Choose your preferred authentication method for {{resourceName}}`, + primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color + }, + disabled: !isPaidUser + }); + + async function updateBranding() { + const isValid = await form.trigger(); + const brandingData = form.getValues(); + + if (!isValid || !isPaidUser) return; + try { + const updateRes = await api.put( + `/org/${orgId}/login-page-branding`, + { + ...brandingData + } + ); + + if (updateRes.status === 200 || updateRes.status === 201) { + router.refresh(); + toast({ + variant: "default", + title: t("success"), + description: t("authPageBrandingUpdated") + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + error, + t("authPageErrorUpdateMessage") + ) + }); + } + } + + async function deleteBranding() { + if (!isPaidUser) return; + + try { + const updateRes = await api.delete( + `/org/${orgId}/login-page-branding` + ); + + if (updateRes.status === 200) { + router.refresh(); + form.reset(); + + toast({ + variant: "default", + title: t("success"), + description: t("authPageBrandingRemoved") + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + error, + t("authPageErrorUpdateMessage") + ) + }); + } + } + + return ( + <> + + + + {t("authPageBranding")} + + + {t("authPageBrandingDescription")} + + + + + + + +
+ + ( + + + {t("brandingPrimaryColor")} + + +
+ + + + +
+ + +
+ )} + /> + +
+ ( + + + {t("brandingLogoURL")} + + + + + + + )} + /> +
+ ( + + + {t("brandingLogoWidth")} + + + + + + + )} + /> + + + + + + ( + + + {t( + "brandingLogoHeight" + )} + + + + + + + )} + /> +
+
+ + {build === "saas" && ( + <> +
+ + {t( + "organizationLoginPageTitle" + )} + + + {t( + "organizationLoginPageDescription" + )} + +
+ +
+ ( + + + {t( + "brandingOrgTitle" + )} + + + + + + + + )} + /> + ( + + + {t( + "brandingOrgSubtitle" + )} + + + + + + + + )} + /> +
+ + )} + +
+ + {t("resourceLoginPageTitle")} + + + {t("resourceLoginPageDescription")} + +
+ +
+ ( + + + {t("brandingResourceTitle")} + + + + + + + + )} + /> + ( + + + {t( + "brandingResourceSubtitle" + )} + + + + + + + )} + /> +
+ + +
+
+ +
+ {branding && ( + + )} + +
+
+ + ); +} diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx index b4472365..139d76b4 100644 --- a/src/components/BrandingLogo.tsx +++ b/src/components/BrandingLogo.tsx @@ -7,6 +7,7 @@ import Image from "next/image"; import { useEffect, useState } from "react"; type BrandingLogoProps = { + logoPath?: string; width: number; height: number; }; @@ -41,13 +42,16 @@ export default function BrandingLogo(props: BrandingLogoProps) { return "/logo/word_mark_white.png"; } - const path = getPath(); - setPath(path); - }, [theme, env]); + setPath(props.logoPath ?? getPath()); + }, [theme, env, props.logoPath]); + + // we use `img` tag here because the `logoPath` could be any URL + // and next.js `Image` component only accepts a restricted number of domains + const Component = props.logoPath ? "img" : Image; return ( path && ( - Logo) { - setLoading(true); + async function onSubmit() { try { await onConfirm(); setOpen(false); - reset(); + form.reset(); } catch (error) { // Handle error if needed console.error("Confirmation failed:", error); - } finally { - setLoading(false); } } @@ -110,7 +80,7 @@ export default function InviteUserForm({ open={open} onOpenChange={(val) => { setOpen(val); - reset(); + form.reset(); }} > @@ -136,7 +106,7 @@ export default function InviteUserForm({
@@ -146,7 +116,12 @@ export default function InviteUserForm({ render={({ field }) => ( - + diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 91ef26da..894315b8 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -42,15 +42,14 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; -import { ListClientsResponse } from "@server/routers/client/listClients"; import { ListSitesResponse } from "@server/routers/site"; -import { ListUsersResponse } from "@server/routers/user"; import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; import { AxiosResponse } from "axios"; @@ -59,6 +58,82 @@ import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +// import { InfoPopup } from "@app/components/ui/info-popup"; + +// Helper to validate port range string format +const isValidPortRangeString = (val: string | undefined | null): boolean => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; + } + + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + return false; + } + + if (startPort > endPort) { + return false; + } + } else { + const port = parseInt(part, 10); + if (isNaN(port)) { + return false; + } + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; +}; + +// Port range string schema for client-side validation +const portRangeStringSchema = z + .string() + .optional() + .nullable() + .refine( + (val) => isValidPortRangeString(val), + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.' + } + ); + +// Helper to determine the port mode from a port range string +type PortMode = "all" | "blocked" | "custom"; +const getPortModeFromString = (val: string | undefined | null): PortMode => { + if (val === "*") return "all"; + if (!val || val.trim() === "") return "blocked"; + return "custom"; +}; + +// Helper to get the port string for API from mode and custom value +const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { + if (mode === "all") return "*"; + if (mode === "blocked") return ""; + return customValue; +}; type Site = ListSitesResponse["sites"][0]; @@ -103,6 +178,9 @@ export default function CreateInternalResourceDialog({ // .max(65535, t("createInternalResourceDialogDestinationPortMax")) // .nullish(), alias: z.string().nullish(), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional(), roles: z .array( z.object({ @@ -209,6 +287,12 @@ export default function CreateInternalResourceDialog({ number | null >(null); + // Port restriction UI state - default to "all" (*) for new resources + const [tcpPortMode, setTcpPortMode] = useState("all"); + const [udpPortMode, setUdpPortMode] = useState("all"); + const [tcpCustomPorts, setTcpCustomPorts] = useState(""); + const [udpCustomPorts, setUdpCustomPorts] = useState(""); + const availableSites = sites.filter( (site) => site.type === "newt" && site.subnet ); @@ -224,6 +308,9 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", + disableIcmp: false, roles: [], users: [], clients: [] @@ -232,6 +319,17 @@ export default function CreateInternalResourceDialog({ const mode = form.watch("mode"); + // Update form values when port mode or custom ports change + useEffect(() => { + const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts); + form.setValue("tcpPortRangeString", tcpValue); + }, [tcpPortMode, tcpCustomPorts, form]); + + useEffect(() => { + const udpValue = getPortStringFromMode(udpPortMode, udpCustomPorts); + form.setValue("udpPortRangeString", udpValue); + }, [udpPortMode, udpCustomPorts, form]); + // Helper function to check if destination contains letters (hostname vs IP) const isHostname = (destination: string): boolean => { return /[a-zA-Z]/.test(destination); @@ -258,10 +356,18 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", + disableIcmp: false, roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode("all"); + setUdpPortMode("all"); + setTcpCustomPorts(""); + setUdpCustomPorts(""); } }, [open]); @@ -304,6 +410,9 @@ export default function CreateInternalResourceDialog({ data.alias.trim() ? data.alias : undefined, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, + disableIcmp: data.disableIcmp ?? false, roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [], @@ -727,6 +836,163 @@ export default function CreateInternalResourceDialog({ )} + {/* Port Restrictions Section */} +
+

+ {t("portRestrictions")} +

+
+ {/* TCP Ports */} + ( + +
+ + TCP + + {/**/} + + {tcpPortMode === "custom" ? ( + + + setTcpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* UDP Ports */} + ( + +
+ + UDP + + {/**/} + + {udpPortMode === "custom" ? ( + + + setUdpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* ICMP Toggle */} + ( + +
+ + ICMP + + + field.onChange(!checked)} + /> + + + {field.value ? t("blocked") : t("allowed")} + +
+ +
+ )} + /> +
+
+ {/* Access Control Section */}

diff --git a/src/components/Credenza.tsx b/src/components/Credenza.tsx index 6a48fc54..0446500c 100644 --- a/src/components/Credenza.tsx +++ b/src/components/Credenza.tsx @@ -179,7 +179,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => { return ( { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; + } + + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + return false; + } + + if (startPort > endPort) { + return false; + } + } else { + const port = parseInt(part, 10); + if (isNaN(port)) { + return false; + } + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; +}; + +// Port range string schema for client-side validation +const portRangeStringSchema = z + .string() + .optional() + .nullable() + .refine( + (val) => isValidPortRangeString(val), + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.' + } + ); + +// Helper to determine the port mode from a port range string +type PortMode = "all" | "blocked" | "custom"; +const getPortModeFromString = (val: string | undefined | null): PortMode => { + if (val === "*") return "all"; + if (!val || val.trim() === "") return "blocked"; + return "custom"; +}; + +// Helper to get the port string for API from mode and custom value +const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { + if (mode === "all") return "*"; + if (mode === "blocked") return ""; + return customValue; +}; type InternalResourceData = { id: number; @@ -61,6 +131,9 @@ type InternalResourceData = { destination: string; // destinationPort?: number | null; alias?: string | null; + tcpPortRangeString?: string | null; + udpPortRangeString?: string | null; + disableIcmp?: boolean; }; type EditInternalResourceDialogProps = { @@ -94,6 +167,9 @@ export default function EditInternalResourceDialog({ destination: z.string().min(1), // destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), alias: z.string().nullish(), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional(), roles: z .array( z.object({ @@ -255,6 +331,24 @@ export default function EditInternalResourceDialog({ number | null >(null); + // Port restriction UI state + const [tcpPortMode, setTcpPortMode] = useState( + getPortModeFromString(resource.tcpPortRangeString) + ); + const [udpPortMode, setUdpPortMode] = useState( + getPortModeFromString(resource.udpPortRangeString) + ); + const [tcpCustomPorts, setTcpCustomPorts] = useState( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + const [udpCustomPorts, setUdpCustomPorts] = useState( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -265,6 +359,9 @@ export default function EditInternalResourceDialog({ destination: resource.destination || "", // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", + disableIcmp: resource.disableIcmp ?? false, roles: [], users: [], clients: [] @@ -273,6 +370,17 @@ export default function EditInternalResourceDialog({ const mode = form.watch("mode"); + // Update form values when port mode or custom ports change + useEffect(() => { + const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts); + form.setValue("tcpPortRangeString", tcpValue); + }, [tcpPortMode, tcpCustomPorts, form]); + + useEffect(() => { + const udpValue = getPortStringFromMode(udpPortMode, udpCustomPorts); + form.setValue("udpPortRangeString", udpValue); + }, [udpPortMode, udpCustomPorts, form]); + // Helper function to check if destination contains letters (hostname vs IP) const isHostname = (destination: string): boolean => { return /[a-zA-Z]/.test(destination); @@ -327,6 +435,9 @@ export default function EditInternalResourceDialog({ data.alias.trim() ? data.alias : null, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, + disableIcmp: data.disableIcmp ?? false, roleIds: (data.roles || []).map((r) => parseInt(r.id)), userIds: (data.users || []).map((u) => u.id), clientIds: (data.clients || []).map((c) => parseInt(c.id)) @@ -396,10 +507,26 @@ export default function EditInternalResourceDialog({ mode: resource.mode || "host", destination: resource.destination || "", alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", + disableIcmp: resource.disableIcmp ?? false, roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); + setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpCustomPorts( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + setUdpCustomPorts( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); previousResourceId.current = resource.id; } @@ -438,10 +565,26 @@ export default function EditInternalResourceDialog({ destination: resource.destination || "", // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", + disableIcmp: resource.disableIcmp ?? false, roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); + setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpCustomPorts( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + setUdpCustomPorts( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); // Reset previous resource ID to ensure clean state on next open previousResourceId.current = null; } @@ -674,6 +817,163 @@ export default function EditInternalResourceDialog({

)} + {/* Port Restrictions Section */} +
+

+ {t("portRestrictions")} +

+
+ {/* TCP Ports */} + ( + +
+ + TCP + + {/**/} + + {tcpPortMode === "custom" ? ( + + + setTcpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* UDP Ports */} + ( + +
+ + UDP + + {/**/} + + {udpPortMode === "custom" ? ( + + + setUdpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* ICMP Toggle */} + ( + +
+ + ICMP + + + field.onChange(!checked)} + /> + + + {field.value ? t("blocked") : t("allowed")} + +
+ +
+ )} + /> +
+
+ {/* Access Control Section */}

diff --git a/src/components/ExitNodeInfoCard.tsx b/src/components/ExitNodeInfoCard.tsx index 8eff6eef..63dff644 100644 --- a/src/components/ExitNodeInfoCard.tsx +++ b/src/components/ExitNodeInfoCard.tsx @@ -19,7 +19,7 @@ export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) { return ( - + <> diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx b/src/components/ExitNodesDataTable.tsx similarity index 75% rename from src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx rename to src/components/ExitNodesDataTable.tsx index c12aa9ba..5573c0e4 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx +++ b/src/components/ExitNodesDataTable.tsx @@ -10,6 +10,8 @@ interface DataTableProps { createRemoteExitNode?: () => void; onRefresh?: () => void; isRefreshing?: boolean; + columnVisibility?: Record; + enableColumnVisibility?: boolean; } export function ExitNodesDataTable({ @@ -17,7 +19,9 @@ export function ExitNodesDataTable({ data, createRemoteExitNode, onRefresh, - isRefreshing + isRefreshing, + columnVisibility, + enableColumnVisibility }: DataTableProps) { const t = useTranslations(); @@ -36,6 +40,10 @@ export function ExitNodesDataTable({ id: "name", desc: false }} + columnVisibility={columnVisibility} + enableColumnVisibility={enableColumnVisibility} + stickyLeftColumn="name" + stickyRightColumn="actions" /> ); } diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx b/src/components/ExitNodesTable.tsx similarity index 96% rename from src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx rename to src/components/ExitNodesTable.tsx index e5250bea..03aa671b 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx +++ b/src/components/ExitNodesTable.tsx @@ -2,7 +2,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { ExtendedColumnDef } from "@app/components/ui/data-table"; -import { ExitNodesDataTable } from "./ExitNodesDataTable"; +import { ExitNodesDataTable } from "@app/components/ExitNodesDataTable"; import { DropdownMenu, DropdownMenuContent, @@ -246,12 +246,13 @@ export default function ExitNodesTable({ }, { id: "actions", - header: () => {t("actions")}, + enableHiding: false, + header: () => , cell: ({ row }) => { const nodeRow = row.original; const remoteExitNodeId = nodeRow.id; return ( -
+
@@ -327,6 +328,11 @@ export default function ExitNodesTable({ } onRefresh={refreshData} isRefreshing={isRefreshing} + columnVisibility={{ + type: false, + address: false, + }} + enableColumnVisibility={true} /> ); diff --git a/src/components/HorizontalTabs.tsx b/src/components/HorizontalTabs.tsx index 9370f67b..ddcc2b08 100644 --- a/src/components/HorizontalTabs.tsx +++ b/src/components/HorizontalTabs.tsx @@ -8,16 +8,17 @@ import { Badge } from "@app/components/ui/badge"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useTranslations } from "next-intl"; -export type HorizontalTabs = Array<{ +export type TabItem = { title: string; href: string; icon?: React.ReactNode; showProfessional?: boolean; -}>; + exact?: boolean; +}; interface HorizontalTabsProps { children: React.ReactNode; - items: HorizontalTabs; + items: TabItem[]; disabled?: boolean; } @@ -38,7 +39,8 @@ export function HorizontalTabs({ .replace("{niceId}", params.niceId as string) .replace("{userId}", params.userId as string) .replace("{clientId}", params.clientId as string) - .replace("{apiKeyId}", params.apiKeyId as string); + .replace("{apiKeyId}", params.apiKeyId as string) + .replace("{remoteExitNodeId}", params.remoteExitNodeId as string); } return ( @@ -49,8 +51,11 @@ export function HorizontalTabs({ {items.map((item) => { const hydratedHref = hydrateHref(item.href); const isActive = - pathname.startsWith(hydratedHref) && + (item.exact + ? pathname === hydratedHref + : pathname.startsWith(hydratedHref)) && !pathname.includes("create"); + const isProfessional = item.showProfessional && !isUnlocked(); const isDisabled = diff --git a/src/components/LayoutHeader.tsx b/src/components/LayoutHeader.tsx index 390e24ac..bef01685 100644 --- a/src/components/LayoutHeader.tsx +++ b/src/components/LayoutHeader.tsx @@ -49,7 +49,7 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) { return (
-
+
diff --git a/src/components/LayoutMobileMenu.tsx b/src/components/LayoutMobileMenu.tsx index 0cf0ae78..49644ff6 100644 --- a/src/components/LayoutMobileMenu.tsx +++ b/src/components/LayoutMobileMenu.tsx @@ -49,7 +49,7 @@ export function LayoutMobileMenu({ return (
-
+
{showSidebar && (
diff --git a/src/components/LogAnalyticsData.tsx b/src/components/LogAnalyticsData.tsx index aaf1b344..90379fdf 100644 --- a/src/components/LogAnalyticsData.tsx +++ b/src/components/LogAnalyticsData.tsx @@ -91,15 +91,12 @@ export function LogAnalyticsData(props: AnalyticsContentProps) { }) ); - const percentBlocked = stats - ? new Intl.NumberFormat(navigator.language, { - maximumFractionDigits: 2 - }).format( - stats.totalRequests - ? (stats.totalBlocked / stats.totalRequests) * 100 - : 0 - ) - : null; + const percentBlocked = + stats && stats.totalRequests > 0 + ? new Intl.NumberFormat(navigator.language, { + maximumFractionDigits: 2 + }).format((stats.totalBlocked / stats.totalRequests) * 100) + : null; const totalRequests = stats ? new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 0 diff --git a/src/components/SecurityFeaturesAlert.tsx b/src/components/PaidFeaturesAlert.tsx similarity index 61% rename from src/components/SecurityFeaturesAlert.tsx rename to src/components/PaidFeaturesAlert.tsx index 58fc1d9b..30ba7d76 100644 --- a/src/components/SecurityFeaturesAlert.tsx +++ b/src/components/PaidFeaturesAlert.tsx @@ -2,17 +2,14 @@ import { Alert, AlertDescription } from "@app/components/ui/alert"; import { build } from "@server/build"; import { useTranslations } from "next-intl"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; -export function SecurityFeaturesAlert() { +export function PaidFeaturesAlert() { const t = useTranslations(); - const { isUnlocked } = useLicenseStatusContext(); - const subscriptionStatus = useSubscriptionStatusContext(); - + const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus(); return ( <> - {build === "saas" && !subscriptionStatus?.isSubscribed() ? ( + {build === "saas" && !hasSaasSubscription ? ( {t("subscriptionRequiredToUse")} @@ -20,7 +17,7 @@ export function SecurityFeaturesAlert() { ) : null} - {build === "enterprise" && !isUnlocked() ? ( + {build === "enterprise" && !hasEnterpriseLicense ? ( {t("licenseRequiredToUse")} diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 49a10215..4862d780 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -27,6 +27,7 @@ function getActionsCategories(root: boolean) { [t("actionUpdateOrg")]: "updateOrg", [t("actionGetOrgUser")]: "getOrgUser", [t("actionInviteUser")]: "inviteUser", + [t("actionRemoveInvitation")]: "removeInvitation", [t("actionListInvitations")]: "listInvitations", [t("actionRemoveUser")]: "removeUser", [t("actionListUsers")]: "listUsers", diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 90d6cf78..133d9a6c 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -39,16 +39,15 @@ import { resourceWhitelistProxy, resourceAccessProxy } from "@app/actions/server"; -import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import Link from "next/link"; -import Image from "next/image"; import BrandingLogo from "@app/components/BrandingLogo"; import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { replacePlaceholder } from "@app/lib/replacePlaceholder"; const pinSchema = z.object({ pin: z @@ -88,6 +87,14 @@ type ResourceAuthPortalProps = { redirect: string; idps?: LoginFormIDP[]; orgId?: string; + branding?: { + logoUrl: string; + logoWidth: number; + logoHeight: number; + primaryColor: string | null; + resourceTitle: string; + resourceSubtitle: string | null; + }; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { @@ -104,7 +111,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { return colLength; }; - const [numMethods, setNumMethods] = useState(getNumMethods()); + const [numMethods] = useState(() => getNumMethods()); const [passwordError, setPasswordError] = useState(null); const [pincodeError, setPincodeError] = useState(null); @@ -309,13 +316,19 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { } } - function getTitle() { + function getTitle(resourceName: string) { if ( - isUnlocked() && build !== "oss" && - env.branding.resourceAuthPage?.titleText + isUnlocked() && + (!!env.branding.resourceAuthPage?.titleText || + !!props.branding?.resourceTitle) ) { - return env.branding.resourceAuthPage.titleText; + if (props.branding?.resourceTitle) { + return replacePlaceholder(props.branding?.resourceTitle, { + resourceName + }); + } + return env.branding.resourceAuthPage?.titleText; } return t("authenticationRequired"); } @@ -324,10 +337,16 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { if ( isUnlocked() && build !== "oss" && - env.branding.resourceAuthPage?.subtitleText + (env.branding.resourceAuthPage?.subtitleText || + props.branding?.resourceSubtitle) ) { - return env.branding.resourceAuthPage.subtitleText - .split("{{resourceName}}") + if (props.branding?.resourceSubtitle) { + return replacePlaceholder(props.branding?.resourceSubtitle, { + resourceName + }); + } + return env.branding.resourceAuthPage?.subtitleText + ?.split("{{resourceName}}") .join(resourceName); } return numMethods > 1 @@ -336,14 +355,23 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { } const logoWidth = isUnlocked() - ? env.branding.logo?.authPage?.width || 100 + ? (props.branding?.logoWidth ?? + env.branding.logo?.authPage?.width ?? + 100) : 100; const logoHeight = isUnlocked() - ? env.branding.logo?.authPage?.height || 100 + ? (props.branding?.logoHeight ?? + env.branding.logo?.authPage?.height ?? + 100) : 100; return ( -
+
{!accessDenied ? (
{isUnlocked() && build === "enterprise" ? ( @@ -381,15 +409,19 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { {isUnlocked() && build !== "oss" && - env.branding?.resourceAuthPage?.showLogo && ( + (env.branding?.resourceAuthPage?.showLogo || + props.branding) && (
)} - {getTitle()} + + {getTitle(props.resource.name)} + {getSubtitle(props.resource.name)} diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 46aeb79f..d8f50e99 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -13,6 +13,7 @@ import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, + ArrowUpRight, Check, MoreHorizontal, X @@ -46,6 +47,7 @@ export type SiteRow = { address?: string; exitNodeName?: string; exitNodeEndpoint?: string; + remoteExitNodeId?: string; }; type SitesTableProps = { @@ -303,27 +305,51 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { }, cell: ({ row }) => { const originalRow = row.original; - return originalRow.exitNodeName ? ( -
- {originalRow.exitNodeName} - {build == "saas" && - originalRow.exitNodeName && - [ - "mercury", - "venus", - "earth", - "mars", - "jupiter", - "saturn", - "uranus", - "neptune" - ].includes( - originalRow.exitNodeName.toLowerCase() - ) && Cloud} -
- ) : ( - "-" - ); + if (!originalRow.exitNodeName) { + return "-"; + } + + const isCloudNode = + build == "saas" && + originalRow.exitNodeName && + [ + "mercury", + "venus", + "earth", + "mars", + "jupiter", + "saturn", + "uranus", + "neptune" + ].includes(originalRow.exitNodeName.toLowerCase()); + + if (isCloudNode) { + const capitalizedName = + originalRow.exitNodeName.charAt(0).toUpperCase() + + originalRow.exitNodeName.slice(1).toLowerCase(); + return ( + + Pangolin {capitalizedName} + + ); + } + + // Self-hosted node + if (originalRow.remoteExitNodeId) { + return ( + + + + ); + } + + // Fallback if no remoteExitNodeId + return {originalRow.exitNodeName}; } }, { diff --git a/src/components/Toploader.tsx b/src/components/Toploader.tsx index e045cc12..17b32bad 100644 --- a/src/components/Toploader.tsx +++ b/src/components/Toploader.tsx @@ -7,7 +7,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; export function TopLoader() { return ( <> - + ); diff --git a/src/components/ValidateOidcToken.tsx b/src/components/ValidateOidcToken.tsx index 8f61cdd1..3677f625 100644 --- a/src/components/ValidateOidcToken.tsx +++ b/src/components/ValidateOidcToken.tsx @@ -26,6 +26,11 @@ type ValidateOidcTokenParams = { stateCookie: string | undefined; idp: { name: string }; loginPageId?: number; + providerError?: { + error: string; + description?: string | null; + uri?: string | null; + }; }; export default function ValidateOidcToken(props: ValidateOidcTokenParams) { @@ -35,14 +40,65 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isProviderError, setIsProviderError] = useState(false); const { licenseStatus, isLicenseViolation } = useLicenseStatusContext(); const t = useTranslations(); useEffect(() => { - async function validate() { + let isCancelled = false; + + async function runValidation() { setLoading(true); + setIsProviderError(false); + + if (props.providerError?.error) { + const providerMessage = + props.providerError.description || + t("idpErrorOidcProviderRejected", { + error: props.providerError.error, + defaultValue: + "The identity provider returned an error: {error}." + }); + const suffix = props.providerError.uri + ? ` (${props.providerError.uri})` + : ""; + if (!isCancelled) { + setIsProviderError(true); + setError(`${providerMessage}${suffix}`); + setLoading(false); + } + return; + } + + if (!props.code) { + if (!isCancelled) { + setIsProviderError(false); + setError( + t("idpErrorOidcMissingCode", { + defaultValue: + "The identity provider did not return an authorization code." + }) + ); + setLoading(false); + } + return; + } + + if (!props.expectedState || !props.stateCookie) { + if (!isCancelled) { + setIsProviderError(false); + setError( + t("idpErrorOidcMissingState", { + defaultValue: + "The login request is missing state information. Please restart the login process." + }) + ); + setLoading(false); + } + return; + } console.log(t("idpOidcTokenValidating"), { code: props.code, @@ -57,22 +113,28 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { try { const response = await validateOidcUrlCallbackProxy( props.idpId, - props.code || "", - props.expectedState || "", - props.stateCookie || "", + props.code, + props.expectedState, + props.stateCookie, props.loginPageId ); if (response.error) { - setError(response.message); - setLoading(false); + if (!isCancelled) { + setIsProviderError(false); + setError(response.message); + setLoading(false); + } return; } const data = response.data; if (!data) { - setError("Unable to validate OIDC token"); - setLoading(false); + if (!isCancelled) { + setIsProviderError(false); + setError("Unable to validate OIDC token"); + setLoading(false); + } return; } @@ -82,8 +144,11 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { router.push(env.app.dashboardUrl); } - setLoading(false); - await new Promise((resolve) => setTimeout(resolve, 100)); + if (!isCancelled) { + setIsProviderError(false); + setLoading(false); + await new Promise((resolve) => setTimeout(resolve, 100)); + } if (redirectUrl.startsWith("http")) { window.location.href = data.redirectUrl; // this is validated by the parent using this component @@ -92,18 +157,27 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { } } catch (e: any) { console.error(e); - setError( - t("idpErrorOidcTokenValidating", { - defaultValue: - "An unexpected error occurred. Please try again." - }) - ); + if (!isCancelled) { + setIsProviderError(false); + setError( + t("idpErrorOidcTokenValidating", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); + } } finally { - setLoading(false); + if (!isCancelled) { + setLoading(false); + } } } - validate(); + runValidation(); + + return () => { + isCancelled = true; + }; }, []); return ( @@ -134,12 +208,16 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { - - {t("idpErrorConnectingTo", { - name: props.idp.name - })} + + {isProviderError + ? error + : t("idpErrorConnectingTo", { + name: props.idp.name + })} - {error} + {!isProviderError && ( + {error} + )} )} diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx index 33d0ab54..bd7c8db6 100644 --- a/src/components/private/AuthPageSettings.tsx +++ b/src/components/private/AuthPageSettings.tsx @@ -3,16 +3,8 @@ import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; -import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@/components/ui/form"; +import { useState, useEffect, useActionState } from "react"; +import { Form } from "@/components/ui/form"; import { Label } from "@/components/ui/label"; import { z } from "zod"; import { useForm } from "react-hook-form"; @@ -51,9 +43,9 @@ import DomainPicker from "@app/components/DomainPicker"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { InfoPopup } from "@app/components/ui/info-popup"; import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; -import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { PaidFeaturesAlert } from "../PaidFeaturesAlert"; // Auth page form schema const AuthPageFormSchema = z.object({ @@ -61,11 +53,10 @@ const AuthPageFormSchema = z.object({ authPageSubdomain: z.string().optional() }); -type AuthPageFormValues = z.infer; - interface AuthPageSettingsProps { onSaveSuccess?: () => void; onSaveError?: (error: any) => void; + loginPage: GetLoginPageResponse | null; } export interface AuthPageSettingsRef { @@ -73,475 +64,428 @@ export interface AuthPageSettingsRef { hasUnsavedChanges: () => boolean; } -const AuthPageSettings = forwardRef( - ({ onSaveSuccess, onSaveError }, ref) => { - const { org } = useOrgContext(); - const api = createApiClient(useEnvContext()); - const router = useRouter(); - const t = useTranslations(); - const { env } = useEnvContext(); +function AuthPageSettings({ + onSaveSuccess, + onSaveError, + loginPage: defaultLoginPage +}: AuthPageSettingsProps) { + const { org } = useOrgContext(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + const t = useTranslations(); + const { env } = useEnvContext(); - const subscription = useSubscriptionStatusContext(); + const { hasSaasSubscription } = usePaidStatus(); - // Auth page domain state - const [loginPage, setLoginPage] = useState( - null - ); - const [loginPageExists, setLoginPageExists] = useState(false); - const [editDomainOpen, setEditDomainOpen] = useState(false); - const [baseDomains, setBaseDomains] = useState([]); - const [selectedDomain, setSelectedDomain] = useState<{ - domainId: string; - subdomain?: string; - fullDomain: string; - baseDomain: string; - } | null>(null); - const [loadingLoginPage, setLoadingLoginPage] = useState(true); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [loadingSave, setLoadingSave] = useState(false); + // Auth page domain state + const [loginPage, setLoginPage] = useState(defaultLoginPage); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + const [loginPageExists, setLoginPageExists] = useState( + Boolean(defaultLoginPage) + ); + const [editDomainOpen, setEditDomainOpen] = useState(false); + const [baseDomains, setBaseDomains] = useState([]); + const [selectedDomain, setSelectedDomain] = useState<{ + domainId: string; + subdomain?: string; + fullDomain: string; + baseDomain: string; + } | null>(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const form = useForm({ - resolver: zodResolver(AuthPageFormSchema), - defaultValues: { - authPageDomainId: loginPage?.domainId || "", - authPageSubdomain: loginPage?.subdomain || "" - }, - mode: "onChange" - }); - - // Expose save function to parent component - useImperativeHandle( - ref, - () => ({ - saveAuthSettings: async () => { - await form.handleSubmit(onSubmit)(); - }, - hasUnsavedChanges: () => hasUnsavedChanges - }), - [form, hasUnsavedChanges] - ); - - // Fetch login page and domains data - useEffect(() => { - const fetchLoginPage = async () => { - try { - const res = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/login-page`); - if (res.status === 200) { - setLoginPage(res.data.data); - setLoginPageExists(true); - // Update form with login page data - form.setValue( - "authPageDomainId", - res.data.data.domainId || "" - ); - form.setValue( - "authPageSubdomain", - res.data.data.subdomain || "" - ); - } - } catch (err) { - // Login page doesn't exist yet, that's okay - setLoginPage(null); - setLoginPageExists(false); - } finally { - setLoadingLoginPage(false); - } - }; - - const fetchDomains = async () => { - try { - const res = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/domains/`); - if (res.status === 200) { - const rawDomains = res.data.data.domains as DomainRow[]; - const domains = rawDomains.map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain) - })); - setBaseDomains(domains); - } - } catch (err) { - console.error("Failed to fetch domains:", err); - } - }; - - if (org?.org.orgId) { - fetchLoginPage(); - fetchDomains(); - } - }, []); - - // Handle domain selection from modal - function handleDomainSelection(domain: { - domainId: string; - subdomain?: string; - fullDomain: string; - baseDomain: string; - }) { - form.setValue("authPageDomainId", domain.domainId); - form.setValue("authPageSubdomain", domain.subdomain || ""); - setEditDomainOpen(false); - - // Update loginPage state to show the selected domain immediately - const sanitizedSubdomain = domain.subdomain - ? finalizeSubdomainSanitize(domain.subdomain) - : ""; - - const sanitizedFullDomain = sanitizedSubdomain - ? `${sanitizedSubdomain}.${domain.baseDomain}` - : domain.baseDomain; - - // Only update loginPage state if a login page already exists - if (loginPageExists && loginPage) { - setLoginPage({ - ...loginPage, - domainId: domain.domainId, - subdomain: sanitizedSubdomain, - fullDomain: sanitizedFullDomain - }); - } - - setHasUnsavedChanges(true); - } - - // Clear auth page domain - function clearAuthPageDomain() { - form.setValue("authPageDomainId", ""); - form.setValue("authPageSubdomain", ""); - setLoginPage(null); - setHasUnsavedChanges(true); - } - - async function onSubmit(data: AuthPageFormValues) { - setLoadingSave(true); + const form = useForm({ + resolver: zodResolver(AuthPageFormSchema), + defaultValues: { + authPageDomainId: loginPage?.domainId || "", + authPageSubdomain: loginPage?.subdomain || "" + }, + mode: "onChange" + }); + // Fetch login page and domains data + useEffect(() => { + const fetchDomains = async () => { try { - // Handle auth page domain - if (data.authPageDomainId) { - if ( - build === "enterprise" || - (build === "saas" && subscription?.subscribed) - ) { - const sanitizedSubdomain = data.authPageSubdomain - ? finalizeSubdomainSanitize(data.authPageSubdomain) - : ""; + const res = await api.get>( + `/org/${org?.org.orgId}/domains/` + ); + if (res.status === 200) { + const rawDomains = res.data.data.domains as DomainRow[]; + const domains = rawDomains.map((domain) => ({ + ...domain, + baseDomain: toUnicode(domain.baseDomain) + })); + setBaseDomains(domains); + } + } catch (err) { + console.error("Failed to fetch domains:", err); + } + }; - if (loginPageExists) { - // Login page exists on server - need to update it - // First, we need to get the loginPageId from the server since loginPage might be null locally - let loginPageId: number; + if (org?.org.orgId) { + fetchDomains(); + } + }, []); - if (loginPage) { - // We have the loginPage data locally - loginPageId = loginPage.loginPageId; - } else { - // User cleared selection locally, but login page still exists on server - // We need to fetch it to get the loginPageId - const fetchRes = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/login-page`); - loginPageId = fetchRes.data.data.loginPageId; - } + // Handle domain selection from modal + function handleDomainSelection(domain: { + domainId: string; + subdomain?: string; + fullDomain: string; + baseDomain: string; + }) { + form.setValue("authPageDomainId", domain.domainId); + form.setValue("authPageSubdomain", domain.subdomain || ""); + setEditDomainOpen(false); - // Update existing auth page domain - const updateRes = await api.post( - `/org/${org?.org.orgId}/login-page/${loginPageId}`, - { - domainId: data.authPageDomainId, - subdomain: sanitizedSubdomain || null - } - ); + // Update loginPage state to show the selected domain immediately + const sanitizedSubdomain = domain.subdomain + ? finalizeSubdomainSanitize(domain.subdomain) + : ""; - if (updateRes.status === 201) { - setLoginPage(updateRes.data.data); - setLoginPageExists(true); - } + const sanitizedFullDomain = sanitizedSubdomain + ? `${sanitizedSubdomain}.${domain.baseDomain}` + : domain.baseDomain; + + // Only update loginPage state if a login page already exists + if (loginPageExists && loginPage) { + setLoginPage({ + ...loginPage, + domainId: domain.domainId, + subdomain: sanitizedSubdomain, + fullDomain: sanitizedFullDomain + }); + } + + setHasUnsavedChanges(true); + } + + // Clear auth page domain + function clearAuthPageDomain() { + form.setValue("authPageDomainId", ""); + form.setValue("authPageSubdomain", ""); + setLoginPage(null); + setHasUnsavedChanges(true); + } + + async function onSubmit() { + const isValid = await form.trigger(); + if (!isValid) return; + + const data = form.getValues(); + + try { + // Handle auth page domain + if (data.authPageDomainId) { + if (build === "enterprise" || hasSaasSubscription) { + const sanitizedSubdomain = data.authPageSubdomain + ? finalizeSubdomainSanitize(data.authPageSubdomain) + : ""; + + if (loginPageExists) { + // Login page exists on server - need to update it + // First, we need to get the loginPageId from the server since loginPage might be null locally + let loginPageId: number; + + if (loginPage) { + // We have the loginPage data locally + loginPageId = loginPage.loginPageId; } else { - // No login page exists on server - create new one - const createRes = await api.put( - `/org/${org?.org.orgId}/login-page`, - { - domainId: data.authPageDomainId, - subdomain: sanitizedSubdomain || null - } - ); + // User cleared selection locally, but login page still exists on server + // We need to fetch it to get the loginPageId + const fetchRes = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/login-page`); + loginPageId = fetchRes.data.data.loginPageId; + } - if (createRes.status === 201) { - setLoginPage(createRes.data.data); - setLoginPageExists(true); + // Update existing auth page domain + const updateRes = await api.post( + `/org/${org?.org.orgId}/login-page/${loginPageId}`, + { + domainId: data.authPageDomainId, + subdomain: sanitizedSubdomain || null } + ); + + if (updateRes.status === 201) { + setLoginPage(updateRes.data.data); + setLoginPageExists(true); + } + } else { + // No login page exists on server - create new one + const createRes = await api.put( + `/org/${org?.org.orgId}/login-page`, + { + domainId: data.authPageDomainId, + subdomain: sanitizedSubdomain || null + } + ); + + if (createRes.status === 201) { + setLoginPage(createRes.data.data); + setLoginPageExists(true); } } - } else if (loginPageExists) { - // Delete existing auth page domain if no domain selected - let loginPageId: number; + } + } else if (loginPageExists) { + // Delete existing auth page domain if no domain selected + let loginPageId: number; - if (loginPage) { - // We have the loginPage data locally - loginPageId = loginPage.loginPageId; - } else { - // User cleared selection locally, but login page still exists on server - // We need to fetch it to get the loginPageId - const fetchRes = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/login-page`); - loginPageId = fetchRes.data.data.loginPageId; - } - - await api.delete( - `/org/${org?.org.orgId}/login-page/${loginPageId}` - ); - setLoginPage(null); - setLoginPageExists(false); + if (loginPage) { + // We have the loginPage data locally + loginPageId = loginPage.loginPageId; + } else { + // User cleared selection locally, but login page still exists on server + // We need to fetch it to get the loginPageId + const fetchRes = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/login-page`); + loginPageId = fetchRes.data.data.loginPageId; } - setHasUnsavedChanges(false); - router.refresh(); - onSaveSuccess?.(); - } catch (e) { - toast({ - variant: "destructive", - title: t("authPageErrorUpdate"), - description: formatAxiosError( - e, - t("authPageErrorUpdateMessage") - ) - }); - onSaveError?.(e); - } finally { - setLoadingSave(false); + await api.delete( + `/org/${org?.org.orgId}/login-page/${loginPageId}` + ); + setLoginPage(null); + setLoginPageExists(false); } + + setHasUnsavedChanges(false); + router.refresh(); + onSaveSuccess?.(); + toast({ + variant: "default", + title: t("success"), + description: t("authPageDomainUpdated") + }); + } catch (e) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + e, + t("authPageErrorUpdateMessage") + ) + }); + onSaveError?.(e); } - - return ( - <> - - - - {t("authPage")} - - - {t("authPageDescription")} - - - - {build === "saas" && !subscription?.subscribed ? ( - - - {t("orgAuthPageDisabled")}{" "} - {t("subscriptionRequiredToUse")} - - - ) : null} - - - {loadingLoginPage ? ( -
-
- {t("loading")} -
-
- ) : ( - - -
- -
- - - {loginPage && - !loginPage.domainId ? ( - - ) : loginPage?.fullDomain ? ( - - {`${window.location.protocol}//${loginPage.fullDomain}`} - - ) : form.watch( - "authPageDomainId" - ) ? ( - // Show selected domain from form state when no loginPage exists yet - (() => { - const selectedDomainId = - form.watch( - "authPageDomainId" - ); - const selectedSubdomain = - form.watch( - "authPageSubdomain" - ); - const domain = - baseDomains.find( - (d) => - d.domainId === - selectedDomainId - ); - if (domain) { - const sanitizedSubdomain = - selectedSubdomain - ? finalizeSubdomainSanitize( - selectedSubdomain - ) - : ""; - const fullDomain = - sanitizedSubdomain - ? `${sanitizedSubdomain}.${domain.baseDomain}` - : domain.baseDomain; - return fullDomain; - } - return t( - "noDomainSet" - ); - })() - ) : ( - t("noDomainSet") - )} - -
- - {form.watch( - "authPageDomainId" - ) && ( - - )} -
-
- - {!form.watch( - "authPageDomainId" - ) && ( -
- {t( - "addDomainToEnableCustomAuthPages" - )} -
- )} - - {env.flags.usePangolinDns && - (build === "enterprise" || - (build === "saas" && - subscription?.subscribed)) && - loginPage?.domainId && - loginPage?.fullDomain && - !hasUnsavedChanges && ( - - )} -
- - - )} -
-
-
- - {/* Domain Picker Modal */} - setEditDomainOpen(setOpen)} - > - - - - {loginPage - ? t("editAuthPageDomain") - : t("setAuthPageDomain")} - - - {t("selectDomainForOrgAuthPage")} - - - - { - const selected = { - domainId: res.domainId, - subdomain: res.subdomain, - fullDomain: res.fullDomain, - baseDomain: res.baseDomain - }; - setSelectedDomain(selected); - }} - /> - - - - - - - - - - - ); } -); + + return ( + <> + + + {t("customDomain")} + + {t("authPageDescription")} + + + + + + + +
+ +
+ +
+ + + {loginPage && + !loginPage.domainId ? ( + + ) : loginPage?.fullDomain ? ( + + {`${window.location.protocol}//${loginPage.fullDomain}`} + + ) : form.watch( + "authPageDomainId" + ) ? ( + // Show selected domain from form state when no loginPage exists yet + (() => { + const selectedDomainId = + form.watch( + "authPageDomainId" + ); + const selectedSubdomain = + form.watch( + "authPageSubdomain" + ); + const domain = + baseDomains.find( + (d) => + d.domainId === + selectedDomainId + ); + if (domain) { + const sanitizedSubdomain = + selectedSubdomain + ? finalizeSubdomainSanitize( + selectedSubdomain + ) + : ""; + const fullDomain = + sanitizedSubdomain + ? `${sanitizedSubdomain}.${domain.baseDomain}` + : domain.baseDomain; + return fullDomain; + } + return t("noDomainSet"); + })() + ) : ( + t("noDomainSet") + )} + +
+ + {form.watch("authPageDomainId") && ( + + )} +
+
+ + {!form.watch("authPageDomainId") && ( +
+ {t( + "addDomainToEnableCustomAuthPages" + )} +
+ )} + + {env.flags.usePangolinDns && + (build === "enterprise" || + !hasSaasSubscription) && + loginPage?.domainId && + loginPage?.fullDomain && + !hasUnsavedChanges && ( + + )} +
+
+ +
+
+ +
+ +
+
+ + {/* Domain Picker Modal */} + setEditDomainOpen(setOpen)} + > + + + + {loginPage + ? t("editAuthPageDomain") + : t("setAuthPageDomain")} + + + {t("selectDomainForOrgAuthPage")} + + + + { + const selected = + res === null + ? null + : { + domainId: res.domainId, + subdomain: res.subdomain, + fullDomain: res.fullDomain, + baseDomain: res.baseDomain + }; + setSelectedDomain(selected); + }} + /> + + + + + + + + + + + ); +} AuthPageSettings.displayName = "AuthPageSettings"; diff --git a/src/components/tags/autocomplete.tsx b/src/components/tags/autocomplete.tsx index 3230f11b..ee865eb6 100644 --- a/src/components/tags/autocomplete.tsx +++ b/src/components/tags/autocomplete.tsx @@ -308,7 +308,7 @@ export const Autocomplete: React.FC = ({ role="option" aria-selected={isSelected} className={cn( - "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent", + "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 hover:bg-accent", isSelected && "bg-accent text-accent-foreground", classStyleProps?.commandItem diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 86fcea09..c3037250 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -19,7 +19,7 @@ const buttonVariants = cva( outlinePrimary: "border border-primary bg-card hover:bg-primary/10 text-primary ", secondary: - "bg-secondary border border-input border text-secondary-foreground hover:bg-secondary/80 ", + "bg-muted border border-input border text-secondary-foreground hover:bg-muted/80 ", ghost: "hover:bg-accent hover:text-accent-foreground", squareOutlinePrimary: "border border-primary bg-card hover:bg-primary/10 text-primary rounded-md ", diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 328431e9..1ff09433 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -38,7 +38,7 @@ const DialogContent = React.forwardRef< ) => (
+ internal.get>( + `/org/${orgId}/user/${userId}`, + await authCookieHeader() + ) +); diff --git a/src/lib/api/getCachedSubscription.ts b/src/lib/api/getCachedSubscription.ts new file mode 100644 index 00000000..dbffee5d --- /dev/null +++ b/src/lib/api/getCachedSubscription.ts @@ -0,0 +1,8 @@ +import type { AxiosResponse } from "axios"; +import { cache } from "react"; +import { priv } from "."; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; + +export const getCachedSubscription = cache(async (orgId: string) => + priv.get>(`/org/${orgId}/billing/tier`) +); diff --git a/src/lib/api/isOrgSubscribed.ts b/src/lib/api/isOrgSubscribed.ts new file mode 100644 index 00000000..9440330b --- /dev/null +++ b/src/lib/api/isOrgSubscribed.ts @@ -0,0 +1,30 @@ +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; +import { cache } from "react"; +import { getCachedSubscription } from "./getCachedSubscription"; +import { priv } from "."; +import { AxiosResponse } from "axios"; +import { GetLicenseStatusResponse } from "@server/routers/license/types"; + +export const isOrgSubscribed = cache(async (orgId: string) => { + let subscribed = false; + + if (build === "enterprise") { + try { + const licenseStatusRes = + await priv.get>( + "/license/status" + ); + subscribed = licenseStatusRes.data.data.isLicenseValid; + } catch (error) {} + } else if (build === "saas") { + try { + const subRes = await getCachedSubscription(orgId); + subscribed = + subRes.data.data.tier === TierId.STANDARD && + subRes.data.data.active; + } catch {} + } + + return subscribed; +}); diff --git a/src/lib/auth/verifySession.ts b/src/lib/auth/verifySession.ts index 0c87b1ae..993f709f 100644 --- a/src/lib/auth/verifySession.ts +++ b/src/lib/auth/verifySession.ts @@ -3,8 +3,9 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { GetUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { pullEnv } from "../pullEnv"; +import { cache } from "react"; -export async function verifySession({ +export const verifySession = cache(async function ({ skipCheckVerifyEmail, forceLogin }: { @@ -14,8 +15,12 @@ export async function verifySession({ const env = pullEnv(); try { + const search = new URLSearchParams(); + if (forceLogin) { + search.set("forceLogin", "true"); + } const res = await internal.get>( - `/user${forceLogin ? "?forceLogin=true" : ""}`, + `/user?${search.toString()}`, await authCookieHeader() ); @@ -37,4 +42,4 @@ export async function verifySession({ } catch (e) { return null; } -} +}); diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 513e2e4f..0dc44147 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -79,7 +79,7 @@ export const productUpdatesQueries = { } return false; }, - enabled: enabled && (build === "oss" || build === "enterprise") // disabled in cloud version + enabled: enabled && build !== "saas" // disabled in cloud version // because we don't need to listen for new versions there }) }; diff --git a/src/lib/replacePlaceholder.ts b/src/lib/replacePlaceholder.ts new file mode 100644 index 00000000..598056e3 --- /dev/null +++ b/src/lib/replacePlaceholder.ts @@ -0,0 +1,17 @@ +export function replacePlaceholder( + stringWithPlaceholder: string, + data: Record +) { + let newString = stringWithPlaceholder; + + const keys = Object.keys(data); + + for (const key of keys) { + newString = newString.replace( + new RegExp(`{{${key}}}`, "gm"), + data[key] + ); + } + + return newString; +}