diff --git a/.dockerignore b/.dockerignore index 9223d5b5..ecd919cd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,4 +29,6 @@ CONTRIBUTING.md dist .git migrations/ -config/ \ No newline at end of file +config/ +build.ts +tsconfig.json \ No newline at end of file diff --git a/.github/workflows/dev-image.yml b/.github/workflows/dev-image.yml deleted file mode 100644 index 1f9ea0ae..00000000 --- a/.github/workflows/dev-image.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Create Dev-Image - -on: - pull_request: - branches: - - main - - dev - types: - - opened - - synchronize - - reopened - -jobs: - docker: - runs-on: ubuntu-latest - - env: - TAG_URL: https://hub.docker.com/r/${{ vars.DOCKER_HUB_REPO }}/tags - TAG: ${{ vars.DOCKER_HUB_REPO }}:dev-pr${{ github.event.pull_request.number }} - TAG_PG: ${{ vars.DOCKER_HUB_REPO }}:postgresql-dev-pr${{ github.event.pull_request.number }} - - steps: - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push Docker image SQLITE - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64 - push: true - tags: ${{ env.TAG }} - cache-from: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache - cache-to: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache,mode=max - build-args: DATABASE=sqlite - - - name: Build and push Docker image PG - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64 - push: true - tags: ${{ env.TAG_PG }} - cache-from: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache-pg - cache-to: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache-pg,mode=max - build-args: DATABASE=pg - - - uses: actions/github-script@v8 - with: - script: | - const repoUrl = process.env.TAG_URL; - const tag = process.env.TAG; - const tagPg = process.env.TAG_PG; - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `👋 Thanks for your PR! - Dev images for this PR are now available on [docker hub](${repoUrl}): - - **SQLITE Image:** - \`\`\` - ${tag} - \`\`\` - - **Postgresql Image:** - \`\`\` - ${tagPg} - \`\`\`` - }) - diff --git a/.gitignore b/.gitignore index 661c32f9..2fc6b10b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,6 @@ server/db/index.ts server/build.ts postgres/ dynamic/ -*.mmdb \ No newline at end of file +*.mmdb +scratch/ +tsconfig.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index edee74eb..1a1493a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,29 @@ RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts -RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi +# Copy the appropriate TypeScript configuration based on build type +RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \ + elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \ + elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \ + fi + +# if the build is oss then remove the server/private directory +RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi + +RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi + +RUN mkdir -p dist +RUN npm run next:build +RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD +RUN if [ "$DATABASE" = "pg" ]; then \ + node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \ + else \ + node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \ + fi + +# test to make sure the build output is there and error if not +RUN test -f dist/server.mjs -RUN npm run build:$DATABASE RUN npm run build:cli FROM node:22-alpine AS runner diff --git a/Makefile b/Makefile index e4a709ec..ed888631 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ build-release: exit 1; \ fi docker buildx build \ + --build-arg BUILD=oss --build-arg DATABASE=sqlite \ --platform linux/arm64,linux/amd64 \ --tag fosrl/pangolin:latest \ @@ -16,6 +17,7 @@ build-release: --tag fosrl/pangolin:$(tag) \ --push . docker buildx build \ + --build-arg BUILD=oss --build-arg DATABASE=pg \ --platform linux/arm64,linux/amd64 \ --tag fosrl/pangolin:postgresql-latest \ diff --git a/README.md b/README.md index 458103c4..0e694c46 100644 --- a/README.md +++ b/README.md @@ -1,157 +1,82 @@

- - - Pangolin Logo + + + + Pangolin Logo +

-

Secure gateway to your private networks

-
- -_Pangolin tunnels your services to the internet so you can access anything from anywhere._ - -
-
Website | - - Quick Install Guide + + Documentation | Contact Us - | - - Slack - - | - - Discord -
+
+
+ +[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) [![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://digpangolin.com/slack) [![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) ![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) -[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) [![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
-

- - Start testing Pangolin at pangolin.fossorial.io - -

+Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN. -Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports. +## Installation -Preview - -![gif](public/clip.gif) - -## Key Features - -### Reverse Proxy Through WireGuard Tunnel - -- Expose private resources on your network **without opening ports** (firewall punching). -- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt). -- Built-in support for any WireGuard client. -- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/). -- Support for HTTP/HTTPS and **raw TCP/UDP services**. -- Load balancing. -- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock). - - **Automatically install and configure Crowdsec via Pangolin's installer script.** -- Attach as many sites to the central server as you wish. - -### Identity & Access Management - -- Centralized authentication system using platform SSO. **Users will only have to manage one login.** -- **Define access control rules for IPs, IP ranges, and URL paths per resource.** -- TOTP with backup codes for two-factor authentication. -- Create organizations, each with multiple sites, users, and roles. -- **Role-based access control** to manage resource access permissions. -- Additional authentication options include: - - Email whitelisting with **one-time passcodes.** - - **Temporary, self-destructing share links.** - - Resource specific pin codes. - - Resource specific passwords. - - Passkeys -- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others. - - Auto-provision users and roles from your IdP. - -Auth and diagram - -## Use Cases - -### Manage Access to Internal Apps - -- Grant users access to your apps from anywhere using just a web browser. No client software required. - -### Developers and DevOps - -- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access. - -### Secure API Gateway - -- One application load balancer across multiple clouds and on-premises. - -### IoT and Edge Devices - -- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring. - -Sites +Check out the [quick install guide](https://docs.digpangolin.com) for how to install and set up Pangolin. ## Deployment Options -### Fully Self Hosted +| | Description | +|-----------------|--------------| +| **Self-Host: Community Edition** | Free, open source, and AGPL-3 compliant. | +| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. | +| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://github.com/fosrl/remote-note) and connect to our control plane. | -Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started. +## Key Features -> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal! +Pangolin packages everything you need for seamless application access and exposure into one cohesive platform. -### Pangolin Cloud +| | | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| +| **Manage applications in one place**

Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | | +| **Reverse proxy across networks anywhere**

Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | | +| **Enforce identity and context aware rules**

Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | | +| **Quickly connect Pangolin sites**

Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | | -Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup). +## Get Started -- Everything you get with self hosted Pangolin, but fully managed for you. +### Check out the docs -### Managed & High Availability +We encourage everyone to read the full documentation first, which is +available at [docs.digpangolin.com](https://docs.digpangolin.com). This README provides only a very brief subset of +the docs to illustrate some basic ideas. -Managed control plane, your infrastructure +### Sign up and try now -- We manage database and control plane. -- You self-host lightweight exit-node. -- Traffic flows through your infra. -- We coordinate failover between your nodes or to Cloud when things go bad. - -Try it out using [Pangolin Cloud](https://pangolin.fossorial.io) - -### Full Enterprise On-Premises - -[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team. - -## Project Development / Roadmap - -We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests). +For Pangolin's managed service, you will first need to create an account at +[pangolin.fossorial.io](https://pangolin.fossorial.io). We have a generous free tier to get started. ## Licensing -Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). +Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://digpangolin.com/fcl.html). For inquiries about commercial licensing, please contact us at [contact@fossorial.io](mailto:contact@fossorial.io). ## Contributions -Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue. - Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. - -Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository. - -If you are looking to help with translations, please contribute [on Crowdin](https://crowdin.com/project/fossorial-pangolin) or open a PR with changes to the translations files found in `messages/`. \ No newline at end of file diff --git a/drizzle.pg.config.ts b/drizzle.pg.config.ts index 2b10d2af..febd5f45 100644 --- a/drizzle.pg.config.ts +++ b/drizzle.pg.config.ts @@ -1,16 +1,9 @@ import { defineConfig } from "drizzle-kit"; import path from "path"; -import { build } from "@server/build"; -let schema; -if (build === "oss") { - schema = [path.join("server", "db", "pg", "schema.ts")]; -} else { - schema = [ - path.join("server", "db", "pg", "schema.ts"), - path.join("server", "db", "pg", "privateSchema.ts") - ]; -} +const schema = [ + path.join("server", "db", "pg", "schema"), +]; export default defineConfig({ dialect: "postgresql", diff --git a/drizzle.sqlite.config.ts b/drizzle.sqlite.config.ts index 25bbe7f3..4912c256 100644 --- a/drizzle.sqlite.config.ts +++ b/drizzle.sqlite.config.ts @@ -1,17 +1,10 @@ -import { build } from "@server/build"; import { APP_PATH } from "@server/lib/consts"; import { defineConfig } from "drizzle-kit"; import path from "path"; -let schema; -if (build === "oss") { - schema = [path.join("server", "db", "sqlite", "schema.ts")]; -} else { - schema = [ - path.join("server", "db", "sqlite", "schema.ts"), - path.join("server", "db", "sqlite", "privateSchema.ts") - ]; -} +const schema = [ + path.join("server", "db", "sqlite", "schema"), +]; export default defineConfig({ dialect: "sqlite", diff --git a/esbuild.mjs b/esbuild.mjs index 8086a77e..7f67fe81 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -2,8 +2,9 @@ import esbuild from "esbuild"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { nodeExternalsPlugin } from "esbuild-node-externals"; +import path from "path"; +import fs from "fs"; // import { glob } from "glob"; -// import path from "path"; const banner = ` // patch __dirname @@ -18,7 +19,7 @@ const require = topLevelCreateRequire(import.meta.url); `; const argv = yargs(hideBin(process.argv)) - .usage("Usage: $0 -entry [string] -out [string]") + .usage("Usage: $0 -entry [string] -out [string] -build [string]") .option("entry", { alias: "e", describe: "Entry point file", @@ -31,6 +32,13 @@ const argv = yargs(hideBin(process.argv)) type: "string", demandOption: true, }) + .option("build", { + alias: "b", + describe: "Build type (oss, saas, enterprise)", + type: "string", + choices: ["oss", "saas", "enterprise"], + default: "oss", + }) .help() .alias("help", "h").argv; @@ -46,6 +54,179 @@ function getPackagePaths() { return ["package.json"]; } +// Plugin to guard against bad imports from #private +function privateImportGuardPlugin() { + return { + name: "private-import-guard", + setup(build) { + const violations = []; + + build.onResolve({ filter: /^#private\// }, (args) => { + const importingFile = args.importer; + + // Check if the importing file is NOT in server/private + const normalizedImporter = path.normalize(importingFile); + const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private")); + + if (!isInServerPrivate) { + const violation = { + file: importingFile, + importPath: args.path, + resolveDir: args.resolveDir + }; + violations.push(violation); + + console.log(`PRIVATE IMPORT VIOLATION:`); + console.log(` File: ${importingFile}`); + console.log(` Import: ${args.path}`); + console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`); + console.log(''); + } + + // Return null to let the default resolver handle it + return null; + }); + + build.onEnd((result) => { + if (violations.length > 0) { + console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`); + violations.forEach((v, i) => { + console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`); + }); + console.log(''); + + result.errors.push({ + text: `Private import violations detected: ${violations.length} violation(s) found`, + location: null, + notes: violations.map(v => ({ + text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`, + location: null + })) + }); + } + }); + } + }; +} + +// Plugin to guard against bad imports from #private +function dynamicImportGuardPlugin() { + return { + name: "dynamic-import-guard", + setup(build) { + const violations = []; + + build.onResolve({ filter: /^#dynamic\// }, (args) => { + const importingFile = args.importer; + + // Check if the importing file is NOT in server/private + const normalizedImporter = path.normalize(importingFile); + const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private")); + + if (isInServerPrivate) { + const violation = { + file: importingFile, + importPath: args.path, + resolveDir: args.resolveDir + }; + violations.push(violation); + + console.log(`DYNAMIC IMPORT VIOLATION:`); + console.log(` File: ${importingFile}`); + console.log(` Import: ${args.path}`); + console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`); + console.log(''); + } + + // Return null to let the default resolver handle it + return null; + }); + + build.onEnd((result) => { + if (violations.length > 0) { + console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`); + violations.forEach((v, i) => { + console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`); + }); + console.log(''); + + result.errors.push({ + text: `Dynamic import violations detected: ${violations.length} violation(s) found`, + location: null, + notes: violations.map(v => ({ + text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`, + location: null + })) + }); + } + }); + } + }; +} + +// Plugin to dynamically switch imports based on build type +function dynamicImportSwitcherPlugin(buildValue) { + return { + name: "dynamic-import-switcher", + setup(build) { + const switches = []; + + build.onStart(() => { + console.log(`Dynamic import switcher using build type: ${buildValue}`); + }); + + build.onResolve({ filter: /^#dynamic\// }, (args) => { + // Extract the path after #dynamic/ + const dynamicPath = args.path.replace(/^#dynamic\//, ''); + + // Determine the replacement based on build type + let replacement; + if (buildValue === "oss") { + replacement = `#open/${dynamicPath}`; + } else if (buildValue === "saas" || buildValue === "enterprise") { + replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private + } else { + console.warn(`Unknown build type '${buildValue}', defaulting to #open/`); + replacement = `#open/${dynamicPath}`; + } + + const switchInfo = { + file: args.importer, + originalPath: args.path, + replacementPath: replacement, + buildType: buildValue + }; + switches.push(switchInfo); + + console.log(`DYNAMIC IMPORT SWITCH:`); + console.log(` File: ${args.importer}`); + console.log(` Original: ${args.path}`); + console.log(` Switched to: ${replacement} (build: ${buildValue})`); + console.log(''); + + // Rewrite the import path and let the normal resolution continue + return build.resolve(replacement, { + importer: args.importer, + namespace: args.namespace, + resolveDir: args.resolveDir, + kind: args.kind + }); + }); + + build.onEnd((result) => { + if (switches.length > 0) { + console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`); + switches.forEach((s, i) => { + console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`); + console.log(` ${s.originalPath} → ${s.replacementPath}`); + }); + console.log(''); + } + }); + } + }; +} + esbuild .build({ entryPoints: [argv.entry], @@ -59,6 +240,9 @@ esbuild platform: "node", external: ["body-parser"], plugins: [ + privateImportGuardPlugin(), + dynamicImportGuardPlugin(), + dynamicImportSwitcherPlugin(argv.build), nodeExternalsPlugin({ packagePath: getPackagePaths(), }), @@ -66,7 +250,27 @@ esbuild sourcemap: "inline", target: "node22", }) - .then(() => { + .then((result) => { + // Check if there were any errors in the build result + if (result.errors && result.errors.length > 0) { + console.error(`Build failed with ${result.errors.length} error(s):`); + result.errors.forEach((error, i) => { + console.error(`${i + 1}. ${error.text}`); + if (error.notes) { + error.notes.forEach(note => { + console.error(` - ${note.text}`); + }); + } + }); + + // remove the output file if it was created + if (fs.existsSync(argv.out)) { + fs.unlinkSync(argv.out); + } + + process.exit(1); + } + console.log("Build completed successfully"); }) .catch((error) => { diff --git a/install/config/config.yml b/install/config/config.yml index b86f7890..c4b76057 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,15 +1,10 @@ # To see all available options, please visit the docs: -# https://docs.digpangolin.com/self-host/advanced/config-file +# https://docs.digpangolin.com/ gerbil: start_port: 51820 base_endpoint: "{{.DashboardDomain}}" -{{if .HybridMode}} -managed: - id: "{{.HybridId}}" - secret: "{{.HybridSecret}}" - -{{else}} + app: dashboard_url: "https://{{.DashboardDomain}}" log_level: "info" @@ -28,6 +23,7 @@ server: methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] allowed_headers: ["X-CSRF-Token", "Content-Type"] credentials: false + {{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}} {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" @@ -40,5 +36,4 @@ flags: require_email_verification: {{.EnableEmail}} disable_signup_without_invite: true disable_user_create_org: false - allow_raw_resources: true -{{end}} \ No newline at end of file + allow_raw_resources: true \ No newline at end of file diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 97b30317..6ec3989c 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -6,8 +6,8 @@ services: restart: unless-stopped volumes: - ./config:/app/config - - pangolin-data:/var/certificates - - pangolin-data:/var/dynamic + - pangolin-data-certificates:/var/certificates + - pangolin-data-dynamic:/var/dynamic healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"] interval: "10s" @@ -33,7 +33,7 @@ services: ports: - 51820:51820/udp - 21820:21820/udp - - 443:{{if .HybridMode}}8443{{else}}443{{end}} + - 443:443 - 80:80 {{end}} traefik: @@ -57,8 +57,8 @@ services: - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs # Shared volume for certificates and dynamic config in file mode - - pangolin-data:/var/certificates:ro - - pangolin-data:/var/dynamic:ro + - pangolin-data-certificates:/var/certificates:ro + - pangolin-data-dynamic:/var/dynamic:ro networks: default: @@ -67,4 +67,5 @@ networks: {{if .EnableIPv6}} enable_ipv6: true{{end}} volumes: - pangolin-data: + pangolin-data-dynamic: + pangolin-data-certificates: diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml index 8bb5aa6c..a9693ce6 100644 --- a/install/config/traefik/traefik_config.yml +++ b/install/config/traefik/traefik_config.yml @@ -3,17 +3,12 @@ api: dashboard: true providers: -{{if not .HybridMode}} http: endpoint: "http://pangolin:3001/api/v1/traefik-config" pollInterval: "5s" file: filename: "/etc/traefik/dynamic_config.yml" -{{else}} - file: - directory: "/var/dynamic" - watch: true -{{end}} + experimental: plugins: badger: @@ -27,7 +22,7 @@ log: maxBackups: 3 maxAge: 3 compress: true -{{if not .HybridMode}} + certificatesResolvers: letsencrypt: acme: @@ -36,22 +31,18 @@ certificatesResolvers: email: "{{.LetsEncryptEmail}}" storage: "/letsencrypt/acme.json" caServer: "https://acme-v02.api.letsencrypt.org/directory" -{{end}} + entryPoints: web: address: ":80" websecure: address: ":443" -{{if .HybridMode}} proxyProtocol: - trustedIPs: - - 0.0.0.0/0 - - ::1/128{{end}} transport: respondingTimeouts: readTimeout: "30m" -{{if not .HybridMode}} http: + http: tls: - certResolver: "letsencrypt"{{end}} + certResolver: "letsencrypt" serversTransport: insecureSkipVerify: true diff --git a/install/main.go b/install/main.go index f16dc214..72ffbac0 100644 --- a/install/main.go +++ b/install/main.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "bytes" "embed" "fmt" "io" @@ -48,10 +47,8 @@ type Config struct { InstallGerbil bool TraefikBouncerKey string DoCrowdsecInstall bool + EnableGeoblocking bool Secret string - HybridMode bool - HybridId string - HybridSecret string } type SupportedContainer string @@ -98,24 +95,6 @@ func main() { fmt.Println("\n=== Generating Configuration Files ===") - // If the secret and id are not generated then generate them - if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") { - // fmt.Println("Requesting hybrid credentials from cloud...") - credentials, err := requestHybridCredentials() - if err != nil { - fmt.Printf("Error requesting hybrid credentials: %v\n", err) - fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.") - os.Exit(1) - } - config.HybridId = credentials.RemoteExitNodeId - config.HybridSecret = credentials.Secret - fmt.Printf("Your managed credentials have been obtained successfully.\n") - fmt.Printf(" ID: %s\n", config.HybridId) - fmt.Printf(" Secret: %s\n", config.HybridSecret) - fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.") - readBool(reader, "Have you adopted your node?", true) - } - if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) @@ -125,6 +104,15 @@ func main() { fmt.Println("\nConfiguration files created successfully!") + // Download MaxMind database if requested + if config.EnableGeoblocking { + fmt.Println("\n=== Downloading MaxMind Database ===") + if err := downloadMaxMindDatabase(); err != nil { + fmt.Printf("Error downloading MaxMind database: %v\n", err) + fmt.Println("You can download it manually later if needed.") + } + } + fmt.Println("\n=== Starting installation ===") if readBool(reader, "Would you like to install and start the containers?", true) { @@ -172,9 +160,34 @@ func main() { } else { alreadyInstalled = true fmt.Println("Looks like you already installed Pangolin!") + + // Check if MaxMind database exists and offer to update it + fmt.Println("\n=== MaxMind Database Update ===") + if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil { + fmt.Println("MaxMind GeoLite2 Country database found.") + if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) { + if err := downloadMaxMindDatabase(); err != nil { + fmt.Printf("Error updating MaxMind database: %v\n", err) + fmt.Println("You can try updating it manually later if needed.") + } + } + } else { + fmt.Println("MaxMind GeoLite2 Country database not found.") + if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) { + if err := downloadMaxMindDatabase(); err != nil { + fmt.Printf("Error downloading MaxMind database: %v\n", err) + fmt.Println("You can try downloading it manually later if needed.") + } + // Now you need to update your config file accordingly to enable geoblocking + fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n") + // add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server + fmt.Println("Add the following line under the 'server' section:") + fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"") + } + } } - if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() { + if !checkIsCrowdsecInstalledInCompose() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed if readBool(reader, "Would you like to install CrowdSec?", false) { @@ -230,7 +243,7 @@ func main() { } } - if !config.HybridMode && !alreadyInstalled { + if !alreadyInstalled { // Setup Token Section fmt.Println("\n=== Setup Token ===") @@ -251,9 +264,7 @@ func main() { fmt.Println("\nInstallation complete!") - if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() { - fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) - } + fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } func podmanOrDocker(reader *bufio.Reader) SupportedContainer { @@ -328,66 +339,38 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") - for { - response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "") - if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") { - config.HybridMode = true - break - } else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") { - config.HybridMode = false - break - } - fmt.Println("Please answer 'yes' or 'no'") + + config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") + + // Set default dashboard domain after base domain is collected + defaultDashboardDomain := "" + if config.BaseDomain != "" { + defaultDashboardDomain = "pangolin." + config.BaseDomain + } + config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) + config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") + config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) + + // Email configuration + fmt.Println("\n=== Email Configuration ===") + config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) + + if config.EnableEmail { + config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") + config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) + config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") + config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? + config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } - if config.HybridMode { - alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) - - if alreadyHaveCreds { - config.HybridId = readString(reader, "Enter your ID", "") - config.HybridSecret = readString(reader, "Enter your secret", "") - } - - // Try to get public IP as default - publicIP := getPublicIP() - if publicIP != "" { - fmt.Printf("Detected public IP: %s\n", publicIP) - } - config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP) - config.InstallGerbil = true - } else { - config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") - - // Set default dashboard domain after base domain is collected - defaultDashboardDomain := "" - if config.BaseDomain != "" { - defaultDashboardDomain = "pangolin." + config.BaseDomain - } - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) - config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") - config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) - - // Email configuration - fmt.Println("\n=== Email Configuration ===") - config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) - - if config.EnableEmail { - config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") - config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) - config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") - config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? - config.EmailNoReply = readString(reader, "Enter no-reply email address", "") - } - - // Validate required fields - if config.BaseDomain == "" { - fmt.Println("Error: Domain name is required") - os.Exit(1) - } - if config.LetsEncryptEmail == "" { - fmt.Println("Error: Let's Encrypt email is required") - os.Exit(1) - } + // Validate required fields + if config.BaseDomain == "" { + fmt.Println("Error: Domain name is required") + os.Exit(1) + } + if config.LetsEncryptEmail == "" { + fmt.Println("Error: Let's Encrypt email is required") + os.Exit(1) } // Advanced configuration @@ -395,6 +378,7 @@ func collectUserInput(reader *bufio.Reader) Config { fmt.Println("\n=== Advanced Configuration ===") config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) + config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", false) if config.DashboardDomain == "" { fmt.Println("Error: Dashboard Domain name is required") @@ -429,11 +413,6 @@ func createConfigFiles(config Config) error { return nil } - // the hybrid does not need the dynamic config - if config.HybridMode && strings.Contains(path, "dynamic_config.yml") { - return nil - } - // skip .DS_Store if strings.Contains(path, ".DS_Store") { return nil @@ -663,18 +642,30 @@ func checkPortsAvailable(port int) error { return nil } -func checkIsPangolinInstalledWithHybrid() bool { - // Check if config/config.yml exists and contains hybrid section - if _, err := os.Stat("config/config.yml"); err != nil { - return false +func downloadMaxMindDatabase() error { + fmt.Println("Downloading MaxMind GeoLite2 Country database...") + + // Download the GeoLite2 Country database + if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz", + "https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil { + return fmt.Errorf("failed to download GeoLite2 database: %v", err) } - - // Read config file to check for hybrid section - content, err := os.ReadFile("config/config.yml") - if err != nil { - return false + + // Extract the database + if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil { + return fmt.Errorf("failed to extract GeoLite2 database: %v", err) } - - // Check for hybrid section - return bytes.Contains(content, []byte("managed:")) + + // Find the .mmdb file and move it to the config directory + if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil { + return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err) + } + + // Clean up the downloaded files + if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil { + fmt.Printf("Warning: failed to clean up temporary files: %v\n", err) + } + + fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!") + return nil } diff --git a/install/quickStart.go b/install/quickStart.go deleted file mode 100644 index ece8e8ff..00000000 --- a/install/quickStart.go +++ /dev/null @@ -1,110 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -const ( - FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e" - // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" - CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" -) - -// HybridCredentials represents the response from the cloud API -type HybridCredentials struct { - RemoteExitNodeId string `json:"remoteExitNodeId"` - Secret string `json:"secret"` -} - -// APIResponse represents the full response structure from the cloud API -type APIResponse struct { - Data HybridCredentials `json:"data"` -} - -// RequestPayload represents the request body structure -type RequestPayload struct { - Token string `json:"token"` -} - -func generateValidationToken() string { - timestamp := time.Now().UnixMilli() - data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp) - obfuscated := make([]byte, len(data)) - for i, char := range []byte(data) { - obfuscated[i] = char + 5 - } - return base64.StdEncoding.EncodeToString(obfuscated) -} - -// requestHybridCredentials makes an HTTP POST request to the cloud API -// to get hybrid credentials (ID and secret) -func requestHybridCredentials() (*HybridCredentials, error) { - // Generate validation token - token := generateValidationToken() - - // Create request payload - payload := RequestPayload{ - Token: token, - } - - // Marshal payload to JSON - jsonData, err := json.Marshal(payload) - if err != nil { - return nil, fmt.Errorf("failed to marshal request payload: %v", err) - } - - // Create HTTP request - req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create HTTP request: %v", err) - } - - // Set headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-CSRF-Token", "x-csrf-protection") - - // Create HTTP client with timeout - client := &http.Client{ - Timeout: 30 * time.Second, - } - - // Make the request - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make HTTP request: %v", err) - } - defer resp.Body.Close() - - // Check response status - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) - } - - // Read response body for debugging - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) - } - - // Print the raw JSON response for debugging - // fmt.Printf("Raw JSON response: %s\n", string(body)) - - // Parse response - var apiResponse APIResponse - if err := json.Unmarshal(body, &apiResponse); err != nil { - return nil, fmt.Errorf("failed to decode API response: %v", err) - } - - // Validate response data - if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" { - return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret") - } - - return &apiResponse.Data, nil -} diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 9cc9e9f0..74bf5d87 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Администратор на сървър - Панголин", "licenseTierProfessional": "Професионален лиценз", "licenseTierEnterprise": "Предприятие лиценз", - "licenseTierCommercial": "Търговски лиценз", "licensed": "Лицензиран", "yes": "Да", "no": "Не", @@ -1084,7 +1083,6 @@ "navbar": "Навигационно меню", "navbarDescription": "Главно навигационно меню за приложението", "navbarDocsLink": "Документация", - "commercialEdition": "Търговско издание", "otpErrorEnable": "Не може да се активира 2FA", "otpErrorEnableDescription": "Възникна грешка при активиране на 2FA", "otpSetupCheckCode": "Моля, въведете 6-цифрен код", diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index f1106fb8..9d573022 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Správce serveru - Pangolin", "licenseTierProfessional": "Profesionální licence", "licenseTierEnterprise": "Podniková licence", - "licenseTierCommercial": "Obchodní licence", "licensed": "Licencováno", "yes": "Ano", "no": "Ne", @@ -1084,7 +1083,6 @@ "navbar": "Navigation Menu", "navbarDescription": "Hlavní navigační menu aplikace", "navbarDocsLink": "Dokumentace", - "commercialEdition": "Obchodní vydání", "otpErrorEnable": "2FA nelze povolit", "otpErrorEnableDescription": "Došlo k chybě při povolování 2FA", "otpSetupCheckCode": "Zadejte 6místný kód", diff --git a/messages/de-DE.json b/messages/de-DE.json index 5ac9fc02..eaca92bf 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Server-Admin - Pangolin", "licenseTierProfessional": "Professional Lizenz", "licenseTierEnterprise": "Enterprise Lizenz", - "licenseTierCommercial": "Gewerbliche Lizenz", "licensed": "Lizenziert", "yes": "Ja", "no": "Nein", @@ -1084,7 +1083,6 @@ "navbar": "Navigationsmenü", "navbarDescription": "Hauptnavigationsmenü für die Anwendung", "navbarDocsLink": "Dokumentation", - "commercialEdition": "Kommerzielle Edition", "otpErrorEnable": "2FA konnte nicht aktiviert werden", "otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten", "otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein", diff --git a/messages/en-US.json b/messages/en-US.json index c27f067e..09b8734d 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -96,7 +96,7 @@ "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", "siteLocalDescription": "Local resources only. No tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "See All Sites", "siteTunnelDescription": "Determine how you want to connect to your site", "siteNewtCredentials": "Newt Credentials", @@ -468,7 +468,10 @@ "createdAt": "Created At", "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", - "proxyEnableSSL": "Enable SSL (https)", + "proxyEnableSSL": "Enable SSL", + "proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.", + "target": "Target", + "configureTarget": "Configure Targets", "targetErrorFetch": "Failed to fetch targets", "targetErrorFetchDescription": "An error occurred while fetching targets", "siteErrorFetch": "Failed to fetch resource", @@ -495,7 +498,7 @@ "targetTlsSettings": "Secure Connection Configuration", "targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource", "targetTlsSettingsAdvanced": "Advanced TLS Settings", - "targetTlsSni": "TLS Server Name (SNI)", + "targetTlsSni": "TLS Server Name", "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSubmit": "Save Settings", "targets": "Targets Configuration", @@ -504,9 +507,21 @@ "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "methodSelect": "Select method", "targetSubmit": "Add Target", - "targetNoOne": "No targets. Add a target using the form.", + "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.", "targetNoOneDescription": "Adding more than one target above will enable load balancing.", "targetsSubmit": "Save Targets", + "addTarget": "Add Target", + "targetErrorInvalidIp": "Invalid IP address", + "targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname", + "targetErrorInvalidPort": "Invalid port", + "targetErrorInvalidPortDescription": "Please enter a valid port number", + "targetErrorNoSite": "No site selected", + "targetErrorNoSiteDescription": "Please select a site for the target", + "targetCreated": "Target created", + "targetCreatedDescription": "Target has been created successfully", + "targetErrorCreate": "Failed to create target", + "targetErrorCreateDescription": "An error occurred while creating the target", + "save": "Save", "proxyAdditional": "Additional Proxy Settings", "proxyAdditionalDescription": "Configure how your resource handles proxy settings", "proxyCustomHeader": "Custom Host Header", @@ -715,7 +730,7 @@ "pangolinServerAdmin": "Server Admin - Pangolin", "licenseTierProfessional": "Professional License", "licenseTierEnterprise": "Enterprise License", - "licenseTierCommercial": "Commercial License", + "licenseTierPersonal": "Personal License", "licensed": "Licensed", "yes": "Yes", "no": "No", @@ -750,7 +765,7 @@ "idpDisplayName": "A display name for this identity provider", "idpAutoProvisionUsers": "Auto Provision Users", "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", - "licenseBadge": "Professional", + "licenseBadge": "EE", "idpType": "Provider Type", "idpTypeDescription": "Select the type of identity provider you want to configure", "idpOidcConfigure": "OAuth2/OIDC Configuration", @@ -1084,7 +1099,6 @@ "navbar": "Navigation Menu", "navbarDescription": "Main navigation menu for the application", "navbarDocsLink": "Documentation", - "commercialEdition": "Commercial Edition", "otpErrorEnable": "Unable to enable 2FA", "otpErrorEnableDescription": "An error occurred while enabling 2FA", "otpSetupCheckCode": "Please enter a 6-digit code", @@ -1140,7 +1154,7 @@ "sidebarAllUsers": "All Users", "sidebarIdentityProviders": "Identity Providers", "sidebarLicense": "License", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Domains", "enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", @@ -1333,7 +1347,6 @@ "twoFactorRequired": "Two-factor authentication is required to register a security key.", "twoFactor": "Two-Factor Authentication", "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", - "continueToApplication": "Continue to Application", "securityKeyAdd": "Add Security Key", "securityKeyRegisterTitle": "Register New Security Key", "securityKeyRegisterDescription": "Connect your security key and enter a name to identify it", @@ -1411,6 +1424,7 @@ "externalProxyEnabled": "External Proxy Enabled", "addNewTarget": "Add New Target", "targetsList": "Targets List", + "advancedMode": "Advanced Mode", "targetErrorDuplicateTargetFound": "Duplicate target found", "healthCheckHealthy": "Healthy", "healthCheckUnhealthy": "Unhealthy", @@ -1543,8 +1557,8 @@ "autoLoginError": "Auto Login Error", "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", - "remoteExitNodeManageRemoteExitNodes": "Manage Self-Hosted", - "remoteExitNodeDescription": "Manage nodes to extend your network connectivity", + "remoteExitNodeManageRemoteExitNodes": "Remote Nodes", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", "remoteExitNodes": "Nodes", "searchRemoteExitNodes": "Search nodes...", "remoteExitNodeAdd": "Add Node", @@ -1554,7 +1568,7 @@ "remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.", "remoteExitNodeConfirmDelete": "Confirm Delete Node", "remoteExitNodeDelete": "Delete Node", - "sidebarRemoteExitNodes": "Nodes", + "sidebarRemoteExitNodes": "Remote Nodes", "remoteExitNodeCreate": { "title": "Create Node", "description": "Create a new node to extend your network connectivity", @@ -1724,20 +1738,160 @@ "healthCheckNotAvailable": "Local", "rewritePath": "Rewrite Path", "rewritePathDescription": "Optionally rewrite the path before forwarding to the target.", + "continueToApplication": "Continue to application", + "checkingInvite": "Checking Invite", "setResourceHeaderAuth": "setResourceHeaderAuth", "resourceHeaderAuthRemove": "Remove Header Auth", "resourceHeaderAuthRemoveDescription": "Header authentication removed successfully.", "resourceErrorHeaderAuthRemove": "Failed to remove Header Authentication", "resourceErrorHeaderAuthRemoveDescription": "Could not remove header authentication for the resource.", - "resourceHeaderAuthProtection": "Header Authentication Protection: {{status}}", - "headerAuthRemove": "Remove", - "headerAuthAdd": "Add", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", "resourceErrorHeaderAuthSetup": "Failed to set Header Authentication", "resourceErrorHeaderAuthSetupDescription": "Could not set header authentication for the resource.", "resourceHeaderAuthSetup": "Header Authentication set successfully", "resourceHeaderAuthSetupDescription": "Header authentication has been successfully set.", "resourceHeaderAuthSetupTitle": "Set Header Authentication", - "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Leave both fields blank to remove existing header authentication.", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", "resourceHeaderAuthSubmit": "Set Header Authentication", - "actionSetResourceHeaderAuth": "Set Header Authentication" -} \ No newline at end of file + "actionSetResourceHeaderAuth": "Set Header Authentication", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Priority", + "priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" +} diff --git a/messages/es-ES.json b/messages/es-ES.json index a1b92f8b..2e7cf00a 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Admin Servidor - Pangolin", "licenseTierProfessional": "Licencia profesional", "licenseTierEnterprise": "Licencia Enterprise", - "licenseTierCommercial": "Licencia comercial", "licensed": "Licenciado", "yes": "Sí", "no": "Nu", @@ -1084,7 +1083,6 @@ "navbar": "Menú de navegación", "navbarDescription": "Menú de navegación principal para la aplicación", "navbarDocsLink": "Documentación", - "commercialEdition": "Edición Comercial", "otpErrorEnable": "No se puede habilitar 2FA", "otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA", "otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 6028ab5b..4a1670f3 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Admin Serveur - Pangolin", "licenseTierProfessional": "Licence Professionnelle", "licenseTierEnterprise": "Licence Entreprise", - "licenseTierCommercial": "Licence commerciale", "licensed": "Sous licence", "yes": "Oui", "no": "Non", @@ -1084,7 +1083,6 @@ "navbar": "Menu de navigation", "navbarDescription": "Menu de navigation principal de l'application", "navbarDocsLink": "Documentation", - "commercialEdition": "Édition Commerciale", "otpErrorEnable": "Impossible d'activer l'A2F", "otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F", "otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres", diff --git a/messages/it-IT.json b/messages/it-IT.json index ca22ba63..143da0c5 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Server Admin - Pangolina", "licenseTierProfessional": "Licenza Professional", "licenseTierEnterprise": "Licenza Enterprise", - "licenseTierCommercial": "Licenza Commerciale", "licensed": "Con Licenza", "yes": "Sì", "no": "No", @@ -1084,7 +1083,6 @@ "navbar": "Menu di Navigazione", "navbarDescription": "Menu di navigazione principale dell'applicazione", "navbarDocsLink": "Documentazione", - "commercialEdition": "Edizione Commerciale", "otpErrorEnable": "Impossibile abilitare 2FA", "otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA", "otpSetupCheckCode": "Inserisci un codice a 6 cifre", diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 3d010cd5..8ace81a5 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "서버 관리자 - 판골린", "licenseTierProfessional": "전문 라이센스", "licenseTierEnterprise": "기업 라이선스", - "licenseTierCommercial": "상업용 라이선스", "licensed": "라이센스", "yes": "예", "no": "아니요", @@ -1084,7 +1083,6 @@ "navbar": "탐색 메뉴", "navbarDescription": "애플리케이션의 주요 탐색 메뉴", "navbarDocsLink": "문서", - "commercialEdition": "상업용 에디션", "otpErrorEnable": "2FA를 활성화할 수 없습니다.", "otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다", "otpSetupCheckCode": "6자리 코드를 입력하세요", diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 84dc5266..f7f0d3ab 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Server Admin - Pangolin", "licenseTierProfessional": "Profesjonell lisens", "licenseTierEnterprise": "Bedriftslisens", - "licenseTierCommercial": "Kommersiell lisens", "licensed": "Lisensiert", "yes": "Ja", "no": "Nei", @@ -1084,7 +1083,6 @@ "navbar": "Navigasjonsmeny", "navbarDescription": "Hovednavigasjonsmeny for applikasjonen", "navbarDocsLink": "Dokumentasjon", - "commercialEdition": "Kommersiell utgave", "otpErrorEnable": "Kunne ikke aktivere 2FA", "otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA", "otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode", diff --git a/messages/nl-NL.json b/messages/nl-NL.json index fb82fbb6..34d1c811 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Serverbeheer - Pangolin", "licenseTierProfessional": "Professionele licentie", "licenseTierEnterprise": "Enterprise Licentie", - "licenseTierCommercial": "Commerciële licentie", "licensed": "Gelicentieerd", "yes": "ja", "no": "Neen", @@ -1084,7 +1083,6 @@ "navbar": "Navigatiemenu", "navbarDescription": "Hoofd navigatie menu voor de applicatie", "navbarDocsLink": "Documentatie", - "commercialEdition": "Commerciële editie", "otpErrorEnable": "Kan 2FA niet inschakelen", "otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA", "otpSetupCheckCode": "Voer een 6-cijferige code in", diff --git a/messages/pl-PL.json b/messages/pl-PL.json index c3db35f5..08287ed9 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Administrator serwera - Pangolin", "licenseTierProfessional": "Licencja Professional", "licenseTierEnterprise": "Licencja Enterprise", - "licenseTierCommercial": "Licencja handlowa", "licensed": "Licencjonowany", "yes": "Tak", "no": "Nie", @@ -1084,7 +1083,6 @@ "navbar": "Menu nawigacyjne", "navbarDescription": "Główne menu nawigacyjne aplikacji", "navbarDocsLink": "Dokumentacja", - "commercialEdition": "Edycja komercyjna", "otpErrorEnable": "Nie można włączyć 2FA", "otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA", "otpSetupCheckCode": "Wprowadź 6-cyfrowy kod", diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 1d61c581..5a45eedd 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Administrador do Servidor - Pangolin", "licenseTierProfessional": "Licença Profissional", "licenseTierEnterprise": "Licença Empresarial", - "licenseTierCommercial": "Licença comercial", "licensed": "Licenciado", "yes": "Sim", "no": "Não", @@ -1084,7 +1083,6 @@ "navbar": "Menu de Navegação", "navbarDescription": "Menu de navegação principal da aplicação", "navbarDocsLink": "Documentação", - "commercialEdition": "Edição Comercial", "otpErrorEnable": "Não foi possível ativar 2FA", "otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA", "otpSetupCheckCode": "Por favor, insira um código de 6 dígitos", diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 80be36dd..c41215d2 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Администратор сервера - Pangolin", "licenseTierProfessional": "Профессиональная лицензия", "licenseTierEnterprise": "Корпоративная лицензия", - "licenseTierCommercial": "Коммерческая лицензия", "licensed": "Лицензировано", "yes": "Да", "no": "Нет", @@ -1084,7 +1083,6 @@ "navbar": "Навигационное меню", "navbarDescription": "Главное навигационное меню приложения", "navbarDocsLink": "Документация", - "commercialEdition": "Коммерческая версия", "otpErrorEnable": "Невозможно включить 2FA", "otpErrorEnableDescription": "Произошла ошибка при включении 2FA", "otpSetupCheckCode": "Пожалуйста, введите 6-значный код", diff --git a/messages/tr-TR.json b/messages/tr-TR.json index b84bef19..6296b7fe 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin", "licenseTierProfessional": "Profesyonel Lisans", "licenseTierEnterprise": "Kurumsal Lisans", - "licenseTierCommercial": "Ticari Lisans", "licensed": "Lisanslı", "yes": "Evet", "no": "Hayır", @@ -1084,7 +1083,6 @@ "navbar": "Navigasyon Menüsü", "navbarDescription": "Uygulamanın ana navigasyon menüsü", "navbarDocsLink": "Dokümantasyon", - "commercialEdition": "Ticari Sürüm", "otpErrorEnable": "2FA etkinleştirilemedi", "otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu", "otpSetupCheckCode": "6 haneli bir kod girin", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 4f1d9c14..a8f578db 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -715,7 +715,6 @@ "pangolinServerAdmin": "服务器管理员 - Pangolin", "licenseTierProfessional": "专业许可证", "licenseTierEnterprise": "企业许可证", - "licenseTierCommercial": "商业许可证", "licensed": "已授权", "yes": "是", "no": "否", @@ -1084,7 +1083,6 @@ "navbar": "导航菜单", "navbarDescription": "应用程序的主导航菜单", "navbarDocsLink": "文件", - "commercialEdition": "商业版", "otpErrorEnable": "无法启用 2FA", "otpErrorEnableDescription": "启用 2FA 时出错", "otpSetupCheckCode": "请输入您的6位数字代码", diff --git a/package-lock.json b/package-lock.json index 2661a0f3..50bc095a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -447,51 +447,51 @@ } }, "node_modules/@aws-sdk/client-sesv2": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.899.0.tgz", - "integrity": "sha512-aMs3QgB9lWaKKrnx9KhIopoeXLNzI/sqdp5M56j30jlBD4vqdcCzW2OwFAAs26QzUgNKOOSY+iLZcE9DUDdIvg==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.901.0.tgz", + "integrity": "sha512-xCS2qZlvgbXKZbJW8XgU8OEAL7BJyVqJ5yODOQxa1TJFZ/+wEhik9XZtULjNnQqa29sJDpPltuSDG1aDG2OUxQ==", "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.899.0", - "@aws-sdk/credential-provider-node": "3.899.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.899.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/signature-v4-multi-region": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.899.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.13.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.5", - "@smithy/middleware-retry": "^4.3.1", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.5", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.5", - "@smithy/util-defaults-mode-node": "^4.1.5", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/credential-provider-node": "3.901.0", + "@aws-sdk/middleware-host-header": "3.901.0", + "@aws-sdk/middleware-logger": "3.901.0", + "@aws-sdk/middleware-recursion-detection": "3.901.0", + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/region-config-resolver": "3.901.0", + "@aws-sdk/signature-v4-multi-region": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@aws-sdk/util-user-agent-browser": "3.901.0", + "@aws-sdk/util-user-agent-node": "3.901.0", + "@smithy/config-resolver": "^4.3.0", + "@smithy/core": "^3.14.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/hash-node": "^4.2.0", + "@smithy/invalid-dependency": "^4.2.0", + "@smithy/middleware-content-length": "^4.2.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-retry": "^4.4.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.0", + "@smithy/util-defaults-mode-browser": "^4.2.0", + "@smithy/util-defaults-mode-node": "^4.2.0", + "@smithy/util-endpoints": "^3.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -499,49 +499,49 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.899.0.tgz", - "integrity": "sha512-EKz/iiVDv2OC8/3ONcXG3+rhphx9Heh7KXQdsZzsAXGVn6mWtrHQLrWjgONckmK4LrD07y4+5WlJlGkMxSMA5A==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.901.0.tgz", + "integrity": "sha512-sGyDjjkJ7ppaE+bAKL/Q5IvVCxtoyBIzN+7+hWTS/mUxWJ9EOq9238IqmVIIK6sYNIzEf9yhobfMARasPYVTNg==", "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.899.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.899.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.899.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.13.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.5", - "@smithy/middleware-retry": "^4.3.1", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.5", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.5", - "@smithy/util-defaults-mode-node": "^4.1.5", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/middleware-host-header": "3.901.0", + "@aws-sdk/middleware-logger": "3.901.0", + "@aws-sdk/middleware-recursion-detection": "3.901.0", + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/region-config-resolver": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@aws-sdk/util-user-agent-browser": "3.901.0", + "@aws-sdk/util-user-agent-node": "3.901.0", + "@smithy/config-resolver": "^4.3.0", + "@smithy/core": "^3.14.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/hash-node": "^4.2.0", + "@smithy/invalid-dependency": "^4.2.0", + "@smithy/middleware-content-length": "^4.2.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-retry": "^4.4.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.0", + "@smithy/util-defaults-mode-browser": "^4.2.0", + "@smithy/util-defaults-mode-node": "^4.2.0", + "@smithy/util-endpoints": "^3.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -549,24 +549,24 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.899.0.tgz", - "integrity": "sha512-Enp5Zw37xaRlnscyaelaUZNxVqyE3CTS8gjahFbW2bbzVtRD2itHBVgq8A3lvKiFb7Feoxa71aTe0fQ1I6AhQQ==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.901.0.tgz", + "integrity": "sha512-brKAc3y64tdhyuEf+OPIUln86bRTqkLgb9xkd6kUdIeA5+qmp/N6amItQz+RN4k4O3kqkCPYnAd3LonTKluobw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@aws-sdk/xml-builder": "3.894.0", - "@smithy/core": "^3.13.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.5", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/xml-builder": "3.901.0", + "@smithy/core": "^3.14.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/signature-v4": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -574,16 +574,16 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.899.0.tgz", - "integrity": "sha512-wXQ//KQ751EFhUbdfoL/e2ZDaM8l2Cff+hVwFcj32yiZyeCMhnoLRMQk2euAaUOugqPY5V5qesFbHhISbIedtw==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.901.0.tgz", + "integrity": "sha512-5hAdVl3tBuARh3zX5MLJ1P/d+Kr5kXtDU3xm1pxUEF4xt2XkEEpwiX5fbkNkz2rbh3BCt2gOHsAbh6b3M7n+DA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -591,21 +591,21 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.899.0.tgz", - "integrity": "sha512-/rRHyJFdnPrupjt/1q/PxaO6O26HFsguVUJSUeMeGUWLy0W8OC3slLFDNh89CgTqnplCyt1aLFMCagRM20HjNQ==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.901.0.tgz", + "integrity": "sha512-Ggr7+0M6QZEsrqRkK7iyJLf4LkIAacAxHz9c4dm9hnDdU7vqrlJm6g73IxMJXWN1bIV7IxfpzB11DsRrB/oNjQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.5", - "@smithy/types": "^4.5.0", - "@smithy/util-stream": "^4.3.2", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-stream": "^4.4.0", "tslib": "^2.6.2" }, "engines": { @@ -613,24 +613,24 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.899.0.tgz", - "integrity": "sha512-B8oFNFTDV0j1yiJiqzkC2ybml+theNnmsLrTLBhJbnBLWkxEcmVGKVIMnATW9BUCBhHmEtDiogdNIzSwP8tbMw==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.901.0.tgz", + "integrity": "sha512-zxadcDS0hNJgv8n4hFYJNOXyfjaNE1vvqIiF/JzZSQpSSYXzCd+WxXef5bQh+W3giDtRUmkvP5JLbamEFjZKyw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/credential-provider-env": "3.899.0", - "@aws-sdk/credential-provider-http": "3.899.0", - "@aws-sdk/credential-provider-process": "3.899.0", - "@aws-sdk/credential-provider-sso": "3.899.0", - "@aws-sdk/credential-provider-web-identity": "3.899.0", - "@aws-sdk/nested-clients": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/credential-provider-imds": "^4.1.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/credential-provider-env": "3.901.0", + "@aws-sdk/credential-provider-http": "3.901.0", + "@aws-sdk/credential-provider-process": "3.901.0", + "@aws-sdk/credential-provider-sso": "3.901.0", + "@aws-sdk/credential-provider-web-identity": "3.901.0", + "@aws-sdk/nested-clients": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/credential-provider-imds": "^4.2.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -638,23 +638,23 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.899.0.tgz", - "integrity": "sha512-nHBnZ2ZCOqTGJ2A9xpVj8iK6+WV+j0JNv3XGEkIuL4mqtGEPJlEex/0mD/hqc1VF8wZzojji2OQ3892m1mUOSA==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.901.0.tgz", + "integrity": "sha512-dPuFzMF7L1s/lQyT3wDxqLe82PyTH+5o1jdfseTEln64LJMl0ZMWaKX/C1UFNDxaTd35Cgt1bDbjjAWHMiKSFQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.899.0", - "@aws-sdk/credential-provider-http": "3.899.0", - "@aws-sdk/credential-provider-ini": "3.899.0", - "@aws-sdk/credential-provider-process": "3.899.0", - "@aws-sdk/credential-provider-sso": "3.899.0", - "@aws-sdk/credential-provider-web-identity": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/credential-provider-imds": "^4.1.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/credential-provider-env": "3.901.0", + "@aws-sdk/credential-provider-http": "3.901.0", + "@aws-sdk/credential-provider-ini": "3.901.0", + "@aws-sdk/credential-provider-process": "3.901.0", + "@aws-sdk/credential-provider-sso": "3.901.0", + "@aws-sdk/credential-provider-web-identity": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/credential-provider-imds": "^4.2.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -662,17 +662,17 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.899.0.tgz", - "integrity": "sha512-1PWSejKcJQUKBNPIqSHlEW4w8vSjmb+3kNJqCinJybjp5uP5BJgBp6QNcb8Nv30VBM0bn3ajVd76LCq4ZshQAw==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.901.0.tgz", + "integrity": "sha512-/IWgmgM3Cl1wTdJA5HqKMAojxLkYchh5kDuphApxKhupLu6Pu0JBOHU8A5GGeFvOycyaVwosod6zDduINZxe+A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -680,19 +680,19 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.899.0.tgz", - "integrity": "sha512-URlMbo74CAhIGrhzEP2fw5F5Tt6MRUctA8aa88MomlEHCEbJDsMD3nh6qoXxwR3LyvEBFmCWOZ/1TWmAjMsSdA==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.901.0.tgz", + "integrity": "sha512-SjmqZQHmqFSET7+6xcZgtH7yEyh5q53LN87GqwYlJZ6KJ5oNw11acUNEhUOL1xTSJEvaWqwTIkS2zqrzLcM9bw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.899.0", - "@aws-sdk/core": "3.899.0", - "@aws-sdk/token-providers": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/client-sso": "3.901.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/token-providers": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -700,66 +700,18 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.899.0.tgz", - "integrity": "sha512-UEn5o5FMcbeFPRRkJI6VCrgdyR9qsLlGA7+AKCYuYADsKbvJGIIQk6A2oD82vIVvLYD3TtbTLDLsF7haF9mpbw==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.901.0.tgz", + "integrity": "sha512-NYjy/6NLxH9m01+pfpB4ql8QgAorJcu8tw69kzHwUd/ql6wUDTbC7HcXqtKlIwWjzjgj2BKL7j6SyFapgCuafA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/nested-clients": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz", - "integrity": "sha512-qL5xYRt80ahDfj9nDYLhpCNkDinEXvjLe/Qen/Y/u12+djrR2MB4DRa6mzBCkLkdXDtf0WAoW2EZsNCfGrmOEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-logger": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz", - "integrity": "sha512-ZqzMecjju5zkBquSIfVfCORI/3Mge21nUY4nWaGQy+NUXehqCGG4W7AiVpiHGOcY2cGJa7xeEkYcr2E2U9U0AA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.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.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.893.0.tgz", - "integrity": "sha512-H7Zotd9zUHQAr/wr3bcWHULYhEeoQrF54artgsoUGIf/9emv6LzY89QUccKIxYd6oHKNTrTyXm9F0ZZrzXNxlg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@aws/lambda-invoke-store": "^0.0.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/nested-clients": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -767,25 +719,25 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.899.0.tgz", - "integrity": "sha512-/3/EIRSwQ5CNOSTHx96gVGzzmTe46OxcPG5FTgM6i9ZD+K/Q3J/UPGFL5DPzct5fXiSLvD1cGQitWHStVDjOVQ==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.901.0.tgz", + "integrity": "sha512-prgjVC3fDT2VIlmQPiw/cLee8r4frTam9GILRUVQyDdNtshNwV3MiaSCLzzQJjKJlLgnBLNUHJCSmvUVtg+3iA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/types": "3.893.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.13.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.5", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.2", - "@smithy/util-utf8": "^4.1.0", + "@smithy/core": "^3.14.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/signature-v4": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-stream": "^4.4.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -793,18 +745,18 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.899.0.tgz", - "integrity": "sha512-6EsVCC9j1VIyVyLOg+HyO3z9L+c0PEwMiHe3kuocoMf8nkfjSzJfIl6zAtgAXWgP5MKvusTP2SUbS9ezEEHZ+A==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.901.0.tgz", + "integrity": "sha512-Zby4F03fvD9xAgXGPywyk4bC1jCbnyubMEYChLYohD+x20ULQCf+AimF/Btn7YL+hBpzh1+RmqmvZcx+RgwgNQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@smithy/core": "^3.13.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@smithy/core": "^3.14.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -812,67 +764,49 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.899.0.tgz", - "integrity": "sha512-ySXXsFO0RH28VISEqvCuPZ78VAkK45/+OCIJgPvYpcCX9CVs70XSvMPXDI46I49mudJ1s4H3IUKccYSEtA+jaw==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.901.0.tgz", + "integrity": "sha512-feAAAMsVwctk2Tms40ONybvpfJPLCmSdI+G+OTrNpizkGLNl6ik2Ng2RzxY6UqOfN8abqKP/DOUj1qYDRDG8ag==", "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.899.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.899.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.899.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.13.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.5", - "@smithy/middleware-retry": "^4.3.1", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.5", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.5", - "@smithy/util-defaults-mode-node": "^4.1.5", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.893.0.tgz", - "integrity": "sha512-/cJvh3Zsa+Of0Zbg7vl9wp/kZtdb40yk/2+XcroAMVPO9hPvmS9r/UOm6tO7FeX4TtkRFwWaQJiTZTgSdsPY+Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/middleware-host-header": "3.901.0", + "@aws-sdk/middleware-logger": "3.901.0", + "@aws-sdk/middleware-recursion-detection": "3.901.0", + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/region-config-resolver": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@aws-sdk/util-user-agent-browser": "3.901.0", + "@aws-sdk/util-user-agent-node": "3.901.0", + "@smithy/config-resolver": "^4.3.0", + "@smithy/core": "^3.14.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/hash-node": "^4.2.0", + "@smithy/invalid-dependency": "^4.2.0", + "@smithy/middleware-content-length": "^4.2.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-retry": "^4.4.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.0", + "@smithy/util-defaults-mode-browser": "^4.2.0", + "@smithy/util-defaults-mode-node": "^4.2.0", + "@smithy/util-endpoints": "^3.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -880,17 +814,17 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.899.0.tgz", - "integrity": "sha512-wV51Jogxhd7dI4Q2Y1ASbkwTsRT3G8uwWFDCwl+WaErOQAzofKlV6nFJQlfgjMk4iEn2gFOIWqJ8fMTGShRK/A==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.901.0.tgz", + "integrity": "sha512-2IWxbll/pRucp1WQkHi2W5E2SVPGBvk4Is923H7gpNksbVFws18ItjMM8ZpGm44cJEoy1zR5gjhLFklatpuoOw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-sdk-s3": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/signature-v4": "^5.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -898,49 +832,18 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": { - "version": "3.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.899.0.tgz", - "integrity": "sha512-Ovu1nWr8HafYa/7DaUvvPnzM/yDUGDBqaiS7rRzv++F5VwyFY37+z/mHhvRnr+PbNWo8uf22a121SNue5uwP2w==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.901.0.tgz", + "integrity": "sha512-pJEr1Ggbc/uVTDqp9IbNu9hdr0eQf3yZix3s4Nnyvmg4xmJSGAlbPC9LrNr5u3CDZoc8Z9CuLrvbP4MwYquNpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.899.0", - "@aws-sdk/nested-clients": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/types": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.893.0.tgz", - "integrity": "sha512-Aht1nn5SnA0N+Tjv0dzhAY7CQbxVtmq1bBR6xI0MhG7p2XYVh1wXuKTzrldEvQWwA3odOYunAfT9aBiKZx9qIg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz", - "integrity": "sha512-MhxBvWbwxmKknuggO2NeMwOVkHOYL98pZ+1ZRI5YwckoCL3AvISMnPJgfN60ww6AIXHGpkp+HhpFdKOe8RHSEg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-endpoints": "^3.1.2", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/nested-clients": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -948,29 +851,29 @@ } }, "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.893.0.tgz", - "integrity": "sha512-PE9NtbDBW6Kgl1bG6A5fF3EPo168tnkj8TgMcT0sg4xYBWsBpq0bpJZRh+Jm5Bkwiw9IgTCLjEU7mR6xWaMB9w==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.901.0.tgz", + "integrity": "sha512-Ntb6V/WFI21Ed4PDgL/8NSfoZQQf9xzrwNgiwvnxgAl/KvAvRBgQtqj5gHsDX8Nj2YmJuVoHfH9BGjL9VQ4WNg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.901.0", + "@smithy/types": "^4.6.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.899.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.899.0.tgz", - "integrity": "sha512-CiP0UAVQWLg2+8yciUBzVLaK5Fr7jBQ7wVu+p/O2+nlCOD3E3vtL1KZ1qX/d3OVpVSVaMAdZ9nbyewGV9hvjjg==", + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.901.0.tgz", + "integrity": "sha512-l59KQP5TY7vPVUfEURc7P5BJKuNg1RSsAKBQW7LHLECXjLqDUbo2SMLrexLBEoArSt6E8QOrIN0C8z/0Xk0jYw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.899.0", - "@aws-sdk/types": "3.893.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/types": "^4.6.0", "tslib": "^2.6.2" }, "engines": { @@ -985,21 +888,6 @@ } } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/xml-builder": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.894.0.tgz", - "integrity": "sha512-E6EAMc9dT1a2DOdo4zyOf3fp5+NJ2wI+mcm7RaW1baFIWDwcb99PpvWoV7YEiK7oaBDshuOEGWKUSYXdW+JYgA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.5.0", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/client-sso": { "version": "3.908.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.908.0.tgz", @@ -1680,6 +1568,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", @@ -1731,6 +1635,41 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", @@ -1749,6 +1688,41 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -1794,13 +1768,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1824,20 +1798,36 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { + "node_modules/@babel/template/node_modules/@babel/parser": { "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" @@ -1923,17 +1913,6 @@ "@noble/ciphers": "^1.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -2146,6 +2125,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "9.37.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", @@ -2354,154 +2345,6 @@ "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", - "cpu": [ - "arm64" - ], - "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-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "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==", - "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", @@ -2519,132 +2362,6 @@ "url": "https://opencollective.com/libvips" } }, - "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", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", - "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-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" - } - }, "node_modules/@img/sharp-linux-x64": { "version": "0.34.4", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", @@ -2668,132 +2385,6 @@ "@img/sharp-libvips-linux-x64": "1.2.3" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.5.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@ioredis/commands": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", @@ -2969,22 +2560,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz", - "integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", @@ -3068,22 +2643,6 @@ "node": ">= 10" } }, - "node_modules/@node-rs/argon2-linux-x64-musl": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", - "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/bcrypt": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", @@ -3129,22 +2688,6 @@ "node": ">= 10" } }, - "node_modules/@node-rs/bcrypt-linux-x64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.9.0.tgz", - "integrity": "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3403,9 +2946,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.4.tgz", - "integrity": "sha512-o2TkycuV98PtAkcqE8B1DJv5LBvHEDTWirK5TlkQMeF2MJg0BYliY95CeRZFILNgZJCbI3k/fhahSMRQlpXOMg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz", + "integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==", "license": "MIT" }, "node_modules/@radix-ui/colors": { @@ -4802,41 +4345,6 @@ "zod": "3.24.3" } }, - "node_modules/@react-email/preview-server/node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "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", @@ -4861,23 +4369,6 @@ "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/@types/node": { "version": "22.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", @@ -4946,16 +4437,6 @@ "node": ">= 6" } }, - "node_modules/@react-email/preview-server/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@react-email/preview-server/node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -5061,55 +4542,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/@react-email/preview-server/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/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/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -5202,6 +4634,55 @@ "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", @@ -5292,9 +4773,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.13.0.tgz", + "integrity": "sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw==", "license": "MIT" }, "node_modules/@scarf/scarf": { @@ -5349,12 +4830,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.0.tgz", - "integrity": "sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.2.tgz", + "integrity": "sha512-fPbcmEI+A6QiGOuumTpKSo7z+9VYr5DLN8d5/8jDJOwmt4HAKy/UGuRstCMpKbtr+FMaHH4pvFinSAbIAYCHZQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5387,15 +4868,15 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.0.tgz", - "integrity": "sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.2.tgz", + "integrity": "sha512-F/G+VaulIebINyfvcoXmODgIc7JU/lxWK9/iI0Divxyvd2QWB7/ZcF7JKwMssWI6/zZzlMkq/Pt6ow2AOEebPw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/types": "^4.7.1", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5403,18 +4884,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.15.0.tgz", - "integrity": "sha512-VJWncXgt+ExNn0U2+Y7UywuATtRYaodGQKFo9mDyh70q+fJGedfrqi2XuKU1BhiLeXgg6RZrW7VEKfeqFhHAJA==", + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.16.1.tgz", + "integrity": "sha512-yRx5ag3xEQ/yGvyo80FVukS7ZkeUP49Vbzg0MjfHLkuCIgg5lFtaEJfZR178KJmjWPqLU4d0P4k7SKgF9UkOaQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-stream": "^4.5.0", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-stream": "^4.5.2", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -5424,15 +4905,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.0.tgz", - "integrity": "sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.2.tgz", + "integrity": "sha512-hOjFTK+4mfehDnfjNkPqHUKBKR2qmlix5gy7YzruNbTdeoBE3QkfNCPvuCK2r05VUJ02QQ9bz2G41CxhSexsMw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5510,14 +4991,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.1.tgz", - "integrity": "sha512-3AvYYbB+Dv5EPLqnJIAgYw/9+WzeBiUYS8B+rU0pHq5NMQMvrZmevUROS4V2GAt0jEOn9viBzPLrZE+riTNd5Q==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.3.tgz", + "integrity": "sha512-cipIcM3xQ5NdIVwcRb37LaQwIxZNMEZb/ZOPmLFS9uGo9TGx2dGCyMBj9oT7ypH4TUD/kOTc/qHmwQzthrSk+g==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/querystring-builder": "^4.2.2", + "@smithy/types": "^4.7.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -5526,14 +5007,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.1.tgz", - "integrity": "sha512-Os9cg1fTXMwuqbvjemELlf+HB5oEeVyZmYsTbAtDQBmjGyibjmbeeqcaw7xOJLIHrkH/u0wAYabNcN6FRTqMRg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.3.tgz", + "integrity": "sha512-ZYd5FYhyvD4PCnqa1vO1tzqQ1s9fWs+el8DJZUfHumvq9wl7UXoLAqof+lAqSe/mVsf2I4tclf9BbTr6NL/pjQ==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5623,18 +5104,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.1.tgz", - "integrity": "sha512-JtM4SjEgImLEJVXdsbvWHYiJ9dtuKE8bqLlvkvGi96LbejDL6qnVpVxEFUximFodoQbg0Gnkyff9EKUhFhVJFw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.3.tgz", + "integrity": "sha512-CfxQ6X9L87/3C67Po6AGWXsx8iS4w2BO8vQEZJD6hwqg2vNRC/lMa2O5wXYCG9tKotdZ0R8KG33TS7kpUnYKiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.15.0", - "@smithy/middleware-serde": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/core": "^3.16.1", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@smithy/util-middleware": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5642,18 +5123,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.1.tgz", - "integrity": "sha512-wXxS4ex8cJJteL0PPQmWYkNi9QKDWZIpsndr0wZI2EL+pSSvA/qqxXU60gBOJoIc2YgtZSWY/PE86qhKCCKP1w==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.3.tgz", + "integrity": "sha512-EHnKGeFuzbmER4oSl/VJDxPLi+aiZUb3nk5KK8eNwHjMhI04jHlui2ZkaBzMfNmXOgymaS6zV//fyt6PSnI1ow==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/service-error-classification": "^4.2.0", - "@smithy/smithy-client": "^4.7.1", - "@smithy/types": "^4.6.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-retry": "^4.2.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/service-error-classification": "^4.2.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -5662,13 +5143,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.0.tgz", - "integrity": "sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.2.tgz", + "integrity": "sha512-tDMPMBCsA1GBxanShhPvQYwdiau3NmctUp+eELMhUTDua+EUrugXlaKCnTMMoEB5mbHFebdv81uJPkVP02oihA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5676,12 +5157,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.0.tgz", - "integrity": "sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.2.tgz", + "integrity": "sha512-7rgzDyLOQouh1bC6gOXnCGSX2dqvbOclgClsFkj735xQM2CHV63Ams8odNZGJgcqnBsEz44V/pDGHU6ALEUD+w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5689,14 +5170,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.0.tgz", - "integrity": "sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.2.tgz", + "integrity": "sha512-u38G0Audi2ORsL0QnzhopZ3yweMblQf8CZNbzUJ3wfTtZ7OiOwOzee0Nge/3dKeG/8lx0kt8K0kqDi6sYu0oKQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5704,15 +5185,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.3.0.tgz", - "integrity": "sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.1.tgz", + "integrity": "sha512-9gKJoL45MNyOCGTG082nmx0A6KrbLVQ+5QSSKyzRi0AzL0R81u3wC1+nPvKXgTaBdAKM73fFPdCBHpmtipQwdQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/abort-controller": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/querystring-builder": "^4.2.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5720,12 +5201,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.0.tgz", - "integrity": "sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.2.tgz", + "integrity": "sha512-MW7MfI+qYe/Ue5RH0uEztEKB+vBlOMM+1Dz68qzTsY8fC9kanXMFPEVdiq35JTGKWt5wZAjU1R0uXYEjK2MM1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5733,12 +5214,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.0.tgz", - "integrity": "sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.2.tgz", + "integrity": "sha512-nkKOI8xEkBXUmdxsFExomOb+wkU+Xgn0Fq2LMC7YIX5r4YPUg7PLayV/s/u3AtbyjWYlrvN7nAiDTLlqSdUjHw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5746,12 +5227,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.0.tgz", - "integrity": "sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.2.tgz", + "integrity": "sha512-YgXvq89o+R/8zIoeuXYv8Ysrbwgjx+iVYu9QbseqZjMDAhIg/FRt7jis0KASYFtd/Cnsnz4/nYTJXkJDWe8wHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -5760,12 +5241,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.0.tgz", - "integrity": "sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.2.tgz", + "integrity": "sha512-DczOD2yJy3NXcv1JvhjFC7bIb/tay6nnIRD/qrzBaju5lrkVBOwCT3Ps37tra20wy8PicZpworStK7ZcI9pCRQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5773,24 +5254,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.0.tgz", - "integrity": "sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.2.tgz", + "integrity": "sha512-1X17cMLwe/vb4RpZbQVpJ1xQQ7fhQKggMdt3qjdV3+6QNllzvUXyS3WFnyaFWLyaGqfYHKkNONbO1fBCMQyZtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0" + "@smithy/types": "^4.7.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.0.tgz", - "integrity": "sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.2.tgz", + "integrity": "sha512-AWnLgSmOTdDXM8aZCN4Im0X07M3GGffeL9vGfea4mdKZD0cPT9yLF9SsRbEa00tHLI+KfubDrmjpaKT2pM4GdQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5817,17 +5298,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.7.1.tgz", - "integrity": "sha512-WXVbiyNf/WOS/RHUoFMkJ6leEVpln5ojCjNBnzoZeMsnCg3A0BRhLK3WYc4V7PmYcYPZh9IYzzAg9XcNSzYxYQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.8.1.tgz", + "integrity": "sha512-N5wK57pVThzLVK5NgmHxocTy5auqGDGQ+JsL5RjCTriPt8JLYgXT0Awa915zCpzc9hXHDOKqDX5g9BFdwkSfUA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.15.0", - "@smithy/middleware-endpoint": "^4.3.1", - "@smithy/middleware-stack": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-stream": "^4.5.0", + "@smithy/core": "^3.16.1", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", + "@smithy/util-stream": "^4.5.2", "tslib": "^2.6.2" }, "engines": { @@ -5835,9 +5316,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.6.0.tgz", - "integrity": "sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.7.1.tgz", + "integrity": "sha512-WwP7vzoDyzvIFLzF5UhLQ6AsEx/PvSObzlNtJNW3lLy+BaSvTqCU628QKVvcJI/dydlAS1mSHQP7anKcxDcOxA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5847,13 +5328,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.0.tgz", - "integrity": "sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.2.tgz", + "integrity": "sha512-s2EYKukaswzjiHJCss6asB1F4zjRc0E/MFyceAKzb3+wqKA2Z/+Gfhb5FP8xVVRHBAvBkregaQAydifgbnUlCw==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/querystring-parser": "^4.2.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5924,14 +5405,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.0.tgz", - "integrity": "sha512-H4MAj8j8Yp19Mr7vVtGgi7noJjvjJbsKQJkvNnLlrIFduRFT5jq5Eri1k838YW7rN2g5FTnXpz5ktKVr1KVgPQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.2.tgz", + "integrity": "sha512-6JvKHZ5GORYkEZ2+yJKEHp6dQQKng+P/Mu3g3CDy0fRLQgXEO8be+FLrBGGb4kB9lCW6wcQDkN7kRiGkkVAXgg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.1", - "@smithy/types": "^4.6.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5939,17 +5420,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.1.tgz", - "integrity": "sha512-PuDcgx7/qKEMzV1QFHJ7E4/MMeEjaA7+zS5UNcHCLPvvn59AeZQ0DSDGMpqC2xecfa/1cNGm4l8Ec/VxCuY7Ug==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.3.tgz", + "integrity": "sha512-bkTGuMmKvghfCh9NayADrQcjngoF8P+XTgID5r3rm+8LphFiuM6ERqpBS95YyVaLjDetnKus9zK/bGlkQOOtNQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.3.0", - "@smithy/credential-provider-imds": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.1", - "@smithy/types": "^4.6.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/credential-provider-imds": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5983,12 +5464,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.0.tgz", - "integrity": "sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.2.tgz", + "integrity": "sha512-wL9tZwWKy0x0qf6ffN7tX5CT03hb1e7XpjdepaKfKcPcyn5+jHAWPqivhF1Sw/T5DYi9wGcxsX8Lu07MOp2Puw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5996,13 +5477,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.0.tgz", - "integrity": "sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.2.tgz", + "integrity": "sha512-TlbnWAOoCuG2PgY0Hi3BGU1w2IXs3xDsD4E8WDfKRZUn2qx3wRA9mbYnmpWHPswTJCz2L+ebh+9OvD42sV4mNw==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/service-error-classification": "^4.2.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -6010,14 +5491,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.0.tgz", - "integrity": "sha512-0TD5M5HCGu5diEvZ/O/WquSjhJPasqv7trjoqHyWjNh/FBeBl7a0ztl9uFMOsauYtRfd8jvpzIAQhDHbx+nvZw==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.2.tgz", + "integrity": "sha512-RWYVuQVKtNbr7E0IxV8XHDId714yHPTxU6dHScd6wSMWAXboErzTG7+xqcL+K3r0Xg0cZSlfuNhl1J0rzMLSSw==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.1", - "@smithy/node-http-handler": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/types": "^4.7.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -6185,23 +5666,6 @@ "node": ">= 10" } }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", - "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tailwindcss/postcss": { "version": "4.1.14", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", @@ -6348,9 +5812,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "dev": true, "license": "MIT", "dependencies": { @@ -6517,6 +5981,28 @@ "license": "MIT" }, "node_modules/@types/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", + "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", + "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", @@ -6527,18 +6013,6 @@ "@types/node": "*" } }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, "node_modules/@types/swagger-ui-express": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", @@ -6881,19 +6355,6 @@ "linux" ] }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -7477,15 +6938,6 @@ "node": ">= 0.4" } }, - "node_modules/async-generator-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-generator-function/-/async-generator-function-1.0.0.tgz", - "integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -7611,9 +7063,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", - "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz", + "integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7720,9 +7172,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -7740,9 +7192,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -7866,9 +7318,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001748", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", + "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", "funding": [ { "type": "opencollective", @@ -8636,9 +8088,9 @@ } }, "node_modules/detect-libc": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", - "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -8952,9 +8404,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.227", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", - "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", + "version": "1.5.233", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz", + "integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==", "dev": true, "license": "ISC" }, @@ -10461,9 +9913,9 @@ } }, "node_modules/generator-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.0.tgz", - "integrity": "sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10501,19 +9953,16 @@ } }, "node_modules/get-intrinsic": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.1.tgz", - "integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "async-function": "^1.0.0", - "async-generator-function": "^1.0.0", "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "generator-function": "^2.0.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", @@ -10580,9 +10029,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.11.0.tgz", + "integrity": "sha512-sNsqf7XKQ38IawiVGPOoAlqZo1DMrO7TU+ZcZwi7yLl7/7S0JwmoBMKz/IkUPhSoXM0Ng3vT0yB1iCe5XavDeQ==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -10655,15 +10104,13 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/globalthis": { @@ -11251,13 +10698,14 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -11586,9 +11034,9 @@ } }, "node_modules/jiti": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", - "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", "bin": { @@ -11858,27 +11306,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "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/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -12383,9 +11810,9 @@ "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" @@ -12537,9 +11964,9 @@ } }, "node_modules/node-abi": { - "version": "3.77.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", - "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -12610,9 +12037,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "dev": true, "license": "MIT" }, @@ -15413,22 +14840,6 @@ "node": ">= 10" } }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz", - "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -15922,12 +15333,12 @@ } }, "node_modules/posthog-node": { - "version": "5.9.5", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.9.5.tgz", - "integrity": "sha512-Rv82jMVhnxlBNf8wDbP+iAJdZrhU0aHul0LaFrQ/JGxxDiK3EkclIqr+QUwA9CulleTtXf6AIFz22tLvbVs/HA==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.10.0.tgz", + "integrity": "sha512-uNN+YUuOdbDSbDMGk/Wq57o2YBEH0Unu1kEq2PuYmqFmnu+oYsKyJBrb58VNwEuYsaXVJmk4FtbD+Tl8BT69+w==", "license": "MIT", "dependencies": { - "@posthog/core": "1.2.4" + "@posthog/core": "1.3.0" }, "engines": { "node": ">=20" @@ -16992,9 +16403,9 @@ "license": "MIT" }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "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": { @@ -18145,9 +17556,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.1.tgz", - "integrity": "sha512-qyjpz0qgcomRr41a5Aye42o69TKwCeHM9F8htLGVeUMKekNS6qAqz9oS7CtSvgGJSppSNAYAIh7vrfrSdHj9zw==", + "version": "5.29.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.3.tgz", + "integrity": "sha512-U99f/2YocRA2Mxqx3eUBRhQonWVtE5dIvMs0Zlsn4a4ip8awMq0JxXhU+Sidtna2WlZrHbK2Rro3RZvYUymRbA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -18185,9 +17596,9 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { @@ -18950,9 +18361,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -19033,9 +18444,9 @@ } }, "node_modules/webpack": { - "version": "5.102.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz", - "integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==", + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19047,7 +18458,7 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.5", + "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", @@ -19059,8 +18470,8 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.2.3", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" diff --git a/package.json b/package.json index 74f540be..bca66d3d 100644 --- a/package.json +++ b/package.json @@ -19,17 +19,17 @@ "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", - "set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts", - "set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts", + "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:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts", "set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts", + "next:build": "next build", "build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", "start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3005", - "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs", - "db:sqlite:seed-exit-node": "sqlite3 config/db/db.sqlite \"INSERT INTO exitNodes (exitNodeId, name, address, endpoint, publicKey, listenPort, reachableAt, maxConnections, online, lastPing, type, region) VALUES (null, 'test', '10.0.0.1/24', 'localhost', 'MJ44MpnWGxMZURgxW/fWXDFsejhabnEFYDo60LQwK3A=', 1234, 'http://localhost:3003', 123, 1, null, 'gerbil', null);\"" + "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs" }, "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.4", diff --git a/public/logo/word_mark_black.png b/public/logo/word_mark_black.png index ba6fb84e..cc412165 100644 Binary files a/public/logo/word_mark_black.png and b/public/logo/word_mark_black.png differ diff --git a/public/logo/word_mark_white.png b/public/logo/word_mark_white.png index fb7a252d..cd02b58a 100644 Binary files a/public/logo/word_mark_white.png and b/public/logo/word_mark_white.png differ diff --git a/server/apiServer.ts b/server/apiServer.ts index 0b5a6305..6c490053 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -7,21 +7,21 @@ import { errorHandlerMiddleware, notFoundMiddleware } from "@server/middlewares"; -import { corsWithLoginPageSupport } from "@server/middlewares/private/corsWithLoginPage"; -import { authenticated, unauthenticated } from "@server/routers/external"; -import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; +import { authenticated, unauthenticated } from "#dynamic/routers/external"; +import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import helmet from "helmet"; -import { stripeWebhookHandler } from "@server/routers/private/billing/webhooks"; import { build } from "./build"; import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import createHttpError from "http-errors"; import HttpCode from "./types/HttpCode"; import requestTimeoutMiddleware from "./middlewares/requestTimeout"; -import { createStore } from "@server/lib/private/rateLimitStore"; -import hybridRouter from "@server/routers/private/hybrid"; +import { createStore } from "#dynamic/lib/rateLimitStore"; import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; +import { corsWithLoginPageSupport } from "@server/lib/corsWithLoginPage"; +import { hybridRouter } from "#dynamic/routers/hybrid"; +import { billingWebhookHandler } from "#dynamic/routers/billing/webhooks"; const dev = config.isDev; const externalPort = config.getRawConfig().server.external_port; @@ -39,32 +39,30 @@ export function createApiServer() { apiServer.post( `${prefix}/billing/webhooks`, express.raw({ type: "application/json" }), - stripeWebhookHandler + billingWebhookHandler ); } const corsConfig = config.getRawConfig().server.cors; + const options = { + ...(corsConfig?.origins + ? { origin: corsConfig.origins } + : { + origin: (origin: any, callback: any) => { + callback(null, true); + } + }), + ...(corsConfig?.methods && { methods: corsConfig.methods }), + ...(corsConfig?.allowed_headers && { + allowedHeaders: corsConfig.allowed_headers + }), + credentials: !(corsConfig?.credentials === false) + }; - if (build == "oss") { - const options = { - ...(corsConfig?.origins - ? { origin: corsConfig.origins } - : { - origin: (origin: any, callback: any) => { - callback(null, true); - } - }), - ...(corsConfig?.methods && { methods: corsConfig.methods }), - ...(corsConfig?.allowed_headers && { - allowedHeaders: corsConfig.allowed_headers - }), - credentials: !(corsConfig?.credentials === false) - }; - + if (build == "oss" || !corsConfig) { logger.debug("Using CORS options", options); - apiServer.use(cors(options)); - } else { + } else if (corsConfig) { // Use the custom CORS middleware with loginPage support apiServer.use(corsWithLoginPageSupport(corsConfig)); } diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 668be0db..e48bc502 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -4,7 +4,6 @@ import { userActions, roleActions, userOrgs } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; -import { sendUsageNotification } from "@server/routers/org"; export enum ActionsEnum { createOrgUser = "createOrgUser", diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index a378202e..31ab2b38 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db"; import { db } from "@server/db"; import { eq, and } from "drizzle-orm"; import config from "@server/lib/config"; -import axios from "axios"; -import logger from "@server/logger"; -import { tokenManager } from "@server/lib/tokenManager"; export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name; @@ -65,29 +62,6 @@ export async function validateResourceSessionToken( token: string, resourceId: number ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, { - token: token - }, await tokenManager.getAuthHeader()); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error validating resource session token in hybrid mode:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error validating resource session token in hybrid mode:", error); - } - return { resourceSession: null }; - } - } - const sessionId = encodeHexLowerCase( sha256(new TextEncoder().encode(token)) ); diff --git a/server/cleanup.ts b/server/cleanup.ts new file mode 100644 index 00000000..de54ed77 --- /dev/null +++ b/server/cleanup.ts @@ -0,0 +1,13 @@ +import { cleanup as wsCleanup } from "@server/routers/ws"; + +async function cleanup() { + await wsCleanup(); + + process.exit(0); +} + +export async function initCleanup() { + // Handle process termination + process.on("SIGTERM", () => cleanup()); + process.on("SIGINT", () => cleanup()); +} \ No newline at end of file diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 23904c7e..6dbef7e8 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -38,9 +38,9 @@ function createDb() { const poolConfig = config.postgres.pool; const primaryPool = new Pool({ connectionString, - max: poolConfig.max_connections, - idleTimeoutMillis: poolConfig.idle_timeout_ms, - connectionTimeoutMillis: poolConfig.connection_timeout_ms, + max: poolConfig?.max_connections || 20, + idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, + connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000, }); const replicas = []; @@ -51,9 +51,9 @@ function createDb() { for (const conn of replicaConnections) { const replicaPool = new Pool({ connectionString: conn.connection_string, - max: poolConfig.max_replica_connections, - idleTimeoutMillis: poolConfig.idle_timeout_ms, - connectionTimeoutMillis: poolConfig.connection_timeout_ms, + max: poolConfig?.max_replica_connections || 20, + idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, + connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000, }); replicas.push(DrizzlePostgres(replicaPool)); } diff --git a/server/db/pg/index.ts b/server/db/pg/index.ts index 5cc80e86..6e2c79f5 100644 --- a/server/db/pg/index.ts +++ b/server/db/pg/index.ts @@ -1,3 +1,3 @@ export * from "./driver"; -export * from "./schema"; -export * from "./privateSchema"; +export * from "./schema/schema"; +export * from "./schema/privateSchema"; diff --git a/server/db/pg/privateSchema.ts b/server/db/pg/schema/privateSchema.ts similarity index 96% rename from server/db/pg/privateSchema.ts rename to server/db/pg/schema/privateSchema.ts index 8ea8f9de..67fb28ec 100644 --- a/server/db/pg/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -1,16 +1,3 @@ -/* - * 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 { pgTable, serial, diff --git a/server/db/pg/schema.ts b/server/db/pg/schema/schema.ts similarity index 99% rename from server/db/pg/schema.ts rename to server/db/pg/schema/schema.ts index e94bfe36..2e307c5f 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -720,4 +720,5 @@ export type OrgDomains = InferSelectModel; export type SiteResource = InferSelectModel; export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; -export type TargetHealthCheck = InferSelectModel; \ No newline at end of file +export type TargetHealthCheck = InferSelectModel; +export type IdpOidcConfig = InferSelectModel; diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 09c465b5..8944a491 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -17,10 +17,6 @@ import { users } from "@server/db"; import { and, eq } from "drizzle-orm"; -import axios from "axios"; -import config from "@server/lib/config"; -import logger from "@server/logger"; -import { tokenManager } from "@server/lib/tokenManager"; export type ResourceWithAuth = { resource: Resource | null; @@ -40,30 +36,6 @@ export type UserSessionWithUser = { export async function getResourceByDomain( domain: string ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const [result] = await db .select() .from(resources) @@ -100,30 +72,6 @@ export async function getResourceByDomain( export async function getUserSessionWithUser( userSessionId: string ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const [res] = await db .select() .from(sessions) @@ -144,30 +92,6 @@ export async function getUserSessionWithUser( * Get user organization role */ export async function getUserOrgRole(userId: string, orgId: string) { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const userOrgRole = await db .select() .from(userOrgs) @@ -184,30 +108,6 @@ export async function getRoleResourceAccess( resourceId: number, roleId: number ) { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const roleResourceAccess = await db .select() .from(roleResources) @@ -229,30 +129,6 @@ export async function getUserResourceAccess( userId: string, resourceId: number ) { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const userResourceAccess = await db .select() .from(userResources) @@ -273,30 +149,6 @@ export async function getUserResourceAccess( export async function getResourceRules( resourceId: number ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return []; - } - } - const rules = await db .select() .from(resourceRules) @@ -311,30 +163,6 @@ export async function getResourceRules( export async function getOrgLoginPage( orgId: string ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/org/${orgId}/login-page`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const [result] = await db .select() .from(loginPageOrg) diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index 6369c268..211ba8ea 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -1,6 +1,6 @@ import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; -import * as schema from "./schema"; +import * as schema from "./schema/schema"; import path from "path"; import fs from "fs"; import { APP_PATH } from "@server/lib/consts"; diff --git a/server/db/sqlite/index.ts b/server/db/sqlite/index.ts index 8c7a15e5..6e2c79f5 100644 --- a/server/db/sqlite/index.ts +++ b/server/db/sqlite/index.ts @@ -1,3 +1,3 @@ export * from "./driver"; -export * from "./schema"; -export * from "./privateSchema"; \ No newline at end of file +export * from "./schema/schema"; +export * from "./schema/privateSchema"; diff --git a/server/db/sqlite/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts similarity index 95% rename from server/db/sqlite/privateSchema.ts rename to server/db/sqlite/schema/privateSchema.ts index fbe86e25..557ebfd6 100644 --- a/server/db/sqlite/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -1,16 +1,3 @@ -/* - * 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 { sqliteTable, integer, diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema/schema.ts similarity index 99% rename from server/db/sqlite/schema.ts rename to server/db/sqlite/schema/schema.ts index 9d64b85e..2c19a1c7 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -759,4 +759,5 @@ export type SiteResource = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; -export type TargetHealthCheck = InferSelectModel; \ No newline at end of file +export type TargetHealthCheck = InferSelectModel; +export type IdpOidcConfig = InferSelectModel; diff --git a/server/emails/index.ts b/server/emails/index.ts index 2cdef8a1..42cfa39c 100644 --- a/server/emails/index.ts +++ b/server/emails/index.ts @@ -6,11 +6,6 @@ import logger from "@server/logger"; import SMTPTransport from "nodemailer/lib/smtp-transport"; function createEmailClient() { - if (config.isManagedMode()) { - // LETS NOT WORRY ABOUT EMAILS IN HYBRID - return; - } - const emailConfig = config.getRawConfig().email; if (!emailConfig) { logger.warn( diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index 24fb4fbd..c8a0b077 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -2,7 +2,6 @@ import { render } from "@react-email/render"; import { ReactElement } from "react"; import emailClient from "@server/emails"; import logger from "@server/logger"; -import config from "@server/lib/config"; export async function sendEmail( template: ReactElement, @@ -25,7 +24,7 @@ export async function sendEmail( const emailHtml = await render(template); - const appName = config.getRawPrivateConfig().branding?.app_name || "Pangolin"; + const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config await emailClient.sendMail({ from: { diff --git a/server/emails/templates/PrivateNotifyUsageLimitApproaching.tsx b/server/emails/templates/NotifyUsageLimitApproaching.tsx similarity index 86% rename from server/emails/templates/PrivateNotifyUsageLimitApproaching.tsx rename to server/emails/templates/NotifyUsageLimitApproaching.tsx index c66265e5..beab0300 100644 --- a/server/emails/templates/PrivateNotifyUsageLimitApproaching.tsx +++ b/server/emails/templates/NotifyUsageLimitApproaching.tsx @@ -1,16 +1,3 @@ -/* - * 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 React from "react"; import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; import { themeColors } from "./lib/theme"; diff --git a/server/emails/templates/PrivateNotifyUsageLimitReached.tsx b/server/emails/templates/NotifyUsageLimitReached.tsx similarity index 87% rename from server/emails/templates/PrivateNotifyUsageLimitReached.tsx rename to server/emails/templates/NotifyUsageLimitReached.tsx index c4eac322..783d1b0e 100644 --- a/server/emails/templates/PrivateNotifyUsageLimitReached.tsx +++ b/server/emails/templates/NotifyUsageLimitReached.tsx @@ -1,16 +1,3 @@ -/* - * 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 React from "react"; import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; import { themeColors } from "./lib/theme"; diff --git a/server/hybridServer.ts b/server/hybridServer.ts deleted file mode 100644 index 7e9ce095..00000000 --- a/server/hybridServer.ts +++ /dev/null @@ -1,151 +0,0 @@ -import logger from "@server/logger"; -import config from "@server/lib/config"; -import { createWebSocketClient } from "./routers/ws/client"; -import { addPeer, deletePeer } from "./routers/gerbil/peers"; -import { db, exitNodes } from "./db"; -import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager"; -import { tokenManager } from "./lib/tokenManager"; -import { APP_VERSION } from "./lib/consts"; -import axios from "axios"; - -export async function createHybridClientServer() { - logger.info("Starting hybrid client server..."); - - // Start the token manager - await tokenManager.start(); - - const token = await tokenManager.getToken(); - - const monitor = new TraefikConfigManager(); - - await monitor.start(); - - // Create client - const client = createWebSocketClient( - token, - config.getRawConfig().managed!.endpoint!, - { - reconnectInterval: 5000, - pingInterval: 30000, - pingTimeout: 10000 - } - ); - - // Register message handlers - client.registerHandler("remoteExitNode/peers/add", async (message) => { - const { publicKey, allowedIps } = message.data; - - // TODO: we are getting the exit node twice here - // NOTE: there should only be one gerbil registered so... - const [exitNode] = await db.select().from(exitNodes).limit(1); - await addPeer(exitNode.exitNodeId, { - publicKey: publicKey, - allowedIps: allowedIps || [] - }); - }); - - client.registerHandler("remoteExitNode/peers/remove", async (message) => { - const { publicKey } = message.data; - - // TODO: we are getting the exit node twice here - // NOTE: there should only be one gerbil registered so... - const [exitNode] = await db.select().from(exitNodes).limit(1); - await deletePeer(exitNode.exitNodeId, publicKey); - }); - - // /update-proxy-mapping - client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => { - try { - const [exitNode] = await db.select().from(exitNodes).limit(1); - if (!exitNode) { - logger.error("No exit node found for proxy mapping update"); - return; - } - - const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data); - logger.info(`Successfully updated proxy mapping: ${response.status}`); - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error updating proxy mapping:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error updating proxy mapping:", error); - } - } - }); - - // /update-destinations - client.registerHandler("remoteExitNode/update-destinations", async (message) => { - try { - const [exitNode] = await db.select().from(exitNodes).limit(1); - if (!exitNode) { - logger.error("No exit node found for destinations update"); - return; - } - - const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data); - logger.info(`Successfully updated destinations: ${response.status}`); - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error updating destinations:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error updating destinations:", error); - } - } - }); - - client.registerHandler("remoteExitNode/traefik/reload", async (message) => { - await monitor.HandleTraefikConfig(); - }); - - // Listen to connection events - client.on("connect", () => { - logger.info("Connected to WebSocket server"); - client.sendMessage("remoteExitNode/register", { - remoteExitNodeVersion: APP_VERSION - }); - }); - - client.on("disconnect", () => { - logger.info("Disconnected from WebSocket server"); - }); - - client.on("message", (message) => { - logger.info( - `Received message: ${message.type} ${JSON.stringify(message.data)}` - ); - }); - - // Connect to the server - try { - await client.connect(); - logger.info("Connection initiated"); - } catch (error) { - logger.error("Failed to connect:", error); - } - - // Store the ping interval stop function for cleanup if needed - const stopPingInterval = client.sendMessageInterval( - "remoteExitNode/ping", - { timestamp: Date.now() / 1000 }, - 60000 - ); // send every minute - - // Return client and cleanup function for potential use - return { client, stopPingInterval }; -} diff --git a/server/index.ts b/server/index.ts index 2497e301..a92968a6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,13 +5,20 @@ import { runSetupFunctions } from "./setup"; import { createApiServer } from "./apiServer"; import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; -import { ApiKey, ApiKeyOrg, RemoteExitNode, Session, User, UserOrg } from "@server/db"; +import { + ApiKey, + ApiKeyOrg, + RemoteExitNode, + Session, + User, + UserOrg +} from "@server/db"; import { createIntegrationApiServer } from "./integrationApiServer"; -import { createHybridClientServer } from "./hybridServer"; import config from "@server/lib/config"; import { setHostMeta } from "@server/lib/hostMeta"; import { initTelemetryClient } from "./lib/telemetry.js"; import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js"; +import { initCleanup } from "#dynamic/cleanup"; async function startServers() { await setHostMeta(); @@ -25,16 +32,11 @@ async function startServers() { const apiServer = createApiServer(); const internalServer = createInternalServer(); - let hybridClientServer; let nextServer; - if (config.isManagedMode()) { - hybridClientServer = await createHybridClientServer(); - } else { - nextServer = await createNextServer(); - if (config.getRawConfig().traefik.file_mode) { - const monitor = new TraefikConfigManager(); - await monitor.start(); - } + nextServer = await createNextServer(); + if (config.getRawConfig().traefik.file_mode) { + const monitor = new TraefikConfigManager(); + await monitor.start(); } let integrationServer; @@ -42,12 +44,13 @@ async function startServers() { integrationServer = createIntegrationApiServer(); } + await initCleanup(); + return { apiServer, nextServer, internalServer, - integrationServer, - hybridClientServer + integrationServer }; } diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts index cbbea83f..3416004c 100644 --- a/server/integrationApiServer.ts +++ b/server/integrationApiServer.ts @@ -7,7 +7,7 @@ import { errorHandlerMiddleware, notFoundMiddleware, } from "@server/middlewares"; -import { authenticated, unauthenticated } from "@server/routers/integration"; +import { authenticated, unauthenticated } from "#dynamic/routers/integration"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; import helmet from "helmet"; import swaggerUi from "swagger-ui-express"; diff --git a/server/internalServer.ts b/server/internalServer.ts index 0ba64eec..d15e3c45 100644 --- a/server/internalServer.ts +++ b/server/internalServer.ts @@ -8,7 +8,7 @@ import { errorHandlerMiddleware, notFoundMiddleware } from "@server/middlewares"; -import internal from "@server/routers/internal"; +import { internalRouter } from "#dynamic/routers/internal"; import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; const internalPort = config.getRawConfig().server.internal_port; @@ -23,7 +23,7 @@ export function createInternalServer() { internalServer.use(express.json()); const prefix = `/api/v1`; - internalServer.use(prefix, internal); + internalServer.use(prefix, internalRouter); internalServer.use(notFoundMiddleware); internalServer.use(errorHandlerMiddleware); diff --git a/server/lib/billing/createCustomer.ts b/server/lib/billing/createCustomer.ts new file mode 100644 index 00000000..7f65bfb2 --- /dev/null +++ b/server/lib/billing/createCustomer.ts @@ -0,0 +1,6 @@ +export async function createCustomer( + orgId: string, + email: string | null | undefined +): Promise { + return; +} diff --git a/server/lib/private/billing/features.ts b/server/lib/billing/features.ts similarity index 88% rename from server/lib/private/billing/features.ts rename to server/lib/billing/features.ts index 11d78bbb..b72543cc 100644 --- a/server/lib/private/billing/features.ts +++ b/server/lib/billing/features.ts @@ -1,16 +1,3 @@ -/* - * 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 Stripe from "stripe"; export enum FeatureId { diff --git a/server/lib/billing/getOrgTierData.ts b/server/lib/billing/getOrgTierData.ts new file mode 100644 index 00000000..24664790 --- /dev/null +++ b/server/lib/billing/getOrgTierData.ts @@ -0,0 +1,8 @@ +export async function getOrgTierData( + orgId: string +): Promise<{ tier: string | null; active: boolean }> { + let tier = null; + let active = false; + + return { tier, active }; +} diff --git a/server/lib/billing/index.ts b/server/lib/billing/index.ts new file mode 100644 index 00000000..6c3ef792 --- /dev/null +++ b/server/lib/billing/index.ts @@ -0,0 +1,5 @@ +export * from "./limitSet"; +export * from "./features"; +export * from "./limitsService"; +export * from "./getOrgTierData"; +export * from "./createCustomer"; \ No newline at end of file diff --git a/server/lib/private/billing/limitSet.ts b/server/lib/billing/limitSet.ts similarity index 82% rename from server/lib/private/billing/limitSet.ts rename to server/lib/billing/limitSet.ts index ec6107b2..153d8ae8 100644 --- a/server/lib/private/billing/limitSet.ts +++ b/server/lib/billing/limitSet.ts @@ -1,16 +1,3 @@ -/* - * 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 { FeatureId } from "./features"; export type LimitSet = { diff --git a/server/lib/private/billing/limitsService.ts b/server/lib/billing/limitsService.ts similarity index 76% rename from server/lib/private/billing/limitsService.ts rename to server/lib/billing/limitsService.ts index 168f5580..a07f70b3 100644 --- a/server/lib/private/billing/limitsService.ts +++ b/server/lib/billing/limitsService.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { db, limits } from "@server/db"; import { and, eq } from "drizzle-orm"; import { LimitSet } from "./limitSet"; diff --git a/server/lib/private/billing/tiers.ts b/server/lib/billing/tiers.ts similarity index 69% rename from server/lib/private/billing/tiers.ts rename to server/lib/billing/tiers.ts index e6322c9f..6ccf8898 100644 --- a/server/lib/private/billing/tiers.ts +++ b/server/lib/billing/tiers.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - export enum TierId { STANDARD = "standard", } diff --git a/server/lib/private/billing/usageService.ts b/server/lib/billing/usageService.ts similarity index 84% rename from server/lib/private/billing/usageService.ts rename to server/lib/billing/usageService.ts index f18542d2..0b2b095f 100644 --- a/server/lib/private/billing/usageService.ts +++ b/server/lib/billing/usageService.ts @@ -1,21 +1,7 @@ -/* - * 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 { eq, sql, and } from "drizzle-orm"; import NodeCache from "node-cache"; import { v4 as uuidv4 } from "uuid"; import { PutObjectCommand } from "@aws-sdk/client-s3"; -import { s3Client } from "../s3"; import * as fs from "fs/promises"; import * as path from "path"; import { @@ -30,10 +16,10 @@ import { Transaction } from "@server/db"; import { FeatureId, getFeatureMeterId } from "./features"; -import config from "@server/lib/config"; import logger from "@server/logger"; -import { sendToClient } from "@server/routers/ws"; +import { sendToClient } from "#dynamic/routers/ws"; import { build } from "@server/build"; +import { s3Client } from "@server/lib/s3"; interface StripeEvent { identifier?: string; @@ -45,6 +31,17 @@ interface StripeEvent { }; } +export function noop() { + if ( + build !== "saas" || + !process.env.S3_BUCKET || + !process.env.LOCAL_FILE_PATH + ) { + return true; + } + return false; +} + export class UsageService { private cache: NodeCache; private bucketName: string | undefined; @@ -55,11 +52,13 @@ export class UsageService { constructor() { this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL - if (build !== "saas") { + if (noop()) { return; } - this.bucketName = config.getRawPrivateConfig().stripe?.s3Bucket; - this.eventsDir = config.getRawPrivateConfig().stripe?.localFilePath; + // this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket; + // this.eventsDir = privateConfig.getRawPrivateConfig().stripe?.localFilePath; + this.bucketName = process.env.S3_BUCKET || undefined; + this.eventsDir = process.env.LOCAL_FILE_PATH || undefined; // Ensure events directory exists this.initializeEventsDirectory().then(() => { @@ -83,7 +82,9 @@ export class UsageService { private async initializeEventsDirectory(): Promise { if (!this.eventsDir) { - logger.warn("Stripe local file path is not configured, skipping events directory initialization."); + logger.warn( + "Stripe local file path is not configured, skipping events directory initialization." + ); return; } try { @@ -95,7 +96,9 @@ export class UsageService { private async uploadPendingEventFilesOnStartup(): Promise { if (!this.eventsDir || !this.bucketName) { - logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload."); + logger.warn( + "Stripe local file path or bucket name is not configured, skipping leftover event file upload." + ); return; } try { @@ -118,15 +121,17 @@ export class UsageService { ContentType: "application/json" }); await s3Client.send(uploadCommand); - + // Check if file still exists before unlinking try { await fs.access(filePath); await fs.unlink(filePath); } catch (unlinkError) { - logger.debug(`Startup file ${file} was already deleted`); + logger.debug( + `Startup file ${file} was already deleted` + ); } - + logger.info( `Uploaded leftover event file ${file} to S3 with ${events.length} events` ); @@ -136,7 +141,9 @@ export class UsageService { await fs.access(filePath); await fs.unlink(filePath); } catch (unlinkError) { - logger.debug(`Empty startup file ${file} was already deleted`); + logger.debug( + `Empty startup file ${file} was already deleted` + ); } } } catch (err) { @@ -147,8 +154,8 @@ export class UsageService { } } } - } catch (err) { - logger.error("Failed to scan for leftover event files:", err); + } catch (error) { + logger.error("Failed to scan for leftover event files"); } } @@ -158,17 +165,17 @@ export class UsageService { value: number, transaction: any = null ): Promise { - if (build !== "saas") { + if (noop()) { return null; } - + // Truncate value to 11 decimal places value = this.truncateValue(value); - + // Implement retry logic for deadlock handling const maxRetries = 3; let attempt = 0; - + while (attempt <= maxRetries) { try { // Get subscription data for this org (with caching) @@ -191,7 +198,12 @@ export class UsageService { ); } else { await db.transaction(async (trx) => { - usage = await this.internalAddUsage(orgId, featureId, value, trx); + usage = await this.internalAddUsage( + orgId, + featureId, + value, + trx + ); }); } @@ -201,25 +213,26 @@ export class UsageService { return usage || null; } catch (error: any) { // Check if this is a deadlock error - const isDeadlock = error?.code === '40P01' || - error?.cause?.code === '40P01' || - (error?.message && error.message.includes('deadlock')); - + const isDeadlock = + error?.code === "40P01" || + error?.cause?.code === "40P01" || + (error?.message && error.message.includes("deadlock")); + if (isDeadlock && attempt < maxRetries) { attempt++; // Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms const baseDelay = Math.pow(2, attempt - 1) * 50; const jitter = Math.random() * baseDelay; const delay = baseDelay + jitter; - + logger.warn( `Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms` ); - - await new Promise(resolve => setTimeout(resolve, delay)); + + await new Promise((resolve) => setTimeout(resolve, delay)); continue; } - + logger.error( `Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`, error @@ -239,10 +252,10 @@ export class UsageService { ): Promise { // Truncate value to 11 decimal places value = this.truncateValue(value); - + const usageId = `${orgId}-${featureId}`; const meterId = getFeatureMeterId(featureId); - + // Use upsert: insert if not exists, otherwise increment const [returnUsage] = await trx .insert(usage) @@ -259,7 +272,8 @@ export class UsageService { set: { latestValue: sql`${usage.latestValue} + ${value}` } - }).returning(); + }) + .returning(); return returnUsage; } @@ -280,7 +294,7 @@ export class UsageService { value?: number, customerId?: string ): Promise { - if (build !== "saas") { + if (noop()) { return; } try { @@ -351,7 +365,7 @@ export class UsageService { .set({ latestValue: newRunningTotal, instantaneousValue: value, - updatedAt: Math.floor(Date.now() / 1000) + updatedAt: Math.floor(Date.now() / 1000) }) .where(eq(usage.usageId, usageId)); } @@ -366,7 +380,7 @@ export class UsageService { meterId, instantaneousValue: truncatedValue, latestValue: truncatedValue, - updatedAt: Math.floor(Date.now() / 1000) + updatedAt: Math.floor(Date.now() / 1000) }); } }); @@ -427,7 +441,7 @@ export class UsageService { ): Promise { // Truncate value to 11 decimal places before sending to Stripe const truncatedValue = this.truncateValue(value); - + const event: StripeEvent = { identifier: uuidv4(), timestamp: Math.floor(new Date().getTime() / 1000), @@ -444,7 +458,9 @@ export class UsageService { private async writeEventToFile(event: StripeEvent): Promise { if (!this.eventsDir || !this.bucketName) { - logger.warn("Stripe local file path or bucket name is not configured, skipping event file write."); + logger.warn( + "Stripe local file path or bucket name is not configured, skipping event file write." + ); return; } if (!this.currentEventFile) { @@ -493,7 +509,9 @@ export class UsageService { private async uploadFileToS3(): Promise { if (!this.bucketName || !this.eventsDir) { - logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload."); + logger.warn( + "Stripe local file path or bucket name is not configured, skipping S3 upload." + ); return; } if (!this.currentEventFile) { @@ -505,7 +523,9 @@ export class UsageService { // Check if this file is already being uploaded if (this.uploadingFiles.has(fileName)) { - logger.debug(`File ${fileName} is already being uploaded, skipping`); + logger.debug( + `File ${fileName} is already being uploaded, skipping` + ); return; } @@ -517,7 +537,9 @@ export class UsageService { try { await fs.access(filePath); } catch (error) { - logger.debug(`File ${fileName} does not exist, may have been already processed`); + logger.debug( + `File ${fileName} does not exist, may have been already processed` + ); this.uploadingFiles.delete(fileName); // Reset current file if it was this file if (this.currentEventFile === fileName) { @@ -537,7 +559,9 @@ export class UsageService { await fs.unlink(filePath); } catch (unlinkError) { // File may have been already deleted - logger.debug(`File ${fileName} was already deleted during cleanup`); + logger.debug( + `File ${fileName} was already deleted during cleanup` + ); } this.currentEventFile = null; this.uploadingFiles.delete(fileName); @@ -560,7 +584,9 @@ export class UsageService { await fs.unlink(filePath); } catch (unlinkError) { // File may have been already deleted by another process - logger.debug(`File ${fileName} was already deleted during upload`); + logger.debug( + `File ${fileName} was already deleted during upload` + ); } logger.info( @@ -571,10 +597,7 @@ export class UsageService { this.currentEventFile = null; this.currentFileStartTime = 0; } catch (error) { - logger.error( - `Failed to upload ${fileName} to S3:`, - error - ); + logger.error(`Failed to upload ${fileName} to S3:`, error); } finally { // Always remove from uploading set this.uploadingFiles.delete(fileName); @@ -591,7 +614,7 @@ export class UsageService { orgId: string, featureId: FeatureId ): Promise { - if (build !== "saas") { + if (noop()) { return null; } @@ -610,7 +633,7 @@ export class UsageService { `Creating new usage record for ${orgId}/${featureId}` ); const meterId = getFeatureMeterId(featureId); - + try { const [newUsage] = await db .insert(usage) @@ -665,7 +688,7 @@ export class UsageService { orgId: string, featureId: FeatureId ): Promise { - if (build !== "saas") { + if (noop()) { return null; } await this.updateDaily(orgId, featureId); // Ensure daily usage is updated @@ -685,7 +708,9 @@ export class UsageService { */ private async uploadOldEventFiles(): Promise { if (!this.eventsDir || !this.bucketName) { - logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload."); + logger.warn( + "Stripe local file path or bucket name is not configured, skipping old event file upload." + ); return; } try { @@ -693,15 +718,17 @@ export class UsageService { const now = Date.now(); for (const file of files) { if (!file.endsWith(".json")) continue; - + // Skip files that are already being uploaded if (this.uploadingFiles.has(file)) { - logger.debug(`Skipping file ${file} as it's already being uploaded`); + logger.debug( + `Skipping file ${file} as it's already being uploaded` + ); continue; } const filePath = path.join(this.eventsDir, file); - + try { // Check if file still exists before processing try { @@ -716,7 +743,7 @@ export class UsageService { if (age >= 90000) { // 1.5 minutes - Mark as being uploaded this.uploadingFiles.add(file); - + try { const fileContent = await fs.readFile( filePath, @@ -732,15 +759,17 @@ export class UsageService { ContentType: "application/json" }); await s3Client.send(uploadCommand); - + // Check if file still exists before unlinking try { await fs.access(filePath); await fs.unlink(filePath); } catch (unlinkError) { - logger.debug(`File ${file} was already deleted during interval upload`); + logger.debug( + `File ${file} was already deleted during interval upload` + ); } - + logger.info( `Interval: Uploaded event file ${file} to S3 with ${events.length} events` ); @@ -755,7 +784,9 @@ export class UsageService { await fs.access(filePath); await fs.unlink(filePath); } catch (unlinkError) { - logger.debug(`Empty file ${file} was already deleted`); + logger.debug( + `Empty file ${file} was already deleted` + ); } } } finally { @@ -777,12 +808,17 @@ export class UsageService { } } - public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise { - if (build !== "saas") { + public async checkLimitSet( + orgId: string, + kickSites = false, + featureId?: FeatureId, + usage?: Usage + ): Promise { + if (noop()) { return false; } // This method should check the current usage against the limits set for the organization - // and kick out all of the sites on the org + // and kick out all of the sites on the org let hasExceededLimits = false; try { @@ -817,16 +853,30 @@ export class UsageService { if (usage) { currentUsage = usage; } else { - currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId); + currentUsage = await this.getUsage( + orgId, + limit.featureId as FeatureId + ); } - const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0; - logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`); - logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`); - if (currentUsage && limit.value !== null && usageValue > limit.value) { + const usageValue = + currentUsage?.instantaneousValue || + currentUsage?.latestValue || + 0; + logger.debug( + `Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}` + ); + logger.debug( + `Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}` + ); + if ( + currentUsage && + limit.value !== null && + usageValue > limit.value + ) { logger.debug( `Org ${orgId} has exceeded limit for ${limit.featureId}: ` + - `${usageValue} > ${limit.value}` + `${usageValue} > ${limit.value}` ); hasExceededLimits = true; break; // Exit early if any limit is exceeded @@ -835,7 +885,9 @@ export class UsageService { // If any limits are exceeded, disconnect all sites for this organization if (hasExceededLimits && kickSites) { - logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`); + logger.warn( + `Disconnecting all sites for org ${orgId} due to exceeded limits` + ); // Get all sites for this organization const orgSites = await db @@ -844,7 +896,7 @@ export class UsageService { .where(eq(sites.orgId, orgId)); // Mark all sites as offline and send termination messages - const siteUpdates = orgSites.map(site => site.siteId); + const siteUpdates = orgSites.map((site) => site.siteId); if (siteUpdates.length > 0) { // Send termination messages to newt sites @@ -865,17 +917,21 @@ export class UsageService { }; // Don't await to prevent blocking - sendToClient(newt.newtId, payload).catch((error: any) => { - logger.error( - `Failed to send termination message to newt ${newt.newtId}:`, - error - ); - }); + sendToClient(newt.newtId, payload).catch( + (error: any) => { + logger.error( + `Failed to send termination message to newt ${newt.newtId}:`, + error + ); + } + ); } } } - logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`); + logger.info( + `Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits` + ); } } } catch (error) { diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts index f69e4854..2afba84c 100644 --- a/server/lib/blueprints/applyNewtDockerBlueprint.ts +++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts @@ -1,4 +1,4 @@ -import { sendToClient } from "@server/routers/ws"; +import { sendToClient } from "#dynamic/routers/ws"; import { processContainerLabels } from "./parseDockerContainers"; import { applyBlueprint } from "./applyBlueprint"; import { db, sites } from "@server/db"; diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 407c6019..a31cfb9d 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -25,7 +25,7 @@ import { TargetData } from "./types"; import logger from "@server/logger"; -import { createCertificate } from "@server/routers/private/certificates/createCertificate"; +import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { pickPort } from "@server/routers/target/helpers"; import { resourcePassword } from "@server/db"; import { hashPassword } from "@server/auth/password"; diff --git a/server/lib/certificates.ts b/server/lib/certificates.ts new file mode 100644 index 00000000..a6c51c96 --- /dev/null +++ b/server/lib/certificates.ts @@ -0,0 +1,13 @@ +export async function getValidCertificatesForDomains(domains: Set): Promise< + Array<{ + id: number; + domain: string; + wildcard: boolean | null; + certFile: string | null; + keyFile: string | null; + expiresAt: number | null; + updatedAt?: number | null; + }> +> { + return []; // stub +} \ No newline at end of file diff --git a/server/lib/config.ts b/server/lib/config.ts index 8b084e62..22c30338 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -3,19 +3,14 @@ import { __DIRNAME, APP_VERSION } from "@server/lib/consts"; import { db } from "@server/db"; import { SupporterKey, supporterKey } from "@server/db"; import { eq } from "drizzle-orm"; -import { license } from "@server/license/license"; +import { license } from "#dynamic/license/license"; import { configSchema, readConfigFile } from "./readConfigFile"; import { fromError } from "zod-validation-error"; -import { - privateConfigSchema, - readPrivateConfigFile -} from "@server/lib/private/readConfigFile"; -import logger from "@server/logger"; import { build } from "@server/build"; +import logger from "@server/logger"; export class Config { private rawConfig!: z.infer; - private rawPrivateConfig!: z.infer; supporterData: SupporterKey | null = null; @@ -37,19 +32,6 @@ export class Config { throw new Error(`Invalid configuration file: ${errors}`); } - const privateEnvironment = readPrivateConfigFile(); - - const { - data: parsedPrivateConfig, - success: privateSuccess, - error: privateError - } = privateConfigSchema.safeParse(privateEnvironment); - - if (!privateSuccess) { - const errors = fromError(privateError); - throw new Error(`Invalid private configuration file: ${errors}`); - } - if ( // @ts-ignore parsedConfig.users || @@ -109,132 +91,29 @@ export class Config { ? "true" : "false"; - if (parsedPrivateConfig.branding?.colors) { - process.env.BRANDING_COLORS = JSON.stringify( - parsedPrivateConfig.branding?.colors - ); - } - - if (parsedPrivateConfig.branding?.logo?.light_path) { - process.env.BRANDING_LOGO_LIGHT_PATH = - parsedPrivateConfig.branding?.logo?.light_path; - } - if (parsedPrivateConfig.branding?.logo?.dark_path) { - process.env.BRANDING_LOGO_DARK_PATH = - parsedPrivateConfig.branding?.logo?.dark_path || undefined; - } - - process.env.HIDE_SUPPORTER_KEY = parsedPrivateConfig.flags - ?.hide_supporter_key - ? "true" - : "false"; - - if (build != "oss") { - if (parsedPrivateConfig.branding?.logo?.light_path) { - process.env.BRANDING_LOGO_LIGHT_PATH = - parsedPrivateConfig.branding?.logo?.light_path; - } - if (parsedPrivateConfig.branding?.logo?.dark_path) { - process.env.BRANDING_LOGO_DARK_PATH = - parsedPrivateConfig.branding?.logo?.dark_path || undefined; - } - - process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding - ?.logo?.auth_page?.width - ? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString() - : undefined; - process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding - ?.logo?.auth_page?.height - ? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString() - : undefined; - - process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig - .branding?.logo?.navbar?.width - ? parsedPrivateConfig.branding?.logo?.navbar?.width.toString() - : undefined; - process.env.BRANDING_LOGO_NAVBAR_HEIGHT = parsedPrivateConfig - .branding?.logo?.navbar?.height - ? parsedPrivateConfig.branding?.logo?.navbar?.height.toString() - : undefined; - - process.env.BRANDING_FAVICON_PATH = - parsedPrivateConfig.branding?.favicon_path; - - process.env.BRANDING_APP_NAME = - parsedPrivateConfig.branding?.app_name || "Pangolin"; - - if (parsedPrivateConfig.branding?.footer) { - process.env.BRANDING_FOOTER = JSON.stringify( - parsedPrivateConfig.branding?.footer - ); - } - - process.env.LOGIN_PAGE_TITLE_TEXT = - parsedPrivateConfig.branding?.login_page?.title_text || ""; - process.env.LOGIN_PAGE_SUBTITLE_TEXT = - parsedPrivateConfig.branding?.login_page?.subtitle_text || ""; - - process.env.SIGNUP_PAGE_TITLE_TEXT = - parsedPrivateConfig.branding?.signup_page?.title_text || ""; - process.env.SIGNUP_PAGE_SUBTITLE_TEXT = - parsedPrivateConfig.branding?.signup_page?.subtitle_text || ""; - - process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY = - parsedPrivateConfig.branding?.resource_auth_page - ?.hide_powered_by === true - ? "true" - : "false"; - process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO = - parsedPrivateConfig.branding?.resource_auth_page?.show_logo === - true - ? "true" - : "false"; - process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT = - parsedPrivateConfig.branding?.resource_auth_page?.title_text || - ""; - process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT = - parsedPrivateConfig.branding?.resource_auth_page - ?.subtitle_text || ""; - - if (parsedPrivateConfig.branding?.background_image_path) { - process.env.BACKGROUND_IMAGE_PATH = - parsedPrivateConfig.branding?.background_image_path; - } - - if (parsedPrivateConfig.server.reo_client_id) { - process.env.REO_CLIENT_ID = - parsedPrivateConfig.server.reo_client_id; - } - } - if (parsedConfig.server.maxmind_db_path) { process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; } this.rawConfig = parsedConfig; - this.rawPrivateConfig = parsedPrivateConfig; } public async initServer() { if (!this.rawConfig) { throw new Error("Config not loaded. Call load() first."); } - if (this.rawConfig.managed) { - // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED - return; - } + license.setServerSecret(this.rawConfig.server.secret!); await this.checkKeyStatus(); } private async checkKeyStatus() { - const licenseStatus = await license.check(); - if ( - !this.rawPrivateConfig.flags?.hide_supporter_key && - build != "oss" && - !licenseStatus.isHostLicensed - ) { + if (build === "enterprise") { + await license.check(); + } + + if (build == "oss") { this.checkSupporterKey(); } } @@ -243,10 +122,6 @@ export class Config { return this.rawConfig; } - public getRawPrivateConfig() { - return this.rawPrivateConfig; - } - public getNoReplyEmail(): string | undefined { return ( this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user @@ -280,10 +155,6 @@ export class Config { return false; } - public isManagedMode() { - return typeof this.rawConfig?.managed === "object"; - } - public async checkSupporterKey() { const [key] = await db.select().from(supporterKey).limit(1); diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 544123a9..8ad98167 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.10.4"; +export const APP_VERSION = "1.11.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/middlewares/private/corsWithLoginPage.ts b/server/lib/corsWithLoginPage.ts similarity index 88% rename from server/middlewares/private/corsWithLoginPage.ts rename to server/lib/corsWithLoginPage.ts index 95867fa1..43b26264 100644 --- a/server/middlewares/private/corsWithLoginPage.ts +++ b/server/lib/corsWithLoginPage.ts @@ -1,16 +1,3 @@ -/* - * 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 cors, { CorsOptions } from "cors"; import config from "@server/lib/config"; diff --git a/server/lib/private/createUserAccountOrg.ts b/server/lib/createUserAccountOrg.ts similarity index 89% rename from server/lib/private/createUserAccountOrg.ts rename to server/lib/createUserAccountOrg.ts index abde5ca7..1406b935 100644 --- a/server/lib/private/createUserAccountOrg.ts +++ b/server/lib/createUserAccountOrg.ts @@ -1,16 +1,3 @@ -/* - * 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 { isValidCIDR } from "@server/lib/validators"; import { getNextAvailableOrgSubnet } from "@server/lib/ip"; import { @@ -28,9 +15,9 @@ import { } from "@server/db"; import { eq } from "drizzle-orm"; import { defaultRoleAllowedActions } from "@server/routers/role"; -import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/private/billing"; -import { createCustomer } from "@server/routers/private/billing/createCustomer"; -import { usageService } from "@server/lib/private/billing/usageService"; +import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing"; +import { createCustomer } from "#dynamic/lib/billing"; +import { usageService } from "@server/lib/billing/usageService"; export async function createUserAccountOrg( userId: string, diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts index 8372d675..bb269710 100644 --- a/server/lib/exitNodes/exitNodes.ts +++ b/server/lib/exitNodes/exitNodes.ts @@ -16,7 +16,11 @@ export async function verifyExitNodeOrgAccess( return { hasAccess: true, exitNode }; } -export async function listExitNodes(orgId: string, filterOnline = false, noCloud = false) { +export async function listExitNodes( + orgId: string, + filterOnline = false, + noCloud = false +) { // TODO: pick which nodes to send and ping better than just all of them that are not remote const allExitNodes = await db .select({ @@ -59,7 +63,16 @@ export async function checkExitNodeOrg(exitNodeId: number, orgId: string) { return false; } -export async function resolveExitNodes(hostname: string, publicKey: string) { +export async function resolveExitNodes( + hostname: string, + publicKey: string +): Promise< + { + endpoint: string; + publicKey: string; + orgId: string; + }[] +> { // OSS version: simple implementation that returns empty array return []; } diff --git a/server/lib/exitNodes/index.ts b/server/lib/exitNodes/index.ts index dda94368..ba30ccc2 100644 --- a/server/lib/exitNodes/index.ts +++ b/server/lib/exitNodes/index.ts @@ -1,33 +1,4 @@ -import { build } from "@server/build"; - -// Import both modules -import * as exitNodesModule from "./exitNodes"; -import * as privateExitNodesModule from "./privateExitNodes"; - -// Conditionally export exit nodes implementation based on build type -const exitNodesImplementation = build === "oss" ? exitNodesModule : privateExitNodesModule; - -// Re-export all items from the selected implementation -export const { - verifyExitNodeOrgAccess, - listExitNodes, - selectBestExitNode, - checkExitNodeOrg, - resolveExitNodes -} = exitNodesImplementation; - -// Import communications modules -import * as exitNodeCommsModule from "./exitNodeComms"; -import * as privateExitNodeCommsModule from "./privateExitNodeComms"; - -// Conditionally export communications implementation based on build type -const exitNodeCommsImplementation = build === "oss" ? exitNodeCommsModule : privateExitNodeCommsModule; - -// Re-export communications functions from the selected implementation -export const { - sendToExitNode -} = exitNodeCommsImplementation; - -// Re-export shared modules +export * from "./exitNodes"; +export * from "./exitNodeComms"; export * from "./subnet"; export * from "./getCurrentExitNodeId"; \ No newline at end of file diff --git a/server/lib/geoip.ts b/server/lib/geoip.ts index d6252360..ac739fa3 100644 --- a/server/lib/geoip.ts +++ b/server/lib/geoip.ts @@ -1,8 +1,5 @@ import logger from "@server/logger"; import { maxmindLookup } from "@server/db/maxmind"; -import axios from "axios"; -import config from "./config"; -import { tokenManager } from "./tokenManager"; export async function getCountryCodeForIp( ip: string @@ -33,32 +30,4 @@ export async function getCountryCodeForIp( } return; -} - -export async function remoteGetCountryCodeForIp( - ip: string -): Promise { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`, - await tokenManager.getAuthHeader() - ); - - return response.data.data.countryCode; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - } - - return; -} +} \ No newline at end of file diff --git a/server/lib/private/rateLimitStore.ts b/server/lib/private/rateLimitStore.ts deleted file mode 100644 index 4700ba09..00000000 --- a/server/lib/private/rateLimitStore.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - -import RedisStore from "@server/db/private/redisStore"; -import { MemoryStore, Store } from "express-rate-limit"; - -export function createStore(): Store { - const rateLimitStore: Store = new RedisStore({ - prefix: 'api-rate-limit', // Optional: customize Redis key prefix - skipFailedRequests: true, // Don't count failed requests - skipSuccessfulRequests: false, // Count successful requests - }); - - return rateLimitStore; -} diff --git a/server/lib/private/readConfigFile.ts b/server/lib/private/readConfigFile.ts deleted file mode 100644 index ddc9ee82..00000000 --- a/server/lib/private/readConfigFile.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - -import fs from "fs"; -import yaml from "js-yaml"; -import { privateConfigFilePath1 } from "@server/lib/consts"; -import { z } from "zod"; -import { colorsSchema } from "@server/lib/colorsSchema"; -import { build } from "@server/build"; - -const portSchema = z.number().positive().gt(0).lte(65535); - -export const privateConfigSchema = z - .object({ - app: z.object({ - region: z.string().optional().default("default"), - base_domain: z.string().optional() - }).optional().default({ - region: "default" - }), - server: z.object({ - encryption_key_path: z - .string() - .optional() - .default("./config/encryption.pem") - .pipe(z.string().min(8)), - resend_api_key: z.string().optional(), - reo_client_id: z.string().optional(), - }).optional().default({ - encryption_key_path: "./config/encryption.pem" - }), - redis: z - .object({ - host: z.string(), - port: portSchema, - password: z.string().optional(), - db: z.number().int().nonnegative().optional().default(0), - replicas: z - .array( - z.object({ - host: z.string(), - port: portSchema, - password: z.string().optional(), - db: z.number().int().nonnegative().optional().default(0) - }) - ) - .optional() - // tls: z - // .object({ - // reject_unauthorized: z - // .boolean() - // .optional() - // .default(true) - // }) - // .optional() - }) - .optional(), - gerbil: z - .object({ - local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003") - }) - .optional() - .default({}), - flags: z - .object({ - enable_redis: z.boolean().optional(), - hide_supporter_key: z.boolean().optional() - }) - .optional(), - branding: z - .object({ - app_name: z.string().optional(), - background_image_path: z.string().optional(), - colors: z - .object({ - light: colorsSchema.optional(), - dark: colorsSchema.optional() - }) - .optional(), - logo: z - .object({ - light_path: z.string().optional(), - dark_path: z.string().optional(), - auth_page: z - .object({ - width: z.number().optional(), - height: z.number().optional() - }) - .optional(), - navbar: z - .object({ - width: z.number().optional(), - height: z.number().optional() - }) - .optional() - }) - .optional(), - favicon_path: z.string().optional(), - footer: z - .array( - z.object({ - text: z.string(), - href: z.string().optional() - }) - ) - .optional(), - login_page: z - .object({ - subtitle_text: z.string().optional(), - title_text: z.string().optional() - }) - .optional(), - signup_page: z - .object({ - subtitle_text: z.string().optional(), - title_text: z.string().optional() - }) - .optional(), - resource_auth_page: z - .object({ - show_logo: z.boolean().optional(), - hide_powered_by: z.boolean().optional(), - title_text: z.string().optional(), - subtitle_text: z.string().optional() - }) - .optional(), - emails: z - .object({ - signature: z.string().optional(), - colors: z - .object({ - primary: z.string().optional() - }) - .optional() - }) - .optional() - }) - .optional(), - stripe: z - .object({ - secret_key: z.string(), - webhook_secret: z.string(), - s3Bucket: z.string(), - s3Region: z.string().default("us-east-1"), - localFilePath: z.string() - }) - .optional(), - }); - -export function readPrivateConfigFile() { - if (build == "oss") { - return {}; - } - - const loadConfig = (configPath: string) => { - try { - const yamlContent = fs.readFileSync(configPath, "utf8"); - const config = yaml.load(yamlContent); - return config; - } catch (error) { - if (error instanceof Error) { - throw new Error( - `Error loading configuration file: ${error.message}` - ); - } - throw error; - } - }; - - let environment: any; - if (fs.existsSync(privateConfigFilePath1)) { - environment = loadConfig(privateConfigFilePath1); - } - - if (!environment) { - throw new Error( - "No private configuration file found." - ); - } - - return environment; -} diff --git a/server/lib/private/s3.ts b/server/lib/private/s3.ts deleted file mode 100644 index 26b1d49b..00000000 --- a/server/lib/private/s3.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - -import { S3Client } from "@aws-sdk/client-s3"; -import config from "@server/lib/config"; - -export const s3Client = new S3Client({ - region: config.getRawPrivateConfig().stripe?.s3Region || "us-east-1", -}); diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index ea872252..d614103a 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -12,42 +12,36 @@ const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { export const configSchema = z .object({ - app: z.object({ - dashboard_url: z - .string() - .url() - .pipe(z.string().url()) - .transform((url) => url.toLowerCase()) - .optional(), - log_level: z - .enum(["debug", "info", "warn", "error"]) - .optional() - .default("info"), - save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false), - telemetry: z - .object({ - anonymous_usage: z.boolean().optional().default(true) - }) - .optional() - .default({}), - }).optional().default({ - log_level: "info", - save_logs: false, - log_failed_attempts: false, - telemetry: { - anonymous_usage: true - } - }), - managed: z + app: z .object({ - name: z.string().optional(), - id: z.string().optional(), - secret: z.string().optional(), - endpoint: z.string().optional().default("https://pangolin.fossorial.io"), - redirect_endpoint: z.string().optional() + dashboard_url: z + .string() + .url() + .pipe(z.string().url()) + .transform((url) => url.toLowerCase()) + .optional(), + log_level: z + .enum(["debug", "info", "warn", "error"]) + .optional() + .default("info"), + save_logs: z.boolean().optional().default(false), + log_failed_attempts: z.boolean().optional().default(false), + telemetry: z + .object({ + anonymous_usage: z.boolean().optional().default(true) + }) + .optional() + .default({}) }) - .optional(), + .optional() + .default({ + log_level: "info", + save_logs: false, + log_failed_attempts: false, + telemetry: { + anonymous_usage: true + } + }), domains: z .record( z.string(), @@ -61,94 +55,95 @@ export const configSchema = z }) ) .optional(), - server: z.object({ - integration_port: portSchema - .optional() - .default(3003) - .transform(stoi) - .pipe(portSchema.optional()), - external_port: portSchema - .optional() - .default(3000) - .transform(stoi) - .pipe(portSchema), - internal_port: portSchema - .optional() - .default(3001) - .transform(stoi) - .pipe(portSchema), - next_port: portSchema - .optional() - .default(3002) - .transform(stoi) - .pipe(portSchema), - internal_hostname: z - .string() - .optional() - .default("pangolin") - .transform((url) => url.toLowerCase()), - session_cookie_name: z - .string() - .optional() - .default("p_session_token"), - resource_access_token_param: z - .string() - .optional() - .default("p_token"), - resource_access_token_headers: z - .object({ - id: z.string().optional().default("P-Access-Token-Id"), - token: z.string().optional().default("P-Access-Token") - }) - .optional() - .default({}), - resource_session_request_param: z - .string() - .optional() - .default("resource_session_request_param"), - dashboard_session_length_hours: z - .number() - .positive() - .gt(0) - .optional() - .default(720), - resource_session_length_hours: z - .number() - .positive() - .gt(0) - .optional() - .default(720), - cors: z - .object({ - origins: z.array(z.string()).optional(), - methods: z.array(z.string()).optional(), - allowed_headers: z.array(z.string()).optional(), - credentials: z.boolean().optional() - }) - .optional(), - trust_proxy: z.number().int().gte(0).optional().default(1), - secret: z - .string() - .pipe(z.string().min(8)) - .optional(), - maxmind_db_path: z.string().optional() - }).optional().default({ - integration_port: 3003, - external_port: 3000, - internal_port: 3001, - next_port: 3002, - internal_hostname: "pangolin", - session_cookie_name: "p_session_token", - resource_access_token_param: "p_token", - resource_access_token_headers: { - id: "P-Access-Token-Id", - token: "P-Access-Token" - }, - resource_session_request_param: "resource_session_request_param", - dashboard_session_length_hours: 720, - resource_session_length_hours: 720, - trust_proxy: 1 - }), + server: z + .object({ + integration_port: portSchema + .optional() + .default(3003) + .transform(stoi) + .pipe(portSchema.optional()), + external_port: portSchema + .optional() + .default(3000) + .transform(stoi) + .pipe(portSchema), + internal_port: portSchema + .optional() + .default(3001) + .transform(stoi) + .pipe(portSchema), + next_port: portSchema + .optional() + .default(3002) + .transform(stoi) + .pipe(portSchema), + internal_hostname: z + .string() + .optional() + .default("pangolin") + .transform((url) => url.toLowerCase()), + session_cookie_name: z + .string() + .optional() + .default("p_session_token"), + resource_access_token_param: z + .string() + .optional() + .default("p_token"), + resource_access_token_headers: z + .object({ + id: z.string().optional().default("P-Access-Token-Id"), + token: z.string().optional().default("P-Access-Token") + }) + .optional() + .default({}), + resource_session_request_param: z + .string() + .optional() + .default("resource_session_request_param"), + dashboard_session_length_hours: z + .number() + .positive() + .gt(0) + .optional() + .default(720), + resource_session_length_hours: z + .number() + .positive() + .gt(0) + .optional() + .default(720), + cors: z + .object({ + origins: z.array(z.string()).optional(), + methods: z.array(z.string()).optional(), + allowed_headers: z.array(z.string()).optional(), + credentials: z.boolean().optional() + }) + .optional(), + trust_proxy: z.number().int().gte(0).optional().default(1), + secret: z.string().pipe(z.string().min(8)).optional(), + maxmind_db_path: z.string().optional() + }) + .optional() + .default({ + integration_port: 3003, + external_port: 3000, + internal_port: 3001, + next_port: 3002, + internal_hostname: "pangolin", + session_cookie_name: "p_session_token", + resource_access_token_param: "p_token", + resource_access_token_headers: { + id: "P-Access-Token-Id", + token: "P-Access-Token" + }, + resource_session_request_param: + "resource_session_request_param", + dashboard_session_length_hours: 720, + resource_session_length_hours: 720, + trust_proxy: 1 + }), postgres: z .object({ connection_string: z.string().optional(), @@ -161,18 +156,29 @@ export const configSchema = z .optional(), pool: z .object({ - max_connections: z.number().positive().optional().default(20), - max_replica_connections: z.number().positive().optional().default(10), - idle_timeout_ms: z.number().positive().optional().default(30000), - connection_timeout_ms: z.number().positive().optional().default(5000) + max_connections: z + .number() + .positive() + .optional() + .default(20), + max_replica_connections: z + .number() + .positive() + .optional() + .default(10), + idle_timeout_ms: z + .number() + .positive() + .optional() + .default(30000), + connection_timeout_ms: z + .number() + .positive() + .optional() + .default(5000) }) .optional() - .default({ - max_connections: 20, - max_replica_connections: 10, - idle_timeout_ms: 30000, - connection_timeout_ms: 5000 - }) + .default({}) }) .optional(), traefik: z @@ -193,7 +199,10 @@ export const configSchema = z .optional() .default("/var/dynamic/router_config.yml"), static_domains: z.array(z.string()).optional().default([]), - site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]), + site_types: z + .array(z.string()) + .optional() + .default(["newt", "wireguard", "local"]), allow_raw_resources: z.boolean().optional().default(true), file_mode: z.boolean().optional().default(false) }) @@ -320,10 +329,7 @@ export const configSchema = z if (data.flags?.disable_config_managed_domains) { return true; } - // If hybrid is defined, domains are not required - if (data.managed) { - return true; - } + if (keys.length === 0) { return false; } @@ -335,15 +341,14 @@ export const configSchema = z ) .refine( (data) => { - // If hybrid is defined, server secret is not required - if (data.managed) { - return true; - } // If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env if (data.server?.secret === undefined) { data.server.secret = process.env.SERVER_SECRET; } - return data.server?.secret !== undefined && data.server.secret.length > 0; + return ( + data.server?.secret !== undefined && + data.server.secret.length > 0 + ); }, { message: "Server secret must be defined" @@ -351,12 +356,11 @@ export const configSchema = z ) .refine( (data) => { - // If hybrid is defined, dashboard_url is not required - if (data.managed) { - return true; - } // If hybrid is not defined, dashboard_url must be defined - return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0; + return ( + data.app.dashboard_url !== undefined && + data.app.dashboard_url.length > 0 + ); }, { message: "Dashboard URL must be defined" diff --git a/server/lib/remoteCertificates/certificates.ts b/server/lib/remoteCertificates/certificates.ts deleted file mode 100644 index 6404ee75..00000000 --- a/server/lib/remoteCertificates/certificates.ts +++ /dev/null @@ -1,80 +0,0 @@ -import axios from "axios"; -import { tokenManager } from "../tokenManager"; -import logger from "@server/logger"; -import config from "../config"; - -/** - * Get valid certificates for the specified domains - */ -export async function getValidCertificatesForDomainsHybrid(domains: Set): Promise< - Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: number | null; - updatedAt?: number | null; - }> -> { - if (domains.size === 0) { - return []; - } - - const domainArray = Array.from(domains); - - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`, - { - params: { - domains: domainArray - }, - headers: (await tokenManager.getAuthHeader()).headers - } - ); - - if (response.status !== 200) { - logger.error( - `Failed to fetch certificates for domains: ${response.status} ${response.statusText}`, - { responseData: response.data, domains: domainArray } - ); - return []; - } - - // logger.debug( - // `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains` - // ); - - return response.data.data; - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error getting certificates:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error getting certificates:", error); - } - return []; - } -} - -export async function getValidCertificatesForDomains(domains: Set): Promise< - Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: number | null; - updatedAt?: number | null; - }> -> { - return []; // stub -} \ No newline at end of file diff --git a/server/lib/remoteCertificates/index.ts b/server/lib/remoteCertificates/index.ts deleted file mode 100644 index fcd43d30..00000000 --- a/server/lib/remoteCertificates/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { build } from "@server/build"; - -// Import both modules -import * as certificateModule from "./certificates"; -import * as privateCertificateModule from "./privateCertificates"; - -// Conditionally export Remote Certificates implementation based on build type -const remoteCertificatesImplementation = build === "oss" ? certificateModule : privateCertificateModule; - -// Re-export all items from the selected implementation -export const { - getValidCertificatesForDomains, - getValidCertificatesForDomainsHybrid - } = remoteCertificatesImplementation; \ No newline at end of file diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts deleted file mode 100644 index c9016071..00000000 --- a/server/lib/remoteProxy.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { Router } from "express"; -import axios from "axios"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import config from "@server/lib/config"; -import { tokenManager } from "./tokenManager"; - -/** - * Proxy function that forwards requests to the remote cloud server - */ - -export const proxyToRemote = async ( - req: Request, - res: Response, - next: NextFunction, - endpoint: string -): Promise => { - try { - const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`; - - logger.debug(`Proxying request to remote server: ${remoteUrl}`); - - // Forward the request to the remote server - const response = await axios({ - method: req.method as any, - url: remoteUrl, - data: req.body, - headers: { - 'Content-Type': 'application/json', - ...(await tokenManager.getAuthHeader()).headers - }, - params: req.query, - timeout: 30000, // 30 second timeout - validateStatus: () => true // Don't throw on non-2xx status codes - }); - - logger.debug(`Proxy response: ${JSON.stringify(response.data)}`); - - // Forward the response status and data - return res.status(response.status).json(response.data); - - } catch (error) { - logger.error("Error proxying request to remote server:", error); - - if (axios.isAxiosError(error)) { - if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { - return next( - createHttpError( - HttpCode.SERVICE_UNAVAILABLE, - "Remote server is unavailable" - ) - ); - } - if (error.code === 'ECONNABORTED') { - return next( - createHttpError( - HttpCode.REQUEST_TIMEOUT, - "Request to remote server timed out" - ) - ); - } - } - - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error communicating with remote server" - ) - ); - } -}; \ No newline at end of file diff --git a/server/lib/resend.ts b/server/lib/resend.ts new file mode 100644 index 00000000..7dd130c8 --- /dev/null +++ b/server/lib/resend.ts @@ -0,0 +1,15 @@ +export enum AudienceIds { + General = "", + Subscribed = "", + Churned = "" +} + +let resend; +export default resend; + +export async function moveEmailToAudience( + email: string, + audienceId: AudienceIds +) { + return; +} \ No newline at end of file diff --git a/server/lib/s3.ts b/server/lib/s3.ts new file mode 100644 index 00000000..5fc3318f --- /dev/null +++ b/server/lib/s3.ts @@ -0,0 +1,5 @@ +import { S3Client } from "@aws-sdk/client-s3"; + +export const s3Client = new S3Client({ + region: process.env.S3_REGION || "us-east-1", +}); diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts index 827f1c7e..0e0ae24e 100644 --- a/server/lib/telemetry.ts +++ b/server/lib/telemetry.ts @@ -9,6 +9,7 @@ import { APP_VERSION } from "./consts"; import crypto from "crypto"; import { UserType } from "@server/types/UserTypes"; import { build } from "@server/build"; +import license from "@server/license/license"; class TelemetryClient { private client: PostHog | null = null; @@ -176,17 +177,36 @@ class TelemetryClient { const stats = await this.getSystemStats(); - this.client.capture({ - distinctId: hostMeta.hostMetaId, - event: "supporter_status", - properties: { - valid: stats.supporterStatus.valid, - tier: stats.supporterStatus.tier, - github_username: stats.supporterStatus.githubUsername - ? this.anon(stats.supporterStatus.githubUsername) - : "None" - } - }); + if (build === "enterprise") { + const licenseStatus = await license.check(); + const payload = { + distinctId: hostMeta.hostMetaId, + event: "enterprise_status", + properties: { + is_host_licensed: licenseStatus.isHostLicensed, + is_license_valid: licenseStatus.isLicenseValid, + license_tier: licenseStatus.tier || "unknown" + } + }; + logger.debug("Sending enterprise startup telemtry payload:", { + payload + }); + // this.client.capture(payload); + } + + if (build === "oss") { + this.client.capture({ + distinctId: hostMeta.hostMetaId, + event: "supporter_status", + properties: { + valid: stats.supporterStatus.valid, + tier: stats.supporterStatus.tier, + github_username: stats.supporterStatus.githubUsername + ? this.anon(stats.supporterStatus.githubUsername) + : "None" + } + }); + } this.client.capture({ distinctId: hostMeta.hostMetaId, diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts deleted file mode 100644 index 2e0e1118..00000000 --- a/server/lib/tokenManager.ts +++ /dev/null @@ -1,274 +0,0 @@ -import axios from "axios"; -import config from "@server/lib/config"; -import logger from "@server/logger"; - -export interface TokenResponse { - success: boolean; - message?: string; - data: { - token: string; - }; -} - -/** - * Token Manager - Handles automatic token refresh for hybrid server authentication - * - * Usage throughout the application: - * ```typescript - * import { tokenManager } from "@server/lib/tokenManager"; - * - * // Get the current valid token - * const token = await tokenManager.getToken(); - * - * // Force refresh if needed - * await tokenManager.refreshToken(); - * ``` - * - * The token manager automatically refreshes tokens every 24 hours by default - * and is started once in the privateHybridServer.ts file. - */ - -export class TokenManager { - private token: string | null = null; - private refreshInterval: NodeJS.Timeout | null = null; - private isRefreshing: boolean = false; - private refreshIntervalMs: number; - private retryInterval: NodeJS.Timeout | null = null; - private retryIntervalMs: number; - private tokenAvailablePromise: Promise | null = null; - private tokenAvailableResolve: (() => void) | null = null; - - constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) { - // Default to 24 hours for refresh, 5 seconds for retry - this.refreshIntervalMs = refreshIntervalMs; - this.retryIntervalMs = retryIntervalMs; - this.setupTokenAvailablePromise(); - } - - /** - * Set up promise that resolves when token becomes available - */ - private setupTokenAvailablePromise(): void { - this.tokenAvailablePromise = new Promise((resolve) => { - this.tokenAvailableResolve = resolve; - }); - } - - /** - * Resolve the token available promise - */ - private resolveTokenAvailable(): void { - if (this.tokenAvailableResolve) { - this.tokenAvailableResolve(); - this.tokenAvailableResolve = null; - } - } - - /** - * Start the token manager - gets initial token and sets up refresh interval - * If initial token fetch fails, keeps retrying every few seconds until successful - */ - async start(): Promise { - logger.info("Starting token manager..."); - - try { - await this.refreshToken(); - this.setupRefreshInterval(); - this.resolveTokenAvailable(); - logger.info("Token manager started successfully"); - } catch (error) { - logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error); - this.setupRetryInterval(); - } - } - - /** - * Set up retry interval for initial token acquisition - */ - private setupRetryInterval(): void { - if (this.retryInterval) { - clearInterval(this.retryInterval); - } - - this.retryInterval = setInterval(async () => { - try { - logger.debug("Retrying initial token acquisition"); - await this.refreshToken(); - this.setupRefreshInterval(); - this.clearRetryInterval(); - this.resolveTokenAvailable(); - logger.info("Token manager started successfully after retry"); - } catch (error) { - logger.debug("Token acquisition retry failed, will try again"); - } - }, this.retryIntervalMs); - } - - /** - * Clear retry interval - */ - private clearRetryInterval(): void { - if (this.retryInterval) { - clearInterval(this.retryInterval); - this.retryInterval = null; - } - } - - /** - * Stop the token manager and clear all intervals - */ - stop(): void { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - this.clearRetryInterval(); - logger.info("Token manager stopped"); - } - - /** - * Get the current valid token - */ - - // TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT - async getToken(): Promise { - // If we don't have a token yet, wait for it to become available - if (!this.token && this.tokenAvailablePromise) { - await this.tokenAvailablePromise; - } - - if (!this.token) { - if (this.isRefreshing) { - // Wait for current refresh to complete - await this.waitForRefresh(); - } else { - throw new Error("No valid token available"); - } - } - - if (!this.token) { - throw new Error("No valid token available"); - } - - return this.token; - } - - async getAuthHeader() { - return { - headers: { - Authorization: `Bearer ${await this.getToken()}`, - "X-CSRF-Token": "x-csrf-protection", - } - }; - } - - /** - * Force refresh the token - */ - async refreshToken(): Promise { - if (this.isRefreshing) { - await this.waitForRefresh(); - return; - } - - this.isRefreshing = true; - - try { - const hybridConfig = config.getRawConfig().managed; - - if ( - !hybridConfig?.id || - !hybridConfig?.secret || - !hybridConfig?.endpoint - ) { - throw new Error("Hybrid configuration is not defined"); - } - - const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`; - - const tokenData = { - remoteExitNodeId: hybridConfig.id, - secret: hybridConfig.secret - }; - - logger.debug("Requesting new token from server"); - - const response = await axios.post( - tokenEndpoint, - tokenData, - { - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": "x-csrf-protection" - }, - timeout: 10000 // 10 second timeout - } - ); - - if (!response.data.success) { - throw new Error( - `Failed to get token: ${response.data.message}` - ); - } - - if (!response.data.data.token) { - throw new Error("Received empty token from server"); - } - - this.token = response.data.data.token; - logger.debug("Token refreshed successfully"); - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error updating proxy mapping:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error updating proxy mapping:", error); - } - - throw new Error("Failed to refresh token"); - } finally { - this.isRefreshing = false; - } - } - - /** - * Set up automatic token refresh interval - */ - private setupRefreshInterval(): void { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - } - - this.refreshInterval = setInterval(async () => { - try { - logger.debug("Auto-refreshing token"); - await this.refreshToken(); - } catch (error) { - logger.error("Failed to auto-refresh token:", error); - } - }, this.refreshIntervalMs); - } - - /** - * Wait for current refresh operation to complete - */ - private async waitForRefresh(): Promise { - return new Promise((resolve) => { - const checkInterval = setInterval(() => { - if (!this.isRefreshing) { - clearInterval(checkInterval); - resolve(); - } - }, 100); - }); - } -} - -// Export a singleton instance for use throughout the application -export const tokenManager = new TokenManager(); diff --git a/server/lib/traefik/TraefikConfigManager.ts b/server/lib/traefik/TraefikConfigManager.ts index 51466ecf..ec4e25f4 100644 --- a/server/lib/traefik/TraefikConfigManager.ts +++ b/server/lib/traefik/TraefikConfigManager.ts @@ -6,14 +6,10 @@ import * as yaml from "js-yaml"; import axios from "axios"; import { db, exitNodes } from "@server/db"; import { eq } from "drizzle-orm"; -import { tokenManager } from "../tokenManager"; import { getCurrentExitNodeId } from "@server/lib/exitNodes"; -import { getTraefikConfig } from "./"; -import { - getValidCertificatesForDomains, - getValidCertificatesForDomainsHybrid -} from "../remoteCertificates"; -import { sendToExitNode } from "../exitNodes"; +import { getTraefikConfig } from "#dynamic/lib/traefik"; +import { getValidCertificatesForDomains } from "#dynamic/lib/certificates"; +import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; export class TraefikConfigManager { @@ -313,93 +309,92 @@ export class TraefikConfigManager { this.lastActiveDomains = new Set(domains); } - // Scan current local certificate state - this.lastLocalCertificateState = - await this.scanLocalCertificateState(); + if ( + process.env.USE_PANGOLIN_DNS === "true" && + build != "oss" + ) { + // Scan current local certificate state + this.lastLocalCertificateState = + await this.scanLocalCertificateState(); - // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) - let validCertificates: Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: number | null; - updatedAt?: number | null; - }> = []; + // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) + let validCertificates: Array<{ + id: number; + domain: string; + wildcard: boolean | null; + certFile: string | null; + keyFile: string | null; + expiresAt: number | null; + updatedAt?: number | null; + }> = []; - if (this.shouldFetchCertificates(domains)) { - // Filter out domains that are already covered by wildcard certificates - const domainsToFetch = new Set(); - for (const domain of domains) { - if ( - !isDomainCoveredByWildcard( - domain, - this.lastLocalCertificateState - ) - ) { - domainsToFetch.add(domain); - } else { - logger.debug( - `Domain ${domain} is covered by existing wildcard certificate, skipping fetch` - ); - } - } - - if (domainsToFetch.size > 0) { - // Get valid certificates for domains not covered by wildcards - if (config.isManagedMode()) { - validCertificates = - await getValidCertificatesForDomainsHybrid( - domainsToFetch + if (this.shouldFetchCertificates(domains)) { + // Filter out domains that are already covered by wildcard certificates + const domainsToFetch = new Set(); + for (const domain of domains) { + if ( + !isDomainCoveredByWildcard( + domain, + this.lastLocalCertificateState + ) + ) { + domainsToFetch.add(domain); + } else { + logger.debug( + `Domain ${domain} is covered by existing wildcard certificate, skipping fetch` ); - } else { + } + } + + if (domainsToFetch.size > 0) { + // Get valid certificates for domains not covered by wildcards validCertificates = await getValidCertificatesForDomains( domainsToFetch ); + this.lastCertificateFetch = new Date(); + this.lastKnownDomains = new Set(domains); + + logger.info( + `Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)` + ); + + // Download and decrypt new certificates + await this.processValidCertificates(validCertificates); + } else { + logger.info( + "All domains are covered by existing wildcard certificates, no fetch needed" + ); + this.lastCertificateFetch = new Date(); + this.lastKnownDomains = new Set(domains); } - this.lastCertificateFetch = new Date(); - this.lastKnownDomains = new Set(domains); - logger.info( - `Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)` - ); - - // Download and decrypt new certificates - await this.processValidCertificates(validCertificates); + // Always ensure all existing certificates (including wildcards) are in the config + await this.updateDynamicConfigFromLocalCerts(domains); } else { - logger.info( - "All domains are covered by existing wildcard certificates, no fetch needed" - ); - this.lastCertificateFetch = new Date(); - this.lastKnownDomains = new Set(domains); + const timeSinceLastFetch = this.lastCertificateFetch + ? Math.round( + (Date.now() - + this.lastCertificateFetch.getTime()) / + (1000 * 60) + ) + : 0; + + // logger.debug( + // `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` + // ); + + // Still need to ensure config is up to date with existing certificates + await this.updateDynamicConfigFromLocalCerts(domains); } - // Always ensure all existing certificates (including wildcards) are in the config - await this.updateDynamicConfigFromLocalCerts(domains); - } else { - const timeSinceLastFetch = this.lastCertificateFetch - ? Math.round( - (Date.now() - this.lastCertificateFetch.getTime()) / - (1000 * 60) - ) - : 0; + // Clean up certificates for domains no longer in use + await this.cleanupUnusedCertificates(domains); - // logger.debug( - // `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` - // ); - - // Still need to ensure config is up to date with existing certificates - await this.updateDynamicConfigFromLocalCerts(domains); + // wait 1 second for traefik to pick up the new certificates + await new Promise((resolve) => setTimeout(resolve, 500)); } - // Clean up certificates for domains no longer in use - await this.cleanupUnusedCertificates(domains); - - // wait 1 second for traefik to pick up the new certificates - await new Promise((resolve) => setTimeout(resolve, 500)); - // Write traefik config as YAML to a second dynamic config file if changed await this.writeTraefikDynamicConfig(traefikConfig); @@ -448,32 +443,15 @@ export class TraefikConfigManager { } | null> { let traefikConfig; try { - if (config.isManagedMode()) { - const resp = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`, - await tokenManager.getAuthHeader() - ); - - if (resp.status !== 200) { - logger.error( - `Failed to fetch traefik config: ${resp.status} ${resp.statusText}`, - { responseData: resp.data } - ); - return null; - } - - traefikConfig = resp.data.data; - } else { - const currentExitNode = await getCurrentExitNodeId(); - // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`); - traefikConfig = await getTraefikConfig( - // this is called by the local exit node to get its own config - currentExitNode, - config.getRawConfig().traefik.site_types, - build == "oss", // filter out the namespace domains in open source - build != "oss" // generate the login pages on the cloud and hybrid - ); - } + const currentExitNode = await getCurrentExitNodeId(); + // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`); + traefikConfig = await getTraefikConfig( + // this is called by the local exit node to get its own config + currentExitNode, + config.getRawConfig().traefik.site_types, + build == "oss", // filter out the namespace domains in open source + build != "oss" // generate the login pages on the cloud and hybrid + ); const domains = new Set(); @@ -718,7 +696,12 @@ export class TraefikConfigManager { for (const cert of validCertificates) { try { - if (!cert.certFile || !cert.keyFile) { + if ( + !cert.certFile || + !cert.keyFile || + cert.certFile.length === 0 || + cert.keyFile.length === 0 + ) { logger.warn( `Certificate for domain ${cert.domain} is missing cert or key file` ); @@ -842,7 +825,9 @@ export class TraefikConfigManager { const lastUpdateStr = fs .readFileSync(lastUpdatePath, "utf8") .trim(); - lastUpdateTime = Math.floor(new Date(lastUpdateStr).getTime() / 1000); + lastUpdateTime = Math.floor( + new Date(lastUpdateStr).getTime() / 1000 + ); } catch { lastUpdateTime = null; } diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 7e1ce562..4055c7a0 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -1,9 +1,8 @@ -import { db, exitNodes, targetHealthCheck } from "@server/db"; +import { db, targetHealthCheck } from "@server/db"; import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm"; import logger from "@server/logger"; import config from "@server/lib/config"; -import { orgs, resources, sites, Target, targets } from "@server/db"; -import { build } from "@server/build"; +import { resources, sites, Target, targets } from "@server/db"; import createPathRewriteMiddleware from "./middleware"; import { sanitize, validatePathRewriteConfig } from "./utils"; @@ -105,7 +104,12 @@ export async function getTraefikConfig( const priority = row.priority ?? 100; // Create a unique key combining resourceId, path config, and rewrite config - const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType] + const pathKey = [ + targetPath, + pathMatchType, + rewritePath, + rewritePathType + ] .filter(Boolean) .join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); @@ -120,13 +124,15 @@ export async function getTraefikConfig( ); if (!validation.isValid) { - logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`); + logger.error( + `Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}` + ); return; } resourcesMap.set(key, { resourceId: row.resourceId, - name: resourceName, + name: resourceName, fullDomain: row.fullDomain, ssl: row.ssl, http: row.http, @@ -158,9 +164,6 @@ export async function getTraefikConfig( port: row.port, internalPort: row.internalPort, enabled: row.targetEnabled, - rewritePath: row.rewritePath, - rewritePathType: row.rewritePathType, - priority: row.priority, site: { siteId: row.siteId, type: row.siteType, @@ -239,21 +242,18 @@ export async function getTraefikConfig( preferWildcardCert = configDomain.prefer_wildcard_cert; } - let tls = {}; - if (build == "oss") { - tls = { - certResolver: certResolver, - ...(preferWildcardCert - ? { - domains: [ - { - main: wildCard - } - ] - } - : {}) - }; - } + const tls = { + certResolver: certResolver, + ...(preferWildcardCert + ? { + domains: [ + { + main: wildCard + } + ] + } + : {}) + }; const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; @@ -264,11 +264,12 @@ export async function getTraefikConfig( ]; // Handle path rewriting middleware - if (resource.rewritePath && - resource.path && + if ( + resource.rewritePath !== null && + resource.path !== null && resource.pathMatchType && - resource.rewritePathType) { - + resource.rewritePathType + ) { // Create a unique middleware name const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`; @@ -287,7 +288,10 @@ export async function getTraefikConfig( } // the middleware to the config - Object.assign(config_output.http.middlewares, rewriteResult.middlewares); + Object.assign( + config_output.http.middlewares, + rewriteResult.middlewares + ); // middlewares to the router middleware chain if (rewriteResult.chain) { @@ -298,9 +302,13 @@ export async function getTraefikConfig( routerMiddlewares.push(rewriteMiddlewareName); } - logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); + logger.debug( + `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` + ); } catch (error) { - logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`); + logger.error( + `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}` + ); } } @@ -316,7 +324,9 @@ export async function getTraefikConfig( value: string; }[]; } catch (e) { - logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`); + logger.warn( + `Failed to parse headers for resource ${resource.resourceId}: ${e}` + ); } headersArr.forEach((header) => { @@ -482,14 +492,14 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - cookie: { - name: "p_sticky", // TODO: make this configurable via config.yml like other cookies - secure: resource.ssl, - httpOnly: true - } - } - } + sticky: { + cookie: { + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies + secure: resource.ssl, + httpOnly: true + } + } + } : {}) } }; @@ -590,13 +600,13 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - ipStrategy: { - depth: 0, - sourcePort: true - } - } - } + sticky: { + ipStrategy: { + depth: 0, + sourcePort: true + } + } + } : {}) } }; diff --git a/server/lib/traefik/index.ts b/server/lib/traefik/index.ts index 1d654510..5630028c 100644 --- a/server/lib/traefik/index.ts +++ b/server/lib/traefik/index.ts @@ -1,11 +1 @@ -import { build } from "@server/build"; - -// Import both modules -import * as traefikModule from "./getTraefikConfig"; -import * as privateTraefikModule from "./privateGetTraefikConfig"; - -// Conditionally export Traefik configuration implementation based on build type -const traefikImplementation = build === "oss" ? traefikModule : privateTraefikModule; - -// Re-export all items from the selected implementation -export const { getTraefikConfig } = traefikImplementation; \ No newline at end of file +export * from "./getTraefikConfig"; \ No newline at end of file diff --git a/server/license/license.ts b/server/license/license.ts index aeb628df..919fdb03 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -1,26 +1,17 @@ -import { db } from "@server/db"; -import { hostMeta, licenseKey, sites } from "@server/db"; -import logger from "@server/logger"; -import NodeCache from "node-cache"; -import { validateJWT } from "./licenseJwt"; -import { count, eq } from "drizzle-orm"; -import moment from "moment"; +import { db, hostMeta, HostMeta } from "@server/db"; import { setHostMeta } from "@server/lib/hostMeta"; -import { encrypt, decrypt } from "@server/lib/crypto"; -const keyTypes = ["HOST", "SITES"] as const; -type KeyType = (typeof keyTypes)[number]; +const keyTypes = ["host"] as const; +export type LicenseKeyType = (typeof keyTypes)[number]; -const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const; -type KeyTier = (typeof keyTiers)[number]; +const keyTiers = ["personal", "enterprise"] as const; +export type LicenseKeyTier = (typeof keyTiers)[number]; export type LicenseStatus = { isHostLicensed: boolean; // Are there any license keys? isLicenseValid: boolean; // Is the license key valid? hostId: string; // Host ID - maxSites?: number; - usedSites?: number; - tier?: KeyTier; + tier?: LicenseKeyTier; }; export type LicenseKeyCache = { @@ -28,451 +19,27 @@ export type LicenseKeyCache = { licenseKeyEncrypted: string; valid: boolean; iat?: Date; - type?: KeyType; - tier?: KeyTier; - numSites?: number; -}; - -type ActivateLicenseKeyAPIResponse = { - data: { - instanceId: string; - }; - success: boolean; - error: string; - message: string; - status: number; -}; - -type ValidateLicenseAPIResponse = { - data: { - licenseKeys: { - [key: string]: string; - }; - }; - success: boolean; - error: string; - message: string; - status: number; -}; - -type TokenPayload = { - valid: boolean; - type: KeyType; - tier: KeyTier; - quantity: number; - terminateAt: string; // ISO - iat: number; // Issued at + type?: LicenseKeyType; + tier?: LicenseKeyTier; + terminateAt?: Date; }; export class License { - private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds - private validationServerUrl = - "https://api.fossorial.io/api/v1/license/professional/validate"; - private activationServerUrl = - "https://api.fossorial.io/api/v1/license/professional/activate"; - - private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval }); - private licenseKeyCache = new NodeCache(); - - private ephemeralKey!: string; - private statusKey = "status"; private serverSecret!: string; - private publicKey = `-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF -FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf -CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl -apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt -h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y -zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y -LQIDAQAB ------END PUBLIC KEY-----`; + constructor(private hostMeta: HostMeta) {} - constructor(private hostId: string) { - this.ephemeralKey = Buffer.from( - JSON.stringify({ ts: new Date().toISOString() }) - ).toString("base64"); - - setInterval( - async () => { - await this.check(); - }, - 1000 * 60 * 60 - ); // 1 hour = 60 * 60 = 3600 seconds - } - - public listKeys(): LicenseKeyCache[] { - const keys = this.licenseKeyCache.keys(); - return keys.map((key) => { - return this.licenseKeyCache.get(key)!; - }); + public async check(): Promise { + return { + hostId: this.hostMeta.hostMetaId, + isHostLicensed: false, + isLicenseValid: false + }; } public setServerSecret(secret: string) { this.serverSecret = secret; } - - public async forceRecheck() { - this.statusCache.flushAll(); - this.licenseKeyCache.flushAll(); - - return await this.check(); - } - - public async isUnlocked(): Promise { - const status = await this.check(); - if (status.isHostLicensed) { - if (status.isLicenseValid) { - return true; - } - } - return false; - } - - public async check(): Promise { - // Set used sites - const [siteCount] = await db - .select({ - value: count() - }) - .from(sites); - - const status: LicenseStatus = { - hostId: this.hostId, - isHostLicensed: true, - isLicenseValid: false, - maxSites: undefined, - usedSites: siteCount.value - }; - - try { - if (this.statusCache.has(this.statusKey)) { - const res = this.statusCache.get("status") as LicenseStatus; - res.usedSites = status.usedSites; - return res; - } - - // Invalidate all - this.licenseKeyCache.flushAll(); - - const allKeysRes = await db.select().from(licenseKey); - - if (allKeysRes.length === 0) { - status.isHostLicensed = false; - return status; - } - - let foundHostKey = false; - // Validate stored license keys - for (const key of allKeysRes) { - try { - // Decrypt the license key and token - const decryptedKey = decrypt( - key.licenseKeyId, - this.serverSecret - ); - const decryptedToken = decrypt( - key.token, - this.serverSecret - ); - - const payload = validateJWT( - decryptedToken, - this.publicKey - ); - - this.licenseKeyCache.set(decryptedKey, { - licenseKey: decryptedKey, - licenseKeyEncrypted: key.licenseKeyId, - valid: payload.valid, - type: payload.type, - tier: payload.tier, - numSites: payload.quantity, - iat: new Date(payload.iat * 1000) - }); - - if (payload.type === "HOST") { - foundHostKey = true; - } - } catch (e) { - logger.error( - `Error validating license key: ${key.licenseKeyId}` - ); - logger.error(e); - - this.licenseKeyCache.set( - key.licenseKeyId, - { - licenseKey: key.licenseKeyId, - licenseKeyEncrypted: key.licenseKeyId, - valid: false - } - ); - } - } - - if (!foundHostKey && allKeysRes.length) { - logger.debug("No host license key found"); - status.isHostLicensed = false; - } - - const keys = allKeysRes.map((key) => ({ - licenseKey: decrypt(key.licenseKeyId, this.serverSecret), - instanceId: decrypt(key.instanceId, this.serverSecret) - })); - - let apiResponse: ValidateLicenseAPIResponse | undefined; - try { - // Phone home to validate license keys - apiResponse = await this.phoneHome(keys); - - if (!apiResponse?.success) { - throw new Error(apiResponse?.error); - } - } catch (e) { - logger.error("Error communicating with license server:"); - logger.error(e); - } - - logger.debug("Validate response", apiResponse); - - // Check and update all license keys with server response - for (const key of keys) { - try { - const cached = this.licenseKeyCache.get( - key.licenseKey - )!; - const licenseKeyRes = - apiResponse?.data?.licenseKeys[key.licenseKey]; - - if (!apiResponse || !licenseKeyRes) { - logger.debug( - `No response from server for license key: ${key.licenseKey}` - ); - if (cached.iat) { - const exp = moment(cached.iat) - .add(7, "days") - .toDate(); - if (exp > new Date()) { - logger.debug( - `Using cached license key: ${key.licenseKey}, valid ${cached.valid}` - ); - continue; - } - } - - logger.debug( - `Can't trust license key: ${key.licenseKey}` - ); - cached.valid = false; - this.licenseKeyCache.set( - key.licenseKey, - cached - ); - continue; - } - - const payload = validateJWT( - licenseKeyRes, - this.publicKey - ); - cached.valid = payload.valid; - cached.type = payload.type; - cached.tier = payload.tier; - cached.numSites = payload.quantity; - cached.iat = new Date(payload.iat * 1000); - - // Encrypt the updated token before storing - const encryptedKey = encrypt( - key.licenseKey, - this.serverSecret - ); - const encryptedToken = encrypt( - licenseKeyRes, - this.serverSecret - ); - - await db - .update(licenseKey) - .set({ - token: encryptedToken - }) - .where(eq(licenseKey.licenseKeyId, encryptedKey)); - - this.licenseKeyCache.set( - key.licenseKey, - cached - ); - } catch (e) { - logger.error(`Error validating license key: ${key}`); - logger.error(e); - } - } - - // Compute host status - for (const key of keys) { - const cached = this.licenseKeyCache.get( - key.licenseKey - )!; - - logger.debug("Checking key", cached); - - if (cached.type === "HOST") { - status.isLicenseValid = cached.valid; - status.tier = cached.tier; - } - - if (!cached.valid) { - continue; - } - - if (!status.maxSites) { - status.maxSites = 0; - } - - status.maxSites += cached.numSites || 0; - } - } catch (error) { - logger.error("Error checking license status:"); - logger.error(error); - } - - this.statusCache.set(this.statusKey, status); - return status; - } - - public async activateLicenseKey(key: string) { - // Encrypt the license key before storing - const encryptedKey = encrypt(key, this.serverSecret); - - const [existingKey] = await db - .select() - .from(licenseKey) - .where(eq(licenseKey.licenseKeyId, encryptedKey)) - .limit(1); - - if (existingKey) { - throw new Error("License key already exists"); - } - - let instanceId: string | undefined; - try { - // Call activate - const apiResponse = await fetch(this.activationServerUrl, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - licenseKey: key, - instanceName: this.hostId - }) - }); - - const data = await apiResponse.json(); - - if (!data.success) { - throw new Error(`${data.message || data.error}`); - } - - const response = data as ActivateLicenseKeyAPIResponse; - - if (!response.data) { - throw new Error("No response from server"); - } - - if (!response.data.instanceId) { - throw new Error("No instance ID in response"); - } - - instanceId = response.data.instanceId; - } catch (error) { - throw Error(`Error activating license key: ${error}`); - } - - // Phone home to validate license key - const keys = [ - { - licenseKey: key, - instanceId: instanceId! - } - ]; - - let validateResponse: ValidateLicenseAPIResponse; - try { - validateResponse = await this.phoneHome(keys); - - if (!validateResponse) { - throw new Error("No response from server"); - } - - if (!validateResponse.success) { - throw new Error(validateResponse.error); - } - - // Validate the license key - const licenseKeyRes = validateResponse.data.licenseKeys[key]; - if (!licenseKeyRes) { - throw new Error("Invalid license key"); - } - - const payload = validateJWT( - licenseKeyRes, - this.publicKey - ); - - if (!payload.valid) { - throw new Error("Invalid license key"); - } - - const encryptedToken = encrypt(licenseKeyRes, this.serverSecret); - // Encrypt the instanceId before storing - const encryptedInstanceId = encrypt(instanceId!, this.serverSecret); - - // Store the license key in the database - await db.insert(licenseKey).values({ - licenseKeyId: encryptedKey, - token: encryptedToken, - instanceId: encryptedInstanceId - }); - } catch (error) { - throw Error(`Error validating license key: ${error}`); - } - - // Invalidate the cache and re-compute the status - return await this.forceRecheck(); - } - - private async phoneHome( - keys: { - licenseKey: string; - instanceId: string; - }[] - ): Promise { - // Decrypt the instanceIds before sending to the server - const decryptedKeys = keys.map((key) => ({ - licenseKey: key.licenseKey, - instanceId: key.instanceId - ? decrypt(key.instanceId, this.serverSecret) - : key.instanceId - })); - - const response = await fetch(this.validationServerUrl, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - licenseKeys: decryptedKeys, - ephemeralKey: this.ephemeralKey, - instanceName: this.hostId - }) - }); - - const data = await response.json(); - - return data as ValidateLicenseAPIResponse; - } } await setHostMeta(); @@ -483,6 +50,6 @@ if (!info) { throw new Error("Host information not found"); } -export const license = new License(info.hostMetaId); +export const license = new License(info); export default license; diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index f211fa9e..629cafe9 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -21,10 +21,9 @@ export * from "./verifyIsLoggedInUser"; export * from "./verifyIsLoggedInUser"; export * from "./verifyClientAccess"; export * from "./integration"; -export * from "./verifyValidLicense"; export * from "./verifyUserHasAction"; export * from "./verifyApiKeyAccess"; export * from "./verifyDomainAccess"; export * from "./verifyClientsEnabled"; export * from "./verifyUserIsOrgOwner"; -export * from "./verifySiteResourceAccess"; \ No newline at end of file +export * from "./verifySiteResourceAccess"; diff --git a/server/auth/sessions/privateRemoteExitNode.ts b/server/private/auth/sessions/remoteExitNode.ts similarity index 100% rename from server/auth/sessions/privateRemoteExitNode.ts rename to server/private/auth/sessions/remoteExitNode.ts diff --git a/server/private/cleanup.ts b/server/private/cleanup.ts new file mode 100644 index 00000000..8bf5ea3d --- /dev/null +++ b/server/private/cleanup.ts @@ -0,0 +1,28 @@ +/* + * 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 { rateLimitService } from "#private/lib/rateLimit"; +import { cleanup as wsCleanup } from "#private/routers/ws"; + +async function cleanup() { + await rateLimitService.cleanup(); + await wsCleanup(); + + process.exit(0); +} + +export async function initCleanup() { + // Handle process termination + process.on("SIGTERM", () => cleanup()); + process.on("SIGINT", () => cleanup()); +} \ No newline at end of file diff --git a/server/routers/private/billing/createCustomer.ts b/server/private/lib/billing/createCustomer.ts similarity index 96% rename from server/routers/private/billing/createCustomer.ts rename to server/private/lib/billing/createCustomer.ts index d1c08a0e..52c72c53 100644 --- a/server/routers/private/billing/createCustomer.ts +++ b/server/private/lib/billing/createCustomer.ts @@ -13,7 +13,7 @@ import { customers, db } from "@server/db"; import { eq } from "drizzle-orm"; -import stripe from "@server/lib/private/stripe"; +import stripe from "#private/lib/stripe"; import { build } from "@server/build"; export async function createCustomer( diff --git a/server/private/lib/billing/getOrgTierData.ts b/server/private/lib/billing/getOrgTierData.ts new file mode 100644 index 00000000..fbfb5cb0 --- /dev/null +++ b/server/private/lib/billing/getOrgTierData.ts @@ -0,0 +1,46 @@ +/* + * 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 { getTierPriceSet } from "@server/lib/billing/tiers"; +import { getOrgSubscriptionData } from "#private/routers/billing/getOrgSubscription"; +import { build } from "@server/build"; + +export async function getOrgTierData( + orgId: string +): Promise<{ tier: string | null; active: boolean }> { + let tier = null; + let active = false; + + if (build !== "saas") { + return { tier, active }; + } + + const { subscription, items } = await getOrgSubscriptionData(orgId); + + if (items && items.length > 0) { + const tierPriceSet = getTierPriceSet(); + // Iterate through tiers in order (earlier keys are higher tiers) + for (const [tierId, priceId] of Object.entries(tierPriceSet)) { + // Check if any subscription item matches this tier's price ID + const matchingItem = items.find((item) => item.priceId === priceId); + if (matchingItem) { + tier = tierId; + break; + } + } + } + if (subscription && subscription.status === "active") { + active = true; + } + return { tier, active }; +} diff --git a/server/lib/private/billing/index.ts b/server/private/lib/billing/index.ts similarity index 81% rename from server/lib/private/billing/index.ts rename to server/private/lib/billing/index.ts index 0212ee1c..13ca3761 100644 --- a/server/lib/private/billing/index.ts +++ b/server/private/lib/billing/index.ts @@ -11,6 +11,5 @@ * This file is not licensed under the AGPLv3. */ -export * from "./limitSet"; -export * from "./features"; -export * from "./limitsService"; +export * from "./getOrgTierData"; +export * from "./createCustomer"; \ No newline at end of file diff --git a/server/lib/remoteCertificates/privateCertificates.ts b/server/private/lib/certificates.ts similarity index 87% rename from server/lib/remoteCertificates/privateCertificates.ts rename to server/private/lib/certificates.ts index fabc9ea5..93eb5603 100644 --- a/server/lib/remoteCertificates/privateCertificates.ts +++ b/server/private/lib/certificates.ts @@ -11,10 +11,10 @@ * This file is not licensed under the AGPLv3. */ -import config from "../config"; +import config from "./config"; import { certificates, db } from "@server/db"; import { and, eq, isNotNull } from "drizzle-orm"; -import { decryptData } from "../encryption"; +import { decryptData } from "@server/lib/encryption"; import * as fs from "fs"; export async function getValidCertificatesForDomains( @@ -97,20 +97,4 @@ export async function getValidCertificatesForDomains( }); return validCertsDecrypted; -} - -export async function getValidCertificatesForDomainsHybrid( - domains: Set -): Promise< - Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: number | null; - updatedAt?: number | null; - }> -> { - return []; // stub -} +} \ No newline at end of file diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts new file mode 100644 index 00000000..dd32e313 --- /dev/null +++ b/server/private/lib/config.ts @@ -0,0 +1,165 @@ +/* + * 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 { z } from "zod"; +import { __DIRNAME } from "@server/lib/consts"; +import { SupporterKey } from "@server/db"; +import { fromError } from "zod-validation-error"; +import { + privateConfigSchema, + readPrivateConfigFile +} from "#private/lib/readConfigFile"; +import { build } from "@server/build"; + +export class PrivateConfig { + private rawPrivateConfig!: z.infer; + + supporterData: SupporterKey | null = null; + + supporterHiddenUntil: number | null = null; + + isDev: boolean = process.env.ENVIRONMENT !== "prod"; + + constructor() { + const privateEnvironment = readPrivateConfigFile(); + + const { + data: parsedPrivateConfig, + success: privateSuccess, + error: privateError + } = privateConfigSchema.safeParse(privateEnvironment); + + if (!privateSuccess) { + const errors = fromError(privateError); + throw new Error(`Invalid private configuration file: ${errors}`); + } + + if (parsedPrivateConfig.branding?.colors) { + process.env.BRANDING_COLORS = JSON.stringify( + parsedPrivateConfig.branding?.colors + ); + } + + if (parsedPrivateConfig.branding?.logo?.light_path) { + process.env.BRANDING_LOGO_LIGHT_PATH = + parsedPrivateConfig.branding?.logo?.light_path; + } + if (parsedPrivateConfig.branding?.logo?.dark_path) { + process.env.BRANDING_LOGO_DARK_PATH = + parsedPrivateConfig.branding?.logo?.dark_path || undefined; + } + + if (build != "oss") { + if (parsedPrivateConfig.branding?.logo?.light_path) { + process.env.BRANDING_LOGO_LIGHT_PATH = + parsedPrivateConfig.branding?.logo?.light_path; + } + if (parsedPrivateConfig.branding?.logo?.dark_path) { + process.env.BRANDING_LOGO_DARK_PATH = + parsedPrivateConfig.branding?.logo?.dark_path || undefined; + } + + process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding + ?.logo?.auth_page?.width + ? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString() + : undefined; + process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding + ?.logo?.auth_page?.height + ? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString() + : undefined; + + process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig + .branding?.logo?.navbar?.width + ? parsedPrivateConfig.branding?.logo?.navbar?.width.toString() + : undefined; + process.env.BRANDING_LOGO_NAVBAR_HEIGHT = parsedPrivateConfig + .branding?.logo?.navbar?.height + ? parsedPrivateConfig.branding?.logo?.navbar?.height.toString() + : undefined; + + process.env.BRANDING_FAVICON_PATH = + parsedPrivateConfig.branding?.favicon_path; + + process.env.BRANDING_APP_NAME = + parsedPrivateConfig.branding?.app_name || "Pangolin"; + + if (parsedPrivateConfig.branding?.footer) { + process.env.BRANDING_FOOTER = JSON.stringify( + parsedPrivateConfig.branding?.footer + ); + } + + process.env.LOGIN_PAGE_TITLE_TEXT = + parsedPrivateConfig.branding?.login_page?.title_text || ""; + process.env.LOGIN_PAGE_SUBTITLE_TEXT = + parsedPrivateConfig.branding?.login_page?.subtitle_text || ""; + + process.env.SIGNUP_PAGE_TITLE_TEXT = + parsedPrivateConfig.branding?.signup_page?.title_text || ""; + process.env.SIGNUP_PAGE_SUBTITLE_TEXT = + parsedPrivateConfig.branding?.signup_page?.subtitle_text || ""; + + process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY = + parsedPrivateConfig.branding?.resource_auth_page + ?.hide_powered_by === true + ? "true" + : "false"; + process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO = + parsedPrivateConfig.branding?.resource_auth_page?.show_logo === + true + ? "true" + : "false"; + process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT = + parsedPrivateConfig.branding?.resource_auth_page?.title_text || + ""; + process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT = + parsedPrivateConfig.branding?.resource_auth_page + ?.subtitle_text || ""; + + if (parsedPrivateConfig.branding?.background_image_path) { + process.env.BACKGROUND_IMAGE_PATH = + parsedPrivateConfig.branding?.background_image_path; + } + + if (parsedPrivateConfig.server.reo_client_id) { + process.env.REO_CLIENT_ID = + parsedPrivateConfig.server.reo_client_id; + } + + if (parsedPrivateConfig.stripe?.s3Bucket) { + process.env.S3_BUCKET = parsedPrivateConfig.stripe.s3Bucket; + } + if (parsedPrivateConfig.stripe?.localFilePath) { + process.env.LOCAL_FILE_PATH = + parsedPrivateConfig.stripe.localFilePath; + } + if (parsedPrivateConfig.stripe?.s3Region) { + process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region; + } + if (parsedPrivateConfig.flags.use_pangolin_dns) { + process.env.USE_PANGOLIN_DNS = + parsedPrivateConfig.flags.use_pangolin_dns.toString(); + } + } + + this.rawPrivateConfig = parsedPrivateConfig; + } + + public getRawPrivateConfig() { + return this.rawPrivateConfig; + } +} + +export const privateConfig = new PrivateConfig(); + +export default privateConfig; diff --git a/server/lib/exitNodes/privateExitNodeComms.ts b/server/private/lib/exitNodes/exitNodeComms.ts similarity index 85% rename from server/lib/exitNodes/privateExitNodeComms.ts rename to server/private/lib/exitNodes/exitNodeComms.ts index 163a962f..20c850a1 100644 --- a/server/lib/exitNodes/privateExitNodeComms.ts +++ b/server/private/lib/exitNodes/exitNodeComms.ts @@ -15,8 +15,9 @@ import axios from "axios"; import logger from "@server/logger"; import { db, ExitNode, remoteExitNodes } from "@server/db"; import { eq } from "drizzle-orm"; -import { sendToClient } from "../../routers/ws"; -import { config } from "../config"; +import { sendToClient } from "#private/routers/ws"; +import privateConfig from "#private/lib/config"; +import config from "@server/lib/config"; interface ExitNodeRequest { remoteType?: string; @@ -56,16 +57,16 @@ export async function sendToExitNode( } else { let hostname = exitNode.reachableAt; - logger.debug(`Exit node details:`, { - type: exitNode.type, - name: exitNode.name, - reachableAt: exitNode.reachableAt, - }); + // logger.debug(`Exit node details:`, { + // type: exitNode.type, + // name: exitNode.name, + // reachableAt: exitNode.reachableAt, + // }); - logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`); + // logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`); if (exitNode.name == config.getRawConfig().gerbil.exit_node_name) { - hostname = config.getRawPrivateConfig().gerbil.local_exit_node_reachable_at; + hostname = privateConfig.getRawPrivateConfig().gerbil.local_exit_node_reachable_at; } if (!hostname) { @@ -74,10 +75,10 @@ export async function sendToExitNode( ); } - logger.debug(`Sending request to exit node at ${hostname}`, { - type: request.remoteType, - data: request.data - }); + // logger.debug(`Sending request to exit node at ${hostname}`, { + // type: request.remoteType, + // data: request.data + // }); // Handle local exit node with HTTP API const method = request.method || "POST"; diff --git a/server/lib/exitNodes/privateExitNodes.ts b/server/private/lib/exitNodes/exitNodes.ts similarity index 100% rename from server/lib/exitNodes/privateExitNodes.ts rename to server/private/lib/exitNodes/exitNodes.ts diff --git a/server/private/lib/exitNodes/index.ts b/server/private/lib/exitNodes/index.ts new file mode 100644 index 00000000..098a0580 --- /dev/null +++ b/server/private/lib/exitNodes/index.ts @@ -0,0 +1,15 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./exitNodeComms"; +export * from "./exitNodes"; \ No newline at end of file diff --git a/server/db/private/rateLimit.test.ts b/server/private/lib/rateLimit.test.ts similarity index 100% rename from server/db/private/rateLimit.test.ts rename to server/private/lib/rateLimit.test.ts diff --git a/server/db/private/rateLimit.ts b/server/private/lib/rateLimit.ts similarity index 98% rename from server/db/private/rateLimit.ts rename to server/private/lib/rateLimit.ts index ff8589bc..44aa0748 100644 --- a/server/db/private/rateLimit.ts +++ b/server/private/lib/rateLimit.ts @@ -12,7 +12,7 @@ */ import logger from "@server/logger"; -import redisManager from "@server/db/private/redis"; +import redisManager from "#private/lib/redis"; import { build } from "@server/build"; // Rate limiting configuration @@ -451,8 +451,4 @@ export class RateLimitService { } // Export singleton instance -export const rateLimitService = new RateLimitService(); - -// Handle process termination -process.on("SIGTERM", () => rateLimitService.cleanup()); -process.on("SIGINT", () => rateLimitService.cleanup()); \ No newline at end of file +export const rateLimitService = new RateLimitService(); \ No newline at end of file diff --git a/server/private/lib/rateLimitStore.ts b/server/private/lib/rateLimitStore.ts new file mode 100644 index 00000000..20355125 --- /dev/null +++ b/server/private/lib/rateLimitStore.ts @@ -0,0 +1,32 @@ +/* + * 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 { build } from "@server/build"; +import privateConfig from "#private/lib/config"; +import { MemoryStore, Store } from "express-rate-limit"; +import RedisStore from "#private/lib/redisStore"; + +export function createStore(): Store { + if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) { + const rateLimitStore: Store = new RedisStore({ + prefix: "api-rate-limit", // Optional: customize Redis key prefix + skipFailedRequests: true, // Don't count failed requests + skipSuccessfulRequests: false // Count successful requests + }); + + return rateLimitStore; + } else { + const rateLimitStore: Store = new MemoryStore(); + return rateLimitStore; + } +} diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts new file mode 100644 index 00000000..6651c1c6 --- /dev/null +++ b/server/private/lib/readConfigFile.ts @@ -0,0 +1,203 @@ +/* + * 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 fs from "fs"; +import yaml from "js-yaml"; +import { privateConfigFilePath1 } from "@server/lib/consts"; +import { z } from "zod"; +import { colorsSchema } from "@server/lib/colorsSchema"; +import { build } from "@server/build"; + +const portSchema = z.number().positive().gt(0).lte(65535); + +export const privateConfigSchema = z.object({ + app: z + .object({ + region: z.string().optional().default("default"), + base_domain: z.string().optional() + }) + .optional() + .default({ + region: "default" + }), + server: z + .object({ + encryption_key_path: z + .string() + .optional() + .default("./config/encryption.pem") + .pipe(z.string().min(8)), + resend_api_key: z.string().optional(), + reo_client_id: z.string().optional(), + fossorial_api_key: z.string().optional() + }) + .optional() + .default({ + encryption_key_path: "./config/encryption.pem" + }), + redis: z + .object({ + host: z.string(), + port: portSchema, + password: z.string().optional(), + db: z.number().int().nonnegative().optional().default(0), + replicas: z + .array( + z.object({ + host: z.string(), + port: portSchema, + password: z.string().optional(), + db: z.number().int().nonnegative().optional().default(0) + }) + ) + .optional() + // tls: z + // .object({ + // reject_unauthorized: z + // .boolean() + // .optional() + // .default(true) + // }) + // .optional() + }) + .optional(), + gerbil: z + .object({ + local_exit_node_reachable_at: z + .string() + .optional() + .default("http://gerbil:3003") + }) + .optional() + .default({}), + flags: z + .object({ + enable_redis: z.boolean().optional().default(false), + use_pangolin_dns: z.boolean().optional().default(false) + }) + .optional() + .default({}), + branding: z + .object({ + app_name: z.string().optional(), + background_image_path: z.string().optional(), + colors: z + .object({ + light: colorsSchema.optional(), + dark: colorsSchema.optional() + }) + .optional(), + logo: z + .object({ + light_path: z.string().optional(), + dark_path: z.string().optional(), + auth_page: z + .object({ + width: z.number().optional(), + height: z.number().optional() + }) + .optional(), + navbar: z + .object({ + width: z.number().optional(), + height: z.number().optional() + }) + .optional() + }) + .optional(), + favicon_path: z.string().optional(), + footer: z + .array( + z.object({ + text: z.string(), + href: z.string().optional() + }) + ) + .optional(), + login_page: z + .object({ + subtitle_text: z.string().optional(), + title_text: z.string().optional() + }) + .optional(), + signup_page: z + .object({ + subtitle_text: z.string().optional(), + title_text: z.string().optional() + }) + .optional(), + resource_auth_page: z + .object({ + show_logo: z.boolean().optional(), + hide_powered_by: z.boolean().optional(), + title_text: z.string().optional(), + subtitle_text: z.string().optional() + }) + .optional(), + emails: z + .object({ + signature: z.string().optional(), + colors: z + .object({ + primary: z.string().optional() + }) + .optional() + }) + .optional() + }) + .optional(), + stripe: z + .object({ + secret_key: z.string(), + webhook_secret: z.string(), + s3Bucket: z.string(), + s3Region: z.string().default("us-east-1"), + localFilePath: z.string() + }) + .optional() +}); + +export function readPrivateConfigFile() { + if (build == "oss") { + return {}; + } + + const loadConfig = (configPath: string) => { + try { + const yamlContent = fs.readFileSync(configPath, "utf8"); + if (yamlContent.trim() === "") { + return {}; + } + const config = yaml.load(yamlContent); + return config; + } catch (error) { + if (error instanceof Error) { + throw new Error( + `Error loading configuration file: ${error.message}` + ); + } + throw error; + } + }; + + let environment: any = {}; + if (fs.existsSync(privateConfigFilePath1)) { + environment = loadConfig(privateConfigFilePath1); + } + + if (!environment) { + throw new Error("No private configuration file found."); + } + + return environment; +} diff --git a/server/db/private/redis.ts b/server/private/lib/redis.ts similarity index 98% rename from server/db/private/redis.ts rename to server/private/lib/redis.ts index d6e67262..324a6a74 100644 --- a/server/db/private/redis.ts +++ b/server/private/lib/redis.ts @@ -13,7 +13,7 @@ import Redis, { RedisOptions } from "ioredis"; import logger from "@server/logger"; -import config from "@server/lib/config"; +import privateConfig from "#private/lib/config"; import { build } from "@server/build"; class RedisManager { @@ -46,7 +46,7 @@ class RedisManager { this.isEnabled = false; return; } - this.isEnabled = config.getRawPrivateConfig().flags?.enable_redis || false; + this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false; if (this.isEnabled) { this.initializeClients(); } @@ -93,7 +93,7 @@ class RedisManager { } private getRedisConfig(): RedisOptions { - const redisConfig = config.getRawPrivateConfig().redis!; + const redisConfig = privateConfig.getRawPrivateConfig().redis!; const opts: RedisOptions = { host: redisConfig.host!, port: redisConfig.port!, @@ -108,7 +108,7 @@ class RedisManager { } private getReplicaRedisConfig(): RedisOptions | null { - const redisConfig = config.getRawPrivateConfig().redis!; + const redisConfig = privateConfig.getRawPrivateConfig().redis!; if (!redisConfig.replicas || redisConfig.replicas.length === 0) { return null; } diff --git a/server/db/private/redisStore.ts b/server/private/lib/redisStore.ts similarity index 100% rename from server/db/private/redisStore.ts rename to server/private/lib/redisStore.ts diff --git a/server/lib/private/resend.ts b/server/private/lib/resend.ts similarity index 96% rename from server/lib/private/resend.ts rename to server/private/lib/resend.ts index 26c3f4a6..1aac3d07 100644 --- a/server/lib/private/resend.ts +++ b/server/private/lib/resend.ts @@ -12,7 +12,7 @@ */ import { Resend } from "resend"; -import config from "../config"; +import privateConfig from "#private/lib/config"; import logger from "@server/logger"; export enum AudienceIds { @@ -22,7 +22,7 @@ export enum AudienceIds { } const resend = new Resend( - config.getRawPrivateConfig().server.resend_api_key || "missing" + privateConfig.getRawPrivateConfig().server.resend_api_key || "missing" ); export default resend; diff --git a/server/lib/private/stripe.ts b/server/private/lib/stripe.ts similarity index 76% rename from server/lib/private/stripe.ts rename to server/private/lib/stripe.ts index 1170202d..01aacb35 100644 --- a/server/lib/private/stripe.ts +++ b/server/private/lib/stripe.ts @@ -12,13 +12,13 @@ */ import Stripe from "stripe"; -import config from "@server/lib/config"; +import privateConfig from "#private/lib/config"; import logger from "@server/logger"; -import { build } from "@server/build"; +import { noop } from "@server/lib/billing/usageService"; let stripe: Stripe | undefined = undefined; -if (build == "saas") { - const stripeApiKey = config.getRawPrivateConfig().stripe?.secret_key; +if (!noop()) { + const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key; if (!stripeApiKey) { logger.error("Stripe secret key is not configured"); } diff --git a/server/lib/traefik/privateGetTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts similarity index 84% rename from server/lib/traefik/privateGetTraefikConfig.ts rename to server/private/lib/traefik/getTraefikConfig.ts index 1350e8b7..f6d1c8ab 100644 --- a/server/lib/traefik/privateGetTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -21,11 +21,11 @@ import { } from "@server/db"; import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm"; import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; import config from "@server/lib/config"; import { orgs, resources, sites, Target, targets } from "@server/db"; -import { build } from "@server/build"; -import { sanitize } from "./utils"; +import { sanitize, validatePathRewriteConfig } from "@server/lib/traefik/utils"; +import privateConfig from "#private/lib/config"; +import createPathRewriteMiddleware from "@server/lib/traefik/middleware"; const redirectHttpsMiddlewareName = "redirect-to-https"; const redirectToRootMiddlewareName = "redirect-to-root"; @@ -78,8 +78,10 @@ export async function getTraefikConfig( hcHealth: targetHealthCheck.hcHealth, path: targets.path, pathMatchType: targets.pathMatchType, + rewritePath: targets.rewritePath, + rewritePathType: targets.rewritePathType, priority: targets.priority, - + // Site fields siteId: sites.siteId, siteType: sites.type, @@ -131,18 +133,41 @@ export async function getTraefikConfig( const resourceName = sanitize(row.resourceName) || ""; const targetPath = sanitize(row.path) || ""; // Handle null/undefined paths const pathMatchType = row.pathMatchType || ""; + const rewritePath = row.rewritePath || ""; + const rewritePathType = row.rewritePathType || ""; const priority = row.priority ?? 100; if (filterOutNamespaceDomains && row.domainNamespaceId) { return; } - // Create a unique key combining resourceId and path+pathMatchType - const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-"); + // Create a unique key combining resourceId, path config, and rewrite config + const pathKey = [ + targetPath, + pathMatchType, + rewritePath, + rewritePathType + ] + .filter(Boolean) + .join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); const key = sanitize(mapKey); if (!resourcesMap.has(key)) { + const validation = validatePathRewriteConfig( + row.path, + row.pathMatchType, + row.rewritePath, + row.rewritePathType + ); + + if (!validation.isValid) { + logger.error( + `Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}` + ); + return; + } + resourcesMap.set(key, { resourceId: row.resourceId, name: resourceName, @@ -163,6 +188,8 @@ export async function getTraefikConfig( headers: row.headers, path: row.path, // the targets will all have the same path pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType + rewritePath: row.rewritePath, + rewritePathType: row.rewritePathType, priority: priority // may be null, we fallback later }); } @@ -176,7 +203,6 @@ export async function getTraefikConfig( port: row.port, internalPort: row.internalPort, enabled: row.targetEnabled, - priority: row.priority, site: { siteId: row.siteId, type: row.siteType, @@ -187,11 +213,6 @@ export async function getTraefikConfig( }); }); - // make sure we have at least one resource - if (resourcesMap.size === 0) { - return {}; - } - const config_output: any = { http: { middlewares: { @@ -234,12 +255,13 @@ export async function getTraefikConfig( continue; } - if (resource.certificateStatus !== "valid") { - logger.debug( - `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}` - ); - continue; - } + // TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail + // if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { + // logger.debug( + // `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}` + // ); + // continue; + // } // add routers and services empty objects if they don't exist if (!config_output.http.routers) { @@ -264,18 +286,18 @@ export async function getTraefikConfig( const configDomain = config.getDomain(resource.domainId); - let certResolver: string, preferWildcardCert: boolean; - if (!configDomain) { - certResolver = config.getRawConfig().traefik.cert_resolver; - preferWildcardCert = - config.getRawConfig().traefik.prefer_wildcard_cert; - } else { - certResolver = configDomain.cert_resolver; - preferWildcardCert = configDomain.prefer_wildcard_cert; - } - let tls = {}; - if (build == "oss") { + if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { + let certResolver: string, preferWildcardCert: boolean; + if (!configDomain) { + certResolver = config.getRawConfig().traefik.cert_resolver; + preferWildcardCert = + config.getRawConfig().traefik.prefer_wildcard_cert; + } else { + certResolver = configDomain.cert_resolver; + preferWildcardCert = configDomain.prefer_wildcard_cert; + } + tls = { certResolver: certResolver, ...(preferWildcardCert @@ -298,6 +320,55 @@ export async function getTraefikConfig( ...additionalMiddlewares ]; + // Handle path rewriting middleware + if ( + resource.rewritePath !== null && + resource.path !== null && + resource.pathMatchType && + resource.rewritePathType + ) { + // Create a unique middleware name + const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`; + + try { + const rewriteResult = createPathRewriteMiddleware( + rewriteMiddlewareName, + resource.path, + resource.pathMatchType, + resource.rewritePath, + resource.rewritePathType + ); + + // Initialize middlewares object if it doesn't exist + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + + // the middleware to the config + Object.assign( + config_output.http.middlewares, + rewriteResult.middlewares + ); + + // middlewares to the router middleware chain + if (rewriteResult.chain) { + // For chained middlewares (like stripPrefix + addPrefix) + routerMiddlewares.push(...rewriteResult.chain); + } else { + // Single middleware + routerMiddlewares.push(rewriteMiddlewareName); + } + + logger.debug( + `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` + ); + } catch (error) { + logger.error( + `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}` + ); + } + } + if (resource.headers || resource.setHostHeader) { // if there are headers, parse them into an object const headersObj: { [key: string]: string } = {}; @@ -419,7 +490,7 @@ export async function getTraefikConfig( return ( (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { + .filter((target: TargetWithSite) => { if (!target.enabled) { return false; } @@ -440,7 +511,7 @@ export async function getTraefikConfig( ) { return false; } - } else if (target.site.type === "newt") { + } else if (target.site.type === "newt") { if ( !target.internalPort || !target.method || @@ -448,10 +519,10 @@ export async function getTraefikConfig( ) { return false; } - } - return true; - }) - .map((target: TargetWithSite) => { + } + return true; + }) + .map((target: TargetWithSite) => { if ( target.site.type === "local" || target.site.type === "wireguard" @@ -459,14 +530,14 @@ export async function getTraefikConfig( return { url: `${target.method}://${target.ip}:${target.port}` }; - } else if (target.site.type === "newt") { + } else if (target.site.type === "newt") { const ip = target.site.subnet!.split("/")[0]; return { url: `${target.method}://${ip}:${target.internalPort}` }; - } - }) + } + }) // filter out duplicates .filter( (v, i, a) => @@ -709,4 +780,4 @@ export async function getTraefikConfig( } return config_output; -} \ No newline at end of file +} diff --git a/server/private/lib/traefik/index.ts b/server/private/lib/traefik/index.ts new file mode 100644 index 00000000..30d83181 --- /dev/null +++ b/server/private/lib/traefik/index.ts @@ -0,0 +1,14 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./getTraefikConfig"; \ No newline at end of file diff --git a/server/private/license/license.ts b/server/private/license/license.ts new file mode 100644 index 00000000..809f5ca9 --- /dev/null +++ b/server/private/license/license.ts @@ -0,0 +1,460 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { db, HostMeta } from "@server/db"; +import { hostMeta, licenseKey } from "@server/db"; +import logger from "@server/logger"; +import NodeCache from "node-cache"; +import { validateJWT } from "./licenseJwt"; +import { eq } from "drizzle-orm"; +import moment from "moment"; +import { encrypt, decrypt } from "@server/lib/crypto"; +import { + LicenseKeyCache, + LicenseKeyTier, + LicenseKeyType, + LicenseStatus +} from "@server/license/license"; +import { setHostMeta } from "@server/lib/hostMeta"; + +type ActivateLicenseKeyAPIResponse = { + data: { + instanceId: string; + }; + success: boolean; + error: string; + message: string; + status: number; +}; + +type ValidateLicenseAPIResponse = { + data: { + licenseKeys: { + [key: string]: string; + }; + }; + success: boolean; + error: string; + message: string; + status: number; +}; + +type TokenPayload = { + valid: boolean; + type: LicenseKeyType; + tier: LicenseKeyTier; + quantity: number; + terminateAt: string; // ISO + iat: number; // Issued at +}; + +export class License { + private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds + private serverBaseUrl = "https://api.fossorial.io"; + private validationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/validate`; + private activationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/activate`; + + private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval }); + private licenseKeyCache = new NodeCache(); + + private statusKey = "status"; + private serverSecret!: string; + + private publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF +FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf +CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl +apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt +h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y +zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y +LQIDAQAB +-----END PUBLIC KEY-----`; + + constructor(private hostMeta: HostMeta) { + setInterval( + async () => { + await this.check(); + }, + 1000 * 60 * 60 + ); + } + + public listKeys(): LicenseKeyCache[] { + const keys = this.licenseKeyCache.keys(); + return keys.map((key) => { + return this.licenseKeyCache.get(key)!; + }); + } + + public setServerSecret(secret: string) { + this.serverSecret = secret; + } + + public async forceRecheck() { + this.statusCache.flushAll(); + this.licenseKeyCache.flushAll(); + + return await this.check(); + } + + public async isUnlocked(): Promise { + const status = await this.check(); + if (status.isHostLicensed) { + if (status.isLicenseValid) { + return true; + } + } + return false; + } + + public async check(): Promise { + const status: LicenseStatus = { + hostId: this.hostMeta.hostMetaId, + isHostLicensed: true, + isLicenseValid: false + }; + + try { + if (this.statusCache.has(this.statusKey)) { + const res = this.statusCache.get("status") as LicenseStatus; + return res; + } + // Invalidate all + this.licenseKeyCache.flushAll(); + + const allKeysRes = await db.select().from(licenseKey); + + if (allKeysRes.length === 0) { + status.isHostLicensed = false; + return status; + } + + let foundHostKey = false; + // Validate stored license keys + for (const key of allKeysRes) { + try { + // Decrypt the license key and token + const decryptedKey = decrypt( + key.licenseKeyId, + this.serverSecret + ); + const decryptedToken = decrypt( + key.token, + this.serverSecret + ); + + const payload = validateJWT( + decryptedToken, + this.publicKey + ); + + this.licenseKeyCache.set(decryptedKey, { + licenseKey: decryptedKey, + licenseKeyEncrypted: key.licenseKeyId, + valid: payload.valid, + type: payload.type, + tier: payload.tier, + iat: new Date(payload.iat * 1000), + terminateAt: new Date(payload.terminateAt) + }); + + if (payload.type === "host") { + foundHostKey = true; + } + } catch (e) { + logger.error( + `Error validating license key: ${key.licenseKeyId}` + ); + logger.error(e); + + this.licenseKeyCache.set( + key.licenseKeyId, + { + licenseKey: key.licenseKeyId, + licenseKeyEncrypted: key.licenseKeyId, + valid: false + } + ); + } + } + + if (!foundHostKey && allKeysRes.length) { + logger.debug("No host license key found"); + status.isHostLicensed = false; + } + + const keys = allKeysRes.map((key) => ({ + licenseKey: decrypt(key.licenseKeyId, this.serverSecret), + instanceId: decrypt(key.instanceId, this.serverSecret) + })); + + let apiResponse: ValidateLicenseAPIResponse | undefined; + try { + // Phone home to validate license keys + apiResponse = await this.phoneHome(keys, false); + + if (!apiResponse?.success) { + throw new Error(apiResponse?.error); + } + } catch (e) { + logger.error("Error communicating with license server:"); + logger.error(e); + } + + // Check and update all license keys with server response + for (const key of keys) { + try { + const cached = this.licenseKeyCache.get( + key.licenseKey + )!; + const licenseKeyRes = + apiResponse?.data?.licenseKeys[key.licenseKey]; + + if (!apiResponse || !licenseKeyRes) { + logger.debug( + `No response from server for license key: ${key.licenseKey}` + ); + if (cached.iat) { + const exp = moment(cached.iat) + .add(7, "days") + .toDate(); + if (exp > new Date()) { + logger.debug( + `Using cached license key: ${key.licenseKey}, valid ${cached.valid}` + ); + continue; + } + } + + logger.debug( + `Can't trust license key: ${key.licenseKey}` + ); + cached.valid = false; + this.licenseKeyCache.set( + key.licenseKey, + cached + ); + continue; + } + + const payload = validateJWT( + licenseKeyRes, + this.publicKey + ); + cached.valid = payload.valid; + cached.type = payload.type; + cached.tier = payload.tier; + cached.iat = new Date(payload.iat * 1000); + cached.terminateAt = new Date(payload.terminateAt); + + // Encrypt the updated token before storing + const encryptedKey = encrypt( + key.licenseKey, + this.serverSecret + ); + const encryptedToken = encrypt( + licenseKeyRes, + this.serverSecret + ); + + await db + .update(licenseKey) + .set({ + token: encryptedToken + }) + .where(eq(licenseKey.licenseKeyId, encryptedKey)); + + this.licenseKeyCache.set( + key.licenseKey, + cached + ); + } catch (e) { + logger.error(`Error validating license key: ${key}`); + logger.error(e); + } + } + + // Compute host status + for (const key of keys) { + const cached = this.licenseKeyCache.get( + key.licenseKey + )!; + + if (cached.type === "host") { + status.isLicenseValid = cached.valid; + status.tier = cached.tier; + } + + if (!cached.valid) { + continue; + } + } + } catch (error) { + logger.error("Error checking license status:"); + logger.error(error); + } + + this.statusCache.set(this.statusKey, status); + return status; + } + + public async activateLicenseKey(key: string) { + // Encrypt the license key before storing + const encryptedKey = encrypt(key, this.serverSecret); + + const [existingKey] = await db + .select() + .from(licenseKey) + .where(eq(licenseKey.licenseKeyId, encryptedKey)) + .limit(1); + + if (existingKey) { + throw new Error("License key already exists"); + } + + let instanceId: string | undefined; + try { + // Call activate + const apiResponse = await fetch(this.activationServerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + licenseKey: key, + instanceName: this.hostMeta.hostMetaId + }) + }); + + const data = await apiResponse.json(); + + if (!data.success) { + throw new Error(`${data.message || data.error}`); + } + + const response = data as ActivateLicenseKeyAPIResponse; + + if (!response.data) { + throw new Error("No response from server"); + } + + if (!response.data.instanceId) { + throw new Error("No instance ID in response"); + } + + logger.debug("Activated license key, instance ID:", { + instanceId: response.data.instanceId + }); + + instanceId = response.data.instanceId; + } catch (error) { + throw Error(`Error activating license key: ${error}`); + } + + // Phone home to validate license key + const keys = [ + { + licenseKey: key, + instanceId: instanceId! + } + ]; + + let validateResponse: ValidateLicenseAPIResponse; + try { + validateResponse = await this.phoneHome(keys, false); + + if (!validateResponse) { + throw new Error("No response from server"); + } + + if (!validateResponse.success) { + throw new Error(validateResponse.error); + } + + // Validate the license key + const licenseKeyRes = validateResponse.data.licenseKeys[key]; + if (!licenseKeyRes) { + throw new Error("Invalid license key"); + } + + const payload = validateJWT( + licenseKeyRes, + this.publicKey + ); + + if (!payload.valid) { + throw new Error("Invalid license key"); + } + + const encryptedToken = encrypt(licenseKeyRes, this.serverSecret); + // Encrypt the instanceId before storing + const encryptedInstanceId = encrypt(instanceId!, this.serverSecret); + + // Store the license key in the database + await db.insert(licenseKey).values({ + licenseKeyId: encryptedKey, + token: encryptedToken, + instanceId: encryptedInstanceId + }); + } catch (error) { + throw Error(`Error validating license key: ${error}`); + } + + // Invalidate the cache and re-compute the status + return await this.forceRecheck(); + } + + private async phoneHome( + keys: { + licenseKey: string; + instanceId: string; + }[], + doDecrypt = true + ): Promise { + // Decrypt the instanceIds before sending to the server + const decryptedKeys = keys.map((key) => ({ + licenseKey: key.licenseKey, + instanceId: + key.instanceId && doDecrypt + ? decrypt(key.instanceId, this.serverSecret) + : key.instanceId + })); + + const response = await fetch(this.validationServerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + licenseKeys: decryptedKeys, + instanceName: this.hostMeta.hostMetaId + }) + }); + + const data = await response.json(); + + return data as ValidateLicenseAPIResponse; + } +} + +await setHostMeta(); + +const [info] = await db.select().from(hostMeta).limit(1); + +if (!info) { + throw new Error("Host information not found"); +} + +export const license = new License(info); + +export default license; diff --git a/server/license/licenseJwt.ts b/server/private/license/licenseJwt.ts similarity index 88% rename from server/license/licenseJwt.ts rename to server/private/license/licenseJwt.ts index 3d148e51..f137db30 100644 --- a/server/license/licenseJwt.ts +++ b/server/private/license/licenseJwt.ts @@ -1,3 +1,16 @@ +/* + * 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 * as crypto from "crypto"; /** diff --git a/server/middlewares/private/index.ts b/server/private/middlewares/index.ts similarity index 92% rename from server/middlewares/private/index.ts rename to server/private/middlewares/index.ts index f034001d..c92b0d3d 100644 --- a/server/middlewares/private/index.ts +++ b/server/private/middlewares/index.ts @@ -15,4 +15,4 @@ export * from "./verifyCertificateAccess"; export * from "./verifyRemoteExitNodeAccess"; export * from "./verifyIdpAccess"; export * from "./verifyLoginPageAccess"; -export * from "./corsWithLoginPage"; \ No newline at end of file +export * from "../../lib/corsWithLoginPage"; \ No newline at end of file diff --git a/server/middlewares/private/verifyCertificateAccess.ts b/server/private/middlewares/verifyCertificateAccess.ts similarity index 100% rename from server/middlewares/private/verifyCertificateAccess.ts rename to server/private/middlewares/verifyCertificateAccess.ts diff --git a/server/middlewares/private/verifyIdpAccess.ts b/server/private/middlewares/verifyIdpAccess.ts similarity index 100% rename from server/middlewares/private/verifyIdpAccess.ts rename to server/private/middlewares/verifyIdpAccess.ts diff --git a/server/middlewares/private/verifyLoginPageAccess.ts b/server/private/middlewares/verifyLoginPageAccess.ts similarity index 100% rename from server/middlewares/private/verifyLoginPageAccess.ts rename to server/private/middlewares/verifyLoginPageAccess.ts diff --git a/server/middlewares/private/verifyRemoteExitNode.ts b/server/private/middlewares/verifyRemoteExitNode.ts similarity index 94% rename from server/middlewares/private/verifyRemoteExitNode.ts rename to server/private/middlewares/verifyRemoteExitNode.ts index 45c244e2..2f6d99d2 100644 --- a/server/middlewares/private/verifyRemoteExitNode.ts +++ b/server/private/middlewares/verifyRemoteExitNode.ts @@ -16,7 +16,7 @@ import ErrorResponse from "@server/types/ErrorResponse"; import config from "@server/lib/config"; import { unauthorized } from "@server/auth/unauthorizedResponse"; import logger from "@server/logger"; -import { validateRemoteExitNodeSessionToken } from "@server/auth/sessions/privateRemoteExitNode"; +import { validateRemoteExitNodeSessionToken } from "#private/auth/sessions/remoteExitNode"; export const verifySessionRemoteExitNodeMiddleware = async ( req: any, diff --git a/server/middlewares/private/verifyRemoteExitNodeAccess.ts b/server/private/middlewares/verifyRemoteExitNodeAccess.ts similarity index 100% rename from server/middlewares/private/verifyRemoteExitNodeAccess.ts rename to server/private/middlewares/verifyRemoteExitNodeAccess.ts diff --git a/server/middlewares/verifyValidLicense.ts b/server/private/middlewares/verifyValidLicense.ts similarity index 80% rename from server/middlewares/verifyValidLicense.ts rename to server/private/middlewares/verifyValidLicense.ts index 7e3bfee3..fde9116e 100644 --- a/server/middlewares/verifyValidLicense.ts +++ b/server/private/middlewares/verifyValidLicense.ts @@ -1,7 +1,8 @@ import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; -import license from "@server/license/license"; +import license from "#private/license/license"; +import { build } from "@server/build"; export async function verifyValidLicense( req: Request, @@ -9,6 +10,10 @@ export async function verifyValidLicense( next: NextFunction ) { try { + if (build != "enterprise") { + return next(); + } + const unlocked = await license.isUnlocked(); if (!unlocked) { return next( diff --git a/server/routers/auth/privateGetSessionTransferToken.ts b/server/private/routers/auth/getSessionTransferToken.ts similarity index 100% rename from server/routers/auth/privateGetSessionTransferToken.ts rename to server/private/routers/auth/getSessionTransferToken.ts diff --git a/server/private/routers/auth/index.ts b/server/private/routers/auth/index.ts new file mode 100644 index 00000000..39a60031 --- /dev/null +++ b/server/private/routers/auth/index.ts @@ -0,0 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./transferSession"; +export * from "./getSessionTransferToken"; +export * from "./quickStart"; \ No newline at end of file diff --git a/server/routers/auth/privateQuickStart.ts b/server/private/routers/auth/quickStart.ts similarity index 98% rename from server/routers/auth/privateQuickStart.ts rename to server/private/routers/auth/quickStart.ts index 683c24a8..582ac4d5 100644 --- a/server/routers/auth/privateQuickStart.ts +++ b/server/private/routers/auth/quickStart.ts @@ -50,16 +50,16 @@ import config from "@server/lib/config"; import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; import { UserType } from "@server/types/UserTypes"; -import { createUserAccountOrg } from "@server/lib/private/createUserAccountOrg"; +import { createUserAccountOrg } from "@server/lib/createUserAccountOrg"; import { sendEmail } from "@server/emails"; import WelcomeQuickStart from "@server/emails/templates/WelcomeQuickStart"; import { alphabet, generateRandomString } from "oslo/crypto"; import { createDate, TimeSpan } from "oslo"; import { getUniqueResourceName, getUniqueSiteName } from "@server/db/names"; -import { pickPort } from "../target/helpers"; -import { addTargets } from "../newt/targets"; +import { pickPort } from "@server/routers/target/helpers"; +import { addTargets } from "@server/routers/newt/targets"; import { isTargetValid } from "@server/lib/validators"; -import { listExitNodes } from "@server/lib/exitNodes"; +import { listExitNodes } from "#private/lib/exitNodes"; const bodySchema = z.object({ email: z.string().toLowerCase().email(), diff --git a/server/routers/auth/privateTransferSession.ts b/server/private/routers/auth/transferSession.ts similarity index 97% rename from server/routers/auth/privateTransferSession.ts rename to server/private/routers/auth/transferSession.ts index e75f77dd..52138a75 100644 --- a/server/routers/auth/privateTransferSession.ts +++ b/server/private/routers/auth/transferSession.ts @@ -26,6 +26,7 @@ import { sha256 } from "@oslojs/crypto/sha2"; import { serializeSessionCookie } from "@server/auth/sessions/app"; import { decrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; +import { TransferSessionResponse } from "@server/routers/auth/types"; const bodySchema = z.object({ token: z.string() @@ -33,11 +34,6 @@ const bodySchema = z.object({ export type TransferSessionBodySchema = z.infer; -export type TransferSessionResponse = { - valid: boolean; - cookie?: string; -}; - export async function transferSession( req: Request, res: Response, diff --git a/server/routers/private/billing/createCheckoutSession.ts b/server/private/routers/billing/createCheckoutSession.ts similarity index 95% rename from server/routers/private/billing/createCheckoutSession.ts rename to server/private/routers/billing/createCheckoutSession.ts index 67507b68..6e1e28c2 100644 --- a/server/routers/private/billing/createCheckoutSession.ts +++ b/server/private/routers/billing/createCheckoutSession.ts @@ -21,9 +21,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import config from "@server/lib/config"; import { fromError } from "zod-validation-error"; -import stripe from "@server/lib/private/stripe"; -import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/private/billing"; -import { getTierPriceSet, TierId } from "@server/lib/private/billing/tiers"; +import stripe from "#private/lib/stripe"; +import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing"; +import { getTierPriceSet, TierId } from "@server/lib/billing/tiers"; const createCheckoutSessionSchema = z .object({ diff --git a/server/routers/private/billing/createPortalSession.ts b/server/private/routers/billing/createPortalSession.ts similarity index 98% rename from server/routers/private/billing/createPortalSession.ts rename to server/private/routers/billing/createPortalSession.ts index aa672377..eb55f007 100644 --- a/server/routers/private/billing/createPortalSession.ts +++ b/server/private/routers/billing/createPortalSession.ts @@ -21,7 +21,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import config from "@server/lib/config"; import { fromError } from "zod-validation-error"; -import stripe from "@server/lib/private/stripe"; +import stripe from "#private/lib/stripe"; const createPortalSessionSchema = z .object({ diff --git a/server/routers/private/billing/getOrgSubscription.ts b/server/private/routers/billing/getOrgSubscription.ts similarity index 97% rename from server/routers/private/billing/getOrgSubscription.ts rename to server/private/routers/billing/getOrgSubscription.ts index 3e4f575e..b97ca39f 100644 --- a/server/routers/private/billing/getOrgSubscription.ts +++ b/server/private/routers/billing/getOrgSubscription.ts @@ -22,6 +22,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { GetOrgSubscriptionResponse } from "@server/routers/billing/types"; + // Import tables for billing import { customers, @@ -37,11 +39,6 @@ const getOrgSchema = z }) .strict(); -export type GetOrgSubscriptionResponse = { - subscription: Subscription | null; - items: SubscriptionItem[]; -}; - registry.registerPath({ method: "get", path: "/org/{orgId}/billing/subscription", diff --git a/server/routers/private/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts similarity index 94% rename from server/routers/private/billing/getOrgUsage.ts rename to server/private/routers/billing/getOrgUsage.ts index e1544e06..bc879659 100644 --- a/server/routers/private/billing/getOrgUsage.ts +++ b/server/private/routers/billing/getOrgUsage.ts @@ -23,8 +23,9 @@ import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { Limit, limits, Usage, usage } from "@server/db"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; +import { GetOrgUsageResponse } from "@server/routers/billing/types"; const getOrgSchema = z .object({ @@ -32,11 +33,6 @@ const getOrgSchema = z }) .strict(); -export type GetOrgUsageResponse = { - usage: Usage[]; - limits: Limit[]; -}; - registry.registerPath({ method: "get", path: "/org/{orgId}/billing/usage", diff --git a/server/routers/private/billing/hooks/handleCustomerCreated.ts b/server/private/routers/billing/hooks/handleCustomerCreated.ts similarity index 100% rename from server/routers/private/billing/hooks/handleCustomerCreated.ts rename to server/private/routers/billing/hooks/handleCustomerCreated.ts diff --git a/server/routers/private/billing/hooks/handleCustomerDeleted.ts b/server/private/routers/billing/hooks/handleCustomerDeleted.ts similarity index 100% rename from server/routers/private/billing/hooks/handleCustomerDeleted.ts rename to server/private/routers/billing/hooks/handleCustomerDeleted.ts diff --git a/server/routers/private/billing/hooks/handleCustomerUpdated.ts b/server/private/routers/billing/hooks/handleCustomerUpdated.ts similarity index 100% rename from server/routers/private/billing/hooks/handleCustomerUpdated.ts rename to server/private/routers/billing/hooks/handleCustomerUpdated.ts diff --git a/server/routers/private/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts similarity index 97% rename from server/routers/private/billing/hooks/handleSubscriptionCreated.ts rename to server/private/routers/billing/hooks/handleSubscriptionCreated.ts index ee6376c9..223a2545 100644 --- a/server/routers/private/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -22,9 +22,9 @@ import { } from "@server/db"; import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; -import stripe from "@server/lib/private/stripe"; +import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; -import { AudienceIds, moveEmailToAudience } from "@server/lib/private/resend"; +import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; export async function handleSubscriptionCreated( subscription: Stripe.Subscription diff --git a/server/routers/private/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts similarity index 97% rename from server/routers/private/billing/hooks/handleSubscriptionDeleted.ts rename to server/private/routers/billing/hooks/handleSubscriptionDeleted.ts index 95123731..114a4b30 100644 --- a/server/routers/private/billing/hooks/handleSubscriptionDeleted.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts @@ -16,7 +16,7 @@ import { subscriptions, db, subscriptionItems, customers, userOrgs, users } from import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; -import { AudienceIds, moveEmailToAudience } from "@server/lib/private/resend"; +import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; export async function handleSubscriptionDeleted( subscription: Stripe.Subscription diff --git a/server/routers/private/billing/hooks/handleSubscriptionUpdated.ts b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts similarity index 98% rename from server/routers/private/billing/hooks/handleSubscriptionUpdated.ts rename to server/private/routers/billing/hooks/handleSubscriptionUpdated.ts index f1cbcafe..01086054 100644 --- a/server/routers/private/billing/hooks/handleSubscriptionUpdated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts @@ -23,8 +23,8 @@ import { } from "@server/db"; import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; -import { getFeatureIdByMetricId } from "@server/lib/private/billing/features"; -import stripe from "@server/lib/private/stripe"; +import { getFeatureIdByMetricId } from "@server/lib/billing/features"; +import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; export async function handleSubscriptionUpdated( diff --git a/server/routers/private/billing/index.ts b/server/private/routers/billing/index.ts similarity index 100% rename from server/routers/private/billing/index.ts rename to server/private/routers/billing/index.ts diff --git a/server/routers/private/billing/internalGetOrgTier.ts b/server/private/routers/billing/internalGetOrgTier.ts similarity index 66% rename from server/routers/private/billing/internalGetOrgTier.ts rename to server/private/routers/billing/internalGetOrgTier.ts index 7f8cc642..cca96243 100644 --- a/server/routers/private/billing/internalGetOrgTier.ts +++ b/server/private/routers/billing/internalGetOrgTier.ts @@ -18,9 +18,8 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; -import { getTierPriceSet } from "@server/lib/private/billing/tiers"; -import { getOrgSubscriptionData } from "./getOrgSubscription"; -import { build } from "@server/build"; +import { getOrgTierData } from "#private/lib/billing"; +import { GetOrgTierResponse } from "@server/routers/billing/types"; const getOrgSchema = z .object({ @@ -28,11 +27,6 @@ const getOrgSchema = z }) .strict(); -export type GetOrgTierResponse = { - tier: string | null; - active: boolean; -}; - export async function getOrgTier( req: Request, res: Response, @@ -87,33 +81,3 @@ export async function getOrgTier( ); } } - -export async function getOrgTierData( - orgId: string -): Promise<{ tier: string | null; active: boolean }> { - let tier = null; - let active = false; - - if (build !== "saas") { - return { tier, active }; - } - - const { subscription, items } = await getOrgSubscriptionData(orgId); - - if (items && items.length > 0) { - const tierPriceSet = getTierPriceSet(); - // Iterate through tiers in order (earlier keys are higher tiers) - for (const [tierId, priceId] of Object.entries(tierPriceSet)) { - // Check if any subscription item matches this tier's price ID - const matchingItem = items.find((item) => item.priceId === priceId); - if (matchingItem) { - tier = tierId; - break; - } - } - } - if (subscription && subscription.status === "active") { - active = true; - } - return { tier, active }; -} diff --git a/server/routers/private/billing/subscriptionLifecycle.ts b/server/private/routers/billing/subscriptionLifecycle.ts similarity index 93% rename from server/routers/private/billing/subscriptionLifecycle.ts rename to server/private/routers/billing/subscriptionLifecycle.ts index 82dbfdbe..06b2a2a8 100644 --- a/server/routers/private/billing/subscriptionLifecycle.ts +++ b/server/private/routers/billing/subscriptionLifecycle.ts @@ -11,8 +11,8 @@ * This file is not licensed under the AGPLv3. */ -import { freeLimitSet, limitsService, subscribedLimitSet } from "@server/lib/private/billing"; -import { usageService } from "@server/lib/private/billing/usageService"; +import { freeLimitSet, limitsService, subscribedLimitSet } from "@server/lib/billing"; +import { usageService } from "@server/lib/billing/usageService"; import logger from "@server/logger"; export async function handleSubscriptionLifesycle(orgId: string, status: string) { diff --git a/server/routers/private/billing/webhooks.ts b/server/private/routers/billing/webhooks.ts similarity index 95% rename from server/routers/private/billing/webhooks.ts rename to server/private/routers/billing/webhooks.ts index 2844943a..24ad1074 100644 --- a/server/routers/private/billing/webhooks.ts +++ b/server/private/routers/billing/webhooks.ts @@ -11,8 +11,8 @@ * This file is not licensed under the AGPLv3. */ -import stripe from "@server/lib/private/stripe"; -import config from "@server/lib/config"; +import stripe from "#private/lib/stripe"; +import privateConfig from "#private/lib/config"; import logger from "@server/logger"; import createHttpError from "http-errors"; import { response } from "@server/lib/response"; @@ -26,13 +26,13 @@ import { handleCustomerUpdated } from "./hooks/handleCustomerUpdated"; import { handleSubscriptionDeleted } from "./hooks/handleSubscriptionDeleted"; import { handleCustomerDeleted } from "./hooks/handleCustomerDeleted"; -export async function stripeWebhookHandler( +export async function billingWebhookHandler( req: Request, res: Response, next: NextFunction ): Promise { let event: Stripe.Event = req.body; - const endpointSecret = config.getRawPrivateConfig().stripe?.webhook_secret; + const endpointSecret = privateConfig.getRawPrivateConfig().stripe?.webhook_secret; if (!endpointSecret) { logger.warn("Stripe webhook secret is not configured. Webhook events will not be priocessed."); return next( diff --git a/server/routers/private/certificates/createCertificate.ts b/server/private/routers/certificates/createCertificate.ts similarity index 86% rename from server/routers/private/certificates/createCertificate.ts rename to server/private/routers/certificates/createCertificate.ts index 210878ef..43a3426e 100644 --- a/server/routers/private/certificates/createCertificate.ts +++ b/server/private/routers/certificates/createCertificate.ts @@ -15,15 +15,19 @@ import { Certificate, certificates, db, domains } from "@server/db"; import logger from "@server/logger"; import { Transaction } from "@server/db"; import { eq, or, and, like } from "drizzle-orm"; -import { build } from "@server/build"; +import privateConfig from "#private/lib/config"; /** * Checks if a certificate exists for the given domain. * If not, creates a new certificate in 'pending' state. * Wildcard certs cover subdomains. */ -export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) { - if (build !== "saas") { +export async function createCertificate( + domainId: string, + domain: string, + trx: Transaction | typeof db +) { + if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { return; } @@ -39,7 +43,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T let existing: Certificate[] = []; if (domainRecord.type == "ns") { - const domainLevelDown = domain.split('.').slice(1).join('.'); + const domainLevelDown = domain.split(".").slice(1).join("."); existing = await trx .select() .from(certificates) @@ -49,7 +53,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T eq(certificates.wildcard, true), // only NS domains can have wildcard certs or( eq(certificates.domain, domain), - eq(certificates.domain, domainLevelDown), + eq(certificates.domain, domainLevelDown) ) ) ); @@ -67,9 +71,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T } if (existing.length > 0) { - logger.info( - `Certificate already exists for domain ${domain}` - ); + logger.info(`Certificate already exists for domain ${domain}`); return; } diff --git a/server/routers/private/certificates/getCertificate.ts b/server/private/routers/certificates/getCertificate.ts similarity index 93% rename from server/routers/private/certificates/getCertificate.ts rename to server/private/routers/certificates/getCertificate.ts index a0bf74f6..8392cbc0 100644 --- a/server/routers/private/certificates/getCertificate.ts +++ b/server/private/routers/certificates/getCertificate.ts @@ -21,6 +21,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { registry } from "@server/openApi"; +import { GetCertificateResponse } from "@server/routers/certificates/types"; const getCertificateSchema = z .object({ @@ -96,20 +97,6 @@ async function query(domainId: string, domain: string) { return existing.length > 0 ? existing[0] : null; } -export type GetCertificateResponse = { - certId: number; - domain: string; - domainId: string; - wildcard: boolean; - status: string; // pending, requested, valid, expired, failed - expiresAt: string | null; - lastRenewalAttempt: Date | null; - createdAt: string; - updatedAt: string; - errorMessage?: string | null; - renewalCount: number; -} - registry.registerPath({ method: "get", path: "/org/{orgId}/certificate/{domainId}/{domain}", diff --git a/server/routers/private/certificates/index.ts b/server/private/routers/certificates/index.ts similarity index 100% rename from server/routers/private/certificates/index.ts rename to server/private/routers/certificates/index.ts diff --git a/server/routers/private/certificates/restartCertificate.ts b/server/private/routers/certificates/restartCertificate.ts similarity index 100% rename from server/routers/private/certificates/restartCertificate.ts rename to server/private/routers/certificates/restartCertificate.ts diff --git a/server/routers/domain/privateCheckDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts similarity index 94% rename from server/routers/domain/privateCheckDomainNamespaceAvailability.ts rename to server/private/routers/domain/checkDomainNamespaceAvailability.ts index f1a4e103..745af9d3 100644 --- a/server/routers/domain/privateCheckDomainNamespaceAvailability.ts +++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts @@ -21,6 +21,7 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { db, domainNamespaces, resources } from "@server/db"; import { inArray } from "drizzle-orm"; +import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; const paramsSchema = z.object({}).strict(); @@ -30,15 +31,6 @@ const querySchema = z }) .strict(); -export type CheckDomainAvailabilityResponse = { - available: boolean; - options: { - domainNamespaceId: string; - domainId: string; - fullDomain: string; - }[]; -}; - registry.registerPath({ method: "get", path: "/domain/check-namespace-availability", diff --git a/server/private/routers/domain/index.ts b/server/private/routers/domain/index.ts new file mode 100644 index 00000000..da9cec3f --- /dev/null +++ b/server/private/routers/domain/index.ts @@ -0,0 +1,15 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./checkDomainNamespaceAvailability"; +export * from "./listDomainNamespaces"; \ No newline at end of file diff --git a/server/routers/domain/privateListDomainNamespaces.ts b/server/private/routers/domain/listDomainNamespaces.ts similarity index 100% rename from server/routers/domain/privateListDomainNamespaces.ts rename to server/private/routers/domain/listDomainNamespaces.ts diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts new file mode 100644 index 00000000..74cd6b0c --- /dev/null +++ b/server/private/routers/external.ts @@ -0,0 +1,336 @@ +/* + * 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 * as certificates from "#private/routers/certificates"; +import { createStore } from "#private/lib/rateLimitStore"; +import * as billing from "#private/routers/billing"; +import * as remoteExitNode from "#private/routers/remoteExitNode"; +import * as loginPage from "#private/routers/loginPage"; +import * as orgIdp from "#private/routers/orgIdp"; +import * as domain from "#private/routers/domain"; +import * as auth from "#private/routers/auth"; +import * as license from "#private/routers/license"; +import * as generateLicense from "./generatedLicense"; + +import { Router } from "express"; +import { + verifyOrgAccess, + verifyUserHasAction, + verifyUserIsOrgOwner, + verifyUserIsServerAdmin +} from "@server/middlewares"; +import { ActionsEnum } from "@server/auth/actions"; +import { + verifyCertificateAccess, + verifyIdpAccess, + verifyLoginPageAccess, + verifyRemoteExitNodeAccess +} from "#private/middlewares"; +import rateLimit, { ipKeyGenerator } from "express-rate-limit"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { verifyValidLicense } from "../middlewares/verifyValidLicense"; +import { build } from "@server/build"; +import { + unauthenticated as ua, + authenticated as a, + authRouter as aa +} from "@server/routers/external"; + +export const authenticated = a; +export const unauthenticated = ua; +export const authRouter = aa; + +unauthenticated.post( + "/remote-exit-node/quick-start", + verifyValidLicense, + rateLimit({ + windowMs: 60 * 60 * 1000, + max: 5, + keyGenerator: (req) => `${req.path}:${ipKeyGenerator(req.ip || "")}`, + handler: (req, res, next) => { + const message = `You can only create 5 remote exit nodes every hour. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + }, + store: createStore() + }), + remoteExitNode.quickStartRemoteExitNode +); + +authenticated.put( + "/org/:orgId/idp/oidc", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createIdp), + orgIdp.createOrgOidcIdp +); + +authenticated.post( + "/org/:orgId/idp/:idpId/oidc", + verifyValidLicense, + verifyOrgAccess, + verifyIdpAccess, + verifyUserHasAction(ActionsEnum.updateIdp), + orgIdp.updateOrgOidcIdp +); + +authenticated.delete( + "/org/:orgId/idp/:idpId", + verifyValidLicense, + verifyOrgAccess, + verifyIdpAccess, + verifyUserHasAction(ActionsEnum.deleteIdp), + orgIdp.deleteOrgIdp +); + +authenticated.get( + "/org/:orgId/idp/:idpId", + verifyValidLicense, + verifyOrgAccess, + verifyIdpAccess, + verifyUserHasAction(ActionsEnum.getIdp), + orgIdp.getOrgIdp +); + +authenticated.get( + "/org/:orgId/idp", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listIdps), + orgIdp.listOrgIdps +); + +authenticated.get("/org/:orgId/idp", orgIdp.listOrgIdps); // anyone can see this; it's just a list of idp names and ids + +authenticated.get( + "/org/:orgId/certificate/:domainId/:domain", + verifyValidLicense, + verifyOrgAccess, + verifyCertificateAccess, + verifyUserHasAction(ActionsEnum.getCertificate), + certificates.getCertificate +); + +authenticated.post( + "/org/:orgId/certificate/:certId/restart", + verifyValidLicense, + verifyOrgAccess, + verifyCertificateAccess, + verifyUserHasAction(ActionsEnum.restartCertificate), + certificates.restartCertificate +); + +if (build === "saas") { + unauthenticated.post( + "/quick-start", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + keyGenerator: (req) => req.path, + handler: (req, res, next) => { + const message = `We're too busy right now. Please try again later.`; + return next( + createHttpError(HttpCode.TOO_MANY_REQUESTS, message) + ); + }, + store: createStore() + }), + auth.quickStart + ); + + authenticated.post( + "/org/:orgId/billing/create-checkout-session", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + billing.createCheckoutSession + ); + + authenticated.post( + "/org/:orgId/billing/create-portal-session", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + billing.createPortalSession + ); + + authenticated.get( + "/org/:orgId/billing/subscription", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + billing.getOrgSubscription + ); + + authenticated.get( + "/org/:orgId/billing/usage", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + billing.getOrgUsage + ); + + authenticated.get( + "/org/:orgId/license", + verifyOrgAccess, + generateLicense.listSaasLicenseKeys + ); + + authenticated.put( + "/org/:orgId/license", + verifyOrgAccess, + generateLicense.generateNewLicense + ); +} + +authenticated.get( + "/domain/namespaces", + verifyValidLicense, + domain.listDomainNamespaces +); + +authenticated.get( + "/domain/check-namespace-availability", + verifyValidLicense, + domain.checkDomainNamespaceAvailability +); + +authenticated.put( + "/org/:orgId/remote-exit-node", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createRemoteExitNode), + remoteExitNode.createRemoteExitNode +); + +authenticated.get( + "/org/:orgId/remote-exit-nodes", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listRemoteExitNode), + remoteExitNode.listRemoteExitNodes +); + +authenticated.get( + "/org/:orgId/remote-exit-node/:remoteExitNodeId", + verifyValidLicense, + verifyOrgAccess, + verifyRemoteExitNodeAccess, + verifyUserHasAction(ActionsEnum.getRemoteExitNode), + remoteExitNode.getRemoteExitNode +); + +authenticated.get( + "/org/:orgId/pick-remote-exit-node-defaults", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createRemoteExitNode), + remoteExitNode.pickRemoteExitNodeDefaults +); + +authenticated.delete( + "/org/:orgId/remote-exit-node/:remoteExitNodeId", + verifyValidLicense, + verifyOrgAccess, + verifyRemoteExitNodeAccess, + verifyUserHasAction(ActionsEnum.deleteRemoteExitNode), + remoteExitNode.deleteRemoteExitNode +); + +authenticated.put( + "/org/:orgId/login-page", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createLoginPage), + loginPage.createLoginPage +); + +authenticated.post( + "/org/:orgId/login-page/:loginPageId", + verifyValidLicense, + verifyOrgAccess, + verifyLoginPageAccess, + verifyUserHasAction(ActionsEnum.updateLoginPage), + loginPage.updateLoginPage +); + +authenticated.delete( + "/org/:orgId/login-page/:loginPageId", + verifyValidLicense, + verifyOrgAccess, + verifyLoginPageAccess, + verifyUserHasAction(ActionsEnum.deleteLoginPage), + loginPage.deleteLoginPage +); + +authenticated.get( + "/org/:orgId/login-page", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getLoginPage), + loginPage.getLoginPage +); + +authRouter.post( + "/remoteExitNode/get-token", + verifyValidLicense, + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 900, + keyGenerator: (req) => + `remoteExitNodeGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`, + handler: (req, res, next) => { + const message = `You can only request an remoteExitNodeToken token ${900} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + }, + store: createStore() + }), + remoteExitNode.getRemoteExitNodeToken +); + +authRouter.post( + "/transfer-session-token", + verifyValidLicense, + rateLimit({ + windowMs: 1 * 60 * 1000, + max: 60, + keyGenerator: (req) => + `transferSessionToken:${ipKeyGenerator(req.ip || "")}`, + handler: (req, res, next) => { + const message = `You can only transfer a session token ${5} times every ${1} minute. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + }, + store: createStore() + }), + auth.transferSession +); + +authenticated.post( + "/license/activate", + verifyUserIsServerAdmin, + license.activateLicense +); + +authenticated.get( + "/license/keys", + verifyUserIsServerAdmin, + license.listLicenseKeys +); + +authenticated.delete( + "/license/:licenseKey", + verifyUserIsServerAdmin, + license.deleteLicenseKey +); + +authenticated.post( + "/license/recheck", + verifyUserIsServerAdmin, + license.recheckStatus +); diff --git a/server/private/routers/generatedLicense/generateNewLicense.ts b/server/private/routers/generatedLicense/generateNewLicense.ts new file mode 100644 index 00000000..fb6ce835 --- /dev/null +++ b/server/private/routers/generatedLicense/generateNewLicense.ts @@ -0,0 +1,73 @@ +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib/response"; +import privateConfig from "@server/private/lib/config"; +import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types"; + +async function createNewLicense(orgId: string, licenseData: any): Promise { + try { + const response = await fetch( + `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/create`, + { + method: "PUT", + headers: { + "api-key": + privateConfig.getRawPrivateConfig().server + .fossorial_api_key!, + "Content-Type": "application/json" + }, + body: JSON.stringify(licenseData) + } + ); + + const data = await response.json(); + + logger.debug("Fossorial API response:", {data}); + return data; + } catch (error) { + console.error("Error creating new license:", error); + throw error; + } +} + +export async function generateNewLicense( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const { orgId } = req.params; + + if (!orgId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Organization ID is required" + ) + ); + } + + logger.debug(`Generating new license for orgId: ${orgId}`); + + const licenseData = req.body; + const apiResponse = await createNewLicense(orgId, licenseData); + + return sendResponse(res, { + data: apiResponse.data, + success: apiResponse.success, + error: apiResponse.error, + message: apiResponse.message, + status: apiResponse.status + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred while generating new license" + ) + ); + } +} diff --git a/server/private/routers/generatedLicense/index.ts b/server/private/routers/generatedLicense/index.ts new file mode 100644 index 00000000..fa07430f --- /dev/null +++ b/server/private/routers/generatedLicense/index.ts @@ -0,0 +1,2 @@ +export * from "./listGeneratedLicenses"; +export * from "./generateNewLicense"; diff --git a/server/private/routers/generatedLicense/listGeneratedLicenses.ts b/server/private/routers/generatedLicense/listGeneratedLicenses.ts new file mode 100644 index 00000000..9b219453 --- /dev/null +++ b/server/private/routers/generatedLicense/listGeneratedLicenses.ts @@ -0,0 +1,72 @@ +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib/response"; +import privateConfig from "@server/private/lib/config"; +import { GeneratedLicenseKey, ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types"; + +async function fetchLicenseKeys(orgId: string): Promise { + try { + const response = await fetch( + `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/list`, + { + method: "GET", + headers: { + "api-key": + privateConfig.getRawPrivateConfig().server + .fossorial_api_key!, + "Content-Type": "application/json" + } + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Error fetching license keys:", error); + throw error; + } +} + +export async function listSaasLicenseKeys( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const { orgId } = req.params; + + if (!orgId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Organization ID is required" + ) + ); + } + + const apiResponse = await fetchLicenseKeys(orgId); + const keys: GeneratedLicenseKey[] = apiResponse.data.licenseKeys || []; + + return sendResponse(res, { + data: keys, + success: true, + error: false, + message: "Successfully retrieved license keys", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred while fetching license keys" + ) + ); + } +} diff --git a/server/routers/gerbil/privateCreateExitNode.ts b/server/private/routers/gerbil/createExitNode.ts similarity index 98% rename from server/routers/gerbil/privateCreateExitNode.ts rename to server/private/routers/gerbil/createExitNode.ts index 9c2a104d..9577d8e9 100644 --- a/server/routers/gerbil/privateCreateExitNode.ts +++ b/server/private/routers/gerbil/createExitNode.ts @@ -50,6 +50,7 @@ export async function createExitNode( endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`, address, listenPort, + online: true, reachableAt, name: exitNodeName }) diff --git a/server/routers/gerbil/privateReceiveBandwidth.ts b/server/private/routers/gerbil/receiveBandwidth.ts similarity index 100% rename from server/routers/gerbil/privateReceiveBandwidth.ts rename to server/private/routers/gerbil/receiveBandwidth.ts diff --git a/server/routers/private/hybrid.ts b/server/private/routers/hybrid.ts similarity index 93% rename from server/routers/private/hybrid.ts rename to server/private/routers/hybrid.ts index 2b59aa40..54c823a5 100644 --- a/server/routers/private/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -11,7 +11,7 @@ * This file is not licensed under the AGPLv3. */ -import { verifySessionRemoteExitNodeMiddleware } from "@server/middlewares/private/verifyRemoteExitNode"; +import { verifySessionRemoteExitNodeMiddleware } from "#private/middlewares/verifyRemoteExitNode"; import { Router } from "express"; import { db, @@ -55,22 +55,24 @@ import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { getTraefikConfig } from "../../lib/traefik"; +import { getTraefikConfig } from "#private/lib/traefik"; import { generateGerbilConfig, generateRelayMappings, updateAndGenerateEndpointDestinations, updateSiteBandwidth -} from "../gerbil"; +} from "@server/routers/gerbil"; import * as gerbil from "@server/routers/gerbil"; import logger from "@server/logger"; import { decryptData } from "@server/lib/encryption"; -import { config } from "@server/lib/config"; +import config from "@server/lib/config"; +import privateConfig from "#private/lib/config"; import * as fs from "fs"; -import { exchangeSession } from "../badger"; +import { exchangeSession } from "@server/routers/badger"; import { validateResourceSessionToken } from "@server/auth/sessions/resource"; -import { checkExitNodeOrg, resolveExitNodes } from "@server/lib/exitNodes"; +import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes"; import { maxmindLookup } from "@server/db/maxmind"; +import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; // Zod schemas for request validation const getResourceByDomainParamsSchema = z @@ -161,6 +163,14 @@ const validateResourceSessionTokenBodySchema = z }) .strict(); +const validateResourceAccessTokenBodySchema = z + .object({ + accessTokenId: z.string().optional(), + resourceId: z.number().optional(), + accessToken: z.string() + }) + .strict(); + // Certificates by domains query validation const getCertificatesByDomainsQuerySchema = z .object({ @@ -211,9 +221,36 @@ export type UserSessionWithUser = { }; // Root routes -const hybridRouter = Router(); +export const hybridRouter = Router(); hybridRouter.use(verifySessionRemoteExitNodeMiddleware); +hybridRouter.get( + "/general-config", + async (req: Request, res: Response, next: NextFunction) => { + return response(res, { + data: { + resource_session_request_param: + config.getRawConfig().server.resource_session_request_param, + resource_access_token_headers: + config.getRawConfig().server.resource_access_token_headers, + resource_access_token_param: + config.getRawConfig().server.resource_access_token_param, + session_cookie_name: + config.getRawConfig().server.session_cookie_name, + require_email_verification: + config.getRawConfig().flags?.require_email_verification || + false, + resource_session_length_hours: + config.getRawConfig().server.resource_session_length_hours + }, + success: true, + error: false, + message: "General config retrieved successfully", + status: HttpCode.OK + }); + } +); + hybridRouter.get( "/traefik-config", async (req: Request, res: Response, next: NextFunction) => { @@ -232,8 +269,10 @@ hybridRouter.get( const traefikConfig = await getTraefikConfig( remoteExitNode.exitNodeId, ["newt", "local", "wireguard"], // Allow them to use all the site types - true // But don't allow domain namespace resources + true, // But don't allow domain namespace resources + false // Dont include login pages ); + return response(res, { data: traefikConfig, success: true, @@ -387,7 +426,7 @@ hybridRouter.get( } const encryptionKeyPath = - config.getRawPrivateConfig().server.encryption_key_path; + privateConfig.getRawPrivateConfig().server.encryption_key_path; if (!fs.existsSync(encryptionKeyPath)) { throw new Error( @@ -1100,6 +1139,52 @@ hybridRouter.post( } ); +// Validate resource session token +hybridRouter.post( + "/resource/:resourceId/access-token/verify", + async (req: Request, res: Response, next: NextFunction) => { + try { + const parsedBody = validateResourceAccessTokenBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { accessToken, resourceId, accessTokenId } = parsedBody.data; + + const result = await verifyResourceAccessToken({ + accessTokenId, + accessToken, + resourceId + }); + + return response(res, { + data: result, + success: true, + error: false, + message: result.valid + ? "Resource access token is valid" + : "Resource access token is invalid or expired", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to validate resource session token" + ) + ); + } + } +); + const geoIpLookupParamsSchema = z.object({ ip: z.string().ip() }); @@ -1489,5 +1574,3 @@ hybridRouter.post( } } ); - -export default hybridRouter; diff --git a/server/private/routers/integration.ts b/server/private/routers/integration.ts new file mode 100644 index 00000000..d767424a --- /dev/null +++ b/server/private/routers/integration.ts @@ -0,0 +1,42 @@ +/* + * 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 * as orgIdp from "#private/routers/orgIdp"; +import * as org from "#private/routers/org"; + +import { Router } from "express"; +import { + verifyApiKey, + verifyApiKeyHasAction, + verifyApiKeyIsRoot, +} from "@server/middlewares"; +import { ActionsEnum } from "@server/auth/actions"; + +import { unauthenticated as ua, authenticated as a } from "@server/routers/integration"; + +export const unauthenticated = ua; +export const authenticated = a; + +authenticated.post( + `/org/:orgId/send-usage-notification`, + verifyApiKeyIsRoot, // We are the only ones who can use root key so its fine + verifyApiKeyHasAction(ActionsEnum.sendUsageNotification), + org.sendUsageNotification +); + +authenticated.delete( + "/idp/:idpId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.deleteIdp), + orgIdp.deleteOrgIdp +); \ No newline at end of file diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts new file mode 100644 index 00000000..b393b884 --- /dev/null +++ b/server/private/routers/internal.ts @@ -0,0 +1,38 @@ +/* + * 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 * as loginPage from "#private/routers/loginPage"; +import * as auth from "#private/routers/auth"; +import * as orgIdp from "#private/routers/orgIdp"; +import * as billing from "#private/routers/billing"; +import * as license from "#private/routers/license"; + +import { verifySessionUserMiddleware } from "@server/middlewares"; + +import { internalRouter as ir } from "@server/routers/internal"; + +export const internalRouter = ir; + +internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps); + +internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier); + +internalRouter.get("/login-page", loginPage.loadLoginPage); + +internalRouter.post( + "/get-session-transfer-token", + verifySessionUserMiddleware, + auth.getSessionTransferToken +); + +internalRouter.get(`/license/status`, license.getLicenseStatus); diff --git a/server/routers/license/activateLicense.ts b/server/private/routers/license/activateLicense.ts similarity index 93% rename from server/routers/license/activateLicense.ts rename to server/private/routers/license/activateLicense.ts index 832bc19d..f7745489 100644 --- a/server/routers/license/activateLicense.ts +++ b/server/private/routers/license/activateLicense.ts @@ -3,7 +3,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; -import license, { LicenseStatus } from "@server/license/license"; +import license from "#private/license/license"; import { z } from "zod"; import { fromError } from "zod-validation-error"; @@ -13,8 +13,6 @@ const bodySchema = z }) .strict(); -export type ActivateLicenseStatus = LicenseStatus; - export async function activateLicense( req: Request, res: Response, diff --git a/server/routers/license/deleteLicenseKey.ts b/server/private/routers/license/deleteLicenseKey.ts similarity index 90% rename from server/routers/license/deleteLicenseKey.ts rename to server/private/routers/license/deleteLicenseKey.ts index 37b74fee..922da0ad 100644 --- a/server/routers/license/deleteLicenseKey.ts +++ b/server/private/routers/license/deleteLicenseKey.ts @@ -8,9 +8,7 @@ import { fromError } from "zod-validation-error"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import { licenseKey } from "@server/db"; -import license, { LicenseStatus } from "@server/license/license"; -import { encrypt } from "@server/lib/crypto"; -import config from "@server/lib/config"; +import license from "#private/license/license"; const paramsSchema = z .object({ @@ -18,8 +16,6 @@ const paramsSchema = z }) .strict(); -export type DeleteLicenseKeyResponse = LicenseStatus; - export async function deleteLicenseKey( req: Request, res: Response, diff --git a/server/routers/license/getLicenseStatus.ts b/server/private/routers/license/getLicenseStatus.ts similarity index 87% rename from server/routers/license/getLicenseStatus.ts rename to server/private/routers/license/getLicenseStatus.ts index e4f28882..838632f2 100644 --- a/server/routers/license/getLicenseStatus.ts +++ b/server/private/routers/license/getLicenseStatus.ts @@ -3,9 +3,8 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; -import license, { LicenseStatus } from "@server/license/license"; - -export type GetLicenseStatusResponse = LicenseStatus; +import license from "#private/license/license"; +import { GetLicenseStatusResponse } from "@server/routers/license/types"; export async function getLicenseStatus( req: Request, diff --git a/server/routers/license/index.ts b/server/private/routers/license/index.ts similarity index 100% rename from server/routers/license/index.ts rename to server/private/routers/license/index.ts diff --git a/server/routers/license/listLicenseKeys.ts b/server/private/routers/license/listLicenseKeys.ts similarity index 86% rename from server/routers/license/listLicenseKeys.ts rename to server/private/routers/license/listLicenseKeys.ts index d106abd7..4aa35335 100644 --- a/server/routers/license/listLicenseKeys.ts +++ b/server/private/routers/license/listLicenseKeys.ts @@ -3,9 +3,8 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; -import license, { LicenseKeyCache } from "@server/license/license"; - -export type ListLicenseKeysResponse = LicenseKeyCache[]; +import license from "#private/license/license"; +import { ListLicenseKeysResponse } from "@server/routers/license/types"; export async function listLicenseKeys( req: Request, diff --git a/server/routers/license/recheckStatus.ts b/server/private/routers/license/recheckStatus.ts similarity index 89% rename from server/routers/license/recheckStatus.ts rename to server/private/routers/license/recheckStatus.ts index cd4bf779..27bf60bb 100644 --- a/server/routers/license/recheckStatus.ts +++ b/server/private/routers/license/recheckStatus.ts @@ -3,9 +3,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; -import license, { LicenseStatus } from "@server/license/license"; - -export type RecheckStatusResponse = LicenseStatus; +import license from "#private/license/license"; export async function recheckStatus( req: Request, diff --git a/server/routers/private/loginPage/createLoginPage.ts b/server/private/routers/loginPage/createLoginPage.ts similarity index 95% rename from server/routers/private/loginPage/createLoginPage.ts rename to server/private/routers/loginPage/createLoginPage.ts index fca29aae..cb0bb923 100644 --- a/server/routers/private/loginPage/createLoginPage.ts +++ b/server/private/routers/loginPage/createLoginPage.ts @@ -29,10 +29,11 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; -import { createCertificate } from "@server/routers/private/certificates/createCertificate"; -import { getOrgTierData } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { createCertificate } from "#private/routers/certificates/createCertificate"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; +import { CreateLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z .object({ @@ -49,8 +50,6 @@ const bodySchema = z export type CreateLoginPageBody = z.infer; -export type CreateLoginPageResponse = LoginPage; - export async function createLoginPage( req: Request, res: Response, diff --git a/server/routers/private/loginPage/deleteLoginPage.ts b/server/private/routers/loginPage/deleteLoginPage.ts similarity index 97% rename from server/routers/private/loginPage/deleteLoginPage.ts rename to server/private/routers/loginPage/deleteLoginPage.ts index 7cc957a2..bf7941e7 100644 --- a/server/routers/private/loginPage/deleteLoginPage.ts +++ b/server/private/routers/loginPage/deleteLoginPage.ts @@ -20,6 +20,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; +import { DeleteLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z .object({ @@ -28,8 +29,6 @@ const paramsSchema = z }) .strict(); -export type DeleteLoginPageResponse = LoginPage; - export async function deleteLoginPage( req: Request, res: Response, diff --git a/server/routers/private/loginPage/getLoginPage.ts b/server/private/routers/loginPage/getLoginPage.ts similarity index 96% rename from server/routers/private/loginPage/getLoginPage.ts rename to server/private/routers/loginPage/getLoginPage.ts index 9c9a18f5..76e20ffb 100644 --- a/server/routers/private/loginPage/getLoginPage.ts +++ b/server/private/routers/loginPage/getLoginPage.ts @@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +import { GetLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z .object({ @@ -40,10 +41,6 @@ async function query(orgId: string) { return res?.loginPage; } -export type GetLoginPageResponse = NonNullable< - Awaited> ->; - export async function getLoginPage( req: Request, res: Response, diff --git a/server/routers/private/loginPage/index.ts b/server/private/routers/loginPage/index.ts similarity index 100% rename from server/routers/private/loginPage/index.ts rename to server/private/routers/loginPage/index.ts diff --git a/server/routers/private/loginPage/loadLoginPage.ts b/server/private/routers/loginPage/loadLoginPage.ts similarity index 97% rename from server/routers/private/loginPage/loadLoginPage.ts rename to server/private/routers/loginPage/loadLoginPage.ts index 91cc002e..133336b6 100644 --- a/server/routers/private/loginPage/loadLoginPage.ts +++ b/server/private/routers/loginPage/loadLoginPage.ts @@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; const querySchema = z.object({ resourceId: z.coerce.number().int().positive().optional(), @@ -70,10 +71,6 @@ async function query(orgId: string | undefined, fullDomain: string) { }; } -export type LoadLoginPageResponse = NonNullable< - Awaited> -> & { orgId: string }; - export async function loadLoginPage( req: Request, res: Response, diff --git a/server/routers/private/loginPage/updateLoginPage.ts b/server/private/routers/loginPage/updateLoginPage.ts similarity index 96% rename from server/routers/private/loginPage/updateLoginPage.ts rename to server/private/routers/loginPage/updateLoginPage.ts index 9c19913d..4f2be084 100644 --- a/server/routers/private/loginPage/updateLoginPage.ts +++ b/server/private/routers/loginPage/updateLoginPage.ts @@ -22,10 +22,11 @@ import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { subdomainSchema } from "@server/lib/schemas"; -import { createCertificate } from "@server/routers/private/certificates/createCertificate"; -import { getOrgTierData } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { createCertificate } from "#private/routers/certificates/createCertificate"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; +import { UpdateLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z .object({ @@ -55,8 +56,6 @@ const bodySchema = z export type UpdateLoginPageBody = z.infer; -export type UpdateLoginPageResponse = LoginPage; - export async function updateLoginPage( req: Request, res: Response, diff --git a/server/private/routers/org/index.ts b/server/private/routers/org/index.ts new file mode 100644 index 00000000..189c5323 --- /dev/null +++ b/server/private/routers/org/index.ts @@ -0,0 +1,14 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./sendUsageNotifications"; \ No newline at end of file diff --git a/server/routers/org/privateSendUsageNotifications.ts b/server/private/routers/org/sendUsageNotifications.ts similarity index 98% rename from server/routers/org/privateSendUsageNotifications.ts rename to server/private/routers/org/sendUsageNotifications.ts index 8b2a773d..3ef27f91 100644 --- a/server/routers/org/privateSendUsageNotifications.ts +++ b/server/private/routers/org/sendUsageNotifications.ts @@ -22,8 +22,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { sendEmail } from "@server/emails"; -import NotifyUsageLimitApproaching from "@server/emails/templates/PrivateNotifyUsageLimitApproaching"; -import NotifyUsageLimitReached from "@server/emails/templates/PrivateNotifyUsageLimitReached"; +import NotifyUsageLimitApproaching from "@server/emails/templates/NotifyUsageLimitApproaching"; +import NotifyUsageLimitReached from "@server/emails/templates/NotifyUsageLimitReached"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; diff --git a/server/routers/private/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts similarity index 96% rename from server/routers/private/orgIdp/createOrgOidcIdp.ts rename to server/private/routers/orgIdp/createOrgOidcIdp.ts index 16697f98..02cef526 100644 --- a/server/routers/private/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -25,8 +25,9 @@ import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; import { build } from "@server/build"; -import { getOrgTierData } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; const paramsSchema = z.object({ orgId: z.string().nonempty() }).strict(); @@ -47,11 +48,6 @@ const bodySchema = z }) .strict(); -export type CreateOrgIdpResponse = { - idpId: number; - redirectUrl: string; -}; - // registry.registerPath({ // method: "put", // path: "/idp/oidc", diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts new file mode 100644 index 00000000..711d1ce3 --- /dev/null +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -0,0 +1,108 @@ +/* + * 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 } 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 { idp, idpOidcConfig, idpOrg } from "@server/db"; +import { eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + orgId: z.string().optional(), // Optional; used with org idp in saas + idpId: z.coerce.number() + }) + .strict(); + +registry.registerPath({ + method: "delete", + path: "/idp/{idpId}", + description: "Delete IDP.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema + }, + responses: {} +}); + +export async function deleteOrgIdp( + 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 { idpId } = parsedParams.data; + + // Check if IDP exists + const [existingIdp] = await db + .select() + .from(idp) + .where(eq(idp.idpId, idpId)); + + if (!existingIdp) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "IdP not found" + ) + ); + } + + // Delete the IDP and its related records in a transaction + await db.transaction(async (trx) => { + // Delete OIDC config if it exists + await trx + .delete(idpOidcConfig) + .where(eq(idpOidcConfig.idpId, idpId)); + + // Delete IDP-org mappings + await trx + .delete(idpOrg) + .where(eq(idpOrg.idpId, idpId)); + + // Delete the IDP itself + await trx + .delete(idp) + .where(eq(idp.idpId, idpId)); + }); + + return response(res, { + data: null, + success: true, + error: false, + message: "IdP deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/private/orgIdp/getOrgIdp.ts b/server/private/routers/orgIdp/getOrgIdp.ts similarity index 96% rename from server/routers/private/orgIdp/getOrgIdp.ts rename to server/private/routers/orgIdp/getOrgIdp.ts index 73ccdcbb..0e6689fc 100644 --- a/server/routers/private/orgIdp/getOrgIdp.ts +++ b/server/private/routers/orgIdp/getOrgIdp.ts @@ -25,6 +25,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import config from "@server/lib/config"; import { decrypt } from "@server/lib/crypto"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; +import { GetOrgIdpResponse } from "@server/routers/orgIdp/types"; const paramsSchema = z .object({ @@ -47,10 +48,6 @@ async function query(idpId: number, orgId: string) { return res; } -export type GetOrgIdpResponse = NonNullable< - Awaited> -> & { redirectUrl: string }; - // registry.registerPath({ // method: "get", // path: "/idp/{idpId}", diff --git a/server/routers/private/orgIdp/index.ts b/server/private/routers/orgIdp/index.ts similarity index 87% rename from server/routers/private/orgIdp/index.ts rename to server/private/routers/orgIdp/index.ts index 99c30654..562582c6 100644 --- a/server/routers/private/orgIdp/index.ts +++ b/server/private/routers/orgIdp/index.ts @@ -14,4 +14,5 @@ export * from "./createOrgOidcIdp"; export * from "./getOrgIdp"; export * from "./listOrgIdps"; -export * from "./updateOrgOidcIdp"; \ No newline at end of file +export * from "./updateOrgOidcIdp"; +export * from "./deleteOrgIdp"; \ No newline at end of file diff --git a/server/routers/private/orgIdp/listOrgIdps.ts b/server/private/routers/orgIdp/listOrgIdps.ts similarity index 95% rename from server/routers/private/orgIdp/listOrgIdps.ts rename to server/private/routers/orgIdp/listOrgIdps.ts index 208732de..0c69ff8d 100644 --- a/server/routers/private/orgIdp/listOrgIdps.ts +++ b/server/private/routers/orgIdp/listOrgIdps.ts @@ -22,6 +22,7 @@ import { eq, sql } from "drizzle-orm"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; const querySchema = z .object({ @@ -65,15 +66,6 @@ async function query(orgId: string, limit: number, offset: number) { return res; } -export type ListOrgIdpsResponse = { - idps: Awaited>; - pagination: { - total: number; - limit: number; - offset: number; - }; -}; - // registry.registerPath({ // method: "get", // path: "/idp", diff --git a/server/routers/private/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts similarity index 97% rename from server/routers/private/orgIdp/updateOrgOidcIdp.ts rename to server/private/routers/orgIdp/updateOrgOidcIdp.ts index a3be85c3..c6e54240 100644 --- a/server/routers/private/orgIdp/updateOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts @@ -24,10 +24,9 @@ import { idp, idpOidcConfig } from "@server/db"; import { eq, and } from "drizzle-orm"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -import license from "@server/license/license"; import { build } from "@server/build"; -import { getOrgTierData } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; const paramsSchema = z .object({ diff --git a/server/routers/private/remoteExitNode/createRemoteExitNode.ts b/server/private/routers/remoteExitNode/createRemoteExitNode.ts similarity index 86% rename from server/routers/private/remoteExitNode/createRemoteExitNode.ts rename to server/private/routers/remoteExitNode/createRemoteExitNode.ts index ac4fd231..28102fab 100644 --- a/server/routers/private/remoteExitNode/createRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/createRemoteExitNode.ts @@ -21,25 +21,20 @@ import response from "@server/lib/response"; import { SqliteError } from "better-sqlite3"; import moment from "moment"; import { generateSessionToken } from "@server/auth/sessions/app"; -import { createRemoteExitNodeSession } from "@server/auth/sessions/privateRemoteExitNode"; +import { createRemoteExitNodeSession } from "#private/auth/sessions/remoteExitNode"; import { fromError } from "zod-validation-error"; import { hashPassword, verifyPassword } from "@server/auth/password"; import logger from "@server/logger"; import { and, eq } from "drizzle-orm"; import { getNextAvailableSubnet } from "@server/lib/exitNodes"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; +import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; export const paramsSchema = z.object({ orgId: z.string() }); -export type CreateRemoteExitNodeResponse = { - token: string; - remoteExitNodeId: string; - secret: string; -}; - const bodySchema = z .object({ remoteExitNodeId: z.string().length(15), @@ -89,30 +84,25 @@ export async function createRemoteExitNode( orgId, FeatureId.REMOTE_EXIT_NODES ); - if (!usage) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "No usage data found for this organization" - ) - ); - } - const rejectRemoteExitNodes = await usageService.checkLimitSet( - orgId, - false, - FeatureId.REMOTE_EXIT_NODES, - { - ...usage, - instantaneousValue: (usage.instantaneousValue || 0) + 1 - } // We need to add one to know if we are violating the limit - ); - if (rejectRemoteExitNodes) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io" - ) + if (usage) { + const rejectRemoteExitNodes = await usageService.checkLimitSet( + orgId, + false, + FeatureId.REMOTE_EXIT_NODES, + { + ...usage, + instantaneousValue: (usage.instantaneousValue || 0) + 1 + } // We need to add one to know if we are violating the limit ); + + if (rejectRemoteExitNodes) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io" + ) + ); + } } const secretHash = await hashPassword(secret); diff --git a/server/routers/private/remoteExitNode/deleteRemoteExitNode.ts b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts similarity index 96% rename from server/routers/private/remoteExitNode/deleteRemoteExitNode.ts rename to server/private/routers/remoteExitNode/deleteRemoteExitNode.ts index 84ef0fab..f7b9d56c 100644 --- a/server/routers/private/remoteExitNode/deleteRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts @@ -21,8 +21,8 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; const paramsSchema = z .object({ diff --git a/server/routers/private/remoteExitNode/getRemoteExitNode.ts b/server/private/routers/remoteExitNode/getRemoteExitNode.ts similarity index 97% rename from server/routers/private/remoteExitNode/getRemoteExitNode.ts rename to server/private/routers/remoteExitNode/getRemoteExitNode.ts index 19c4f263..2ef3fb06 100644 --- a/server/routers/private/remoteExitNode/getRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/getRemoteExitNode.ts @@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; const getRemoteExitNodeSchema = z .object({ @@ -52,8 +53,6 @@ async function query(remoteExitNodeId: string) { return remoteExitNode; } -export type GetRemoteExitNodeResponse = Awaited>; - export async function getRemoteExitNode( req: Request, res: Response, diff --git a/server/routers/private/remoteExitNode/getRemoteExitNodeToken.ts b/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts similarity index 96% rename from server/routers/private/remoteExitNode/getRemoteExitNodeToken.ts rename to server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts index 3905f1f7..16ec4d5d 100644 --- a/server/routers/private/remoteExitNode/getRemoteExitNodeToken.ts +++ b/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts @@ -24,7 +24,7 @@ import { fromError } from "zod-validation-error"; import { createRemoteExitNodeSession, validateRemoteExitNodeSessionToken -} from "@server/auth/sessions/privateRemoteExitNode"; +} from "#private/auth/sessions/remoteExitNode"; import { verifyPassword } from "@server/auth/password"; import logger from "@server/logger"; import config from "@server/lib/config"; @@ -35,8 +35,6 @@ export const remoteExitNodeGetTokenBodySchema = z.object({ token: z.string().optional() }); -export type RemoteExitNodeGetTokenBody = z.infer; - export async function getRemoteExitNodeToken( req: Request, res: Response, diff --git a/server/routers/private/remoteExitNode/handleRemoteExitNodePingMessage.ts b/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts similarity index 100% rename from server/routers/private/remoteExitNode/handleRemoteExitNodePingMessage.ts rename to server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts diff --git a/server/routers/private/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts b/server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts similarity index 100% rename from server/routers/private/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts rename to server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts diff --git a/server/routers/private/remoteExitNode/index.ts b/server/private/routers/remoteExitNode/index.ts similarity index 100% rename from server/routers/private/remoteExitNode/index.ts rename to server/private/routers/remoteExitNode/index.ts diff --git a/server/routers/private/remoteExitNode/listRemoteExitNodes.ts b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts similarity index 94% rename from server/routers/private/remoteExitNode/listRemoteExitNodes.ts rename to server/private/routers/remoteExitNode/listRemoteExitNodes.ts index d6d2466e..1029b1e9 100644 --- a/server/routers/private/remoteExitNode/listRemoteExitNodes.ts +++ b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts @@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; const listRemoteExitNodesParamsSchema = z .object({ @@ -43,7 +44,7 @@ const listRemoteExitNodesSchema = z.object({ .pipe(z.number().int().nonnegative()) }); -function queryRemoteExitNodes(orgId: string) { +export function queryRemoteExitNodes(orgId: string) { return db .select({ remoteExitNodeId: remoteExitNodes.remoteExitNodeId, @@ -65,11 +66,6 @@ function queryRemoteExitNodes(orgId: string) { ); } -export type ListRemoteExitNodesResponse = { - remoteExitNodes: Awaited>; - pagination: { total: number; limit: number; offset: number }; -}; - export async function listRemoteExitNodes( req: Request, res: Response, diff --git a/server/routers/private/remoteExitNode/pickRemoteExitNodeDefaults.ts b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts similarity index 94% rename from server/routers/private/remoteExitNode/pickRemoteExitNodeDefaults.ts rename to server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts index 684e616c..e5762f0d 100644 --- a/server/routers/private/remoteExitNode/pickRemoteExitNodeDefaults.ts +++ b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts @@ -19,11 +19,7 @@ import logger from "@server/logger"; import { generateId } from "@server/auth/sessions/app"; import { fromError } from "zod-validation-error"; import { z } from "zod"; - -export type PickRemoteExitNodeDefaultsResponse = { - remoteExitNodeId: string; - secret: string; -}; +import { PickRemoteExitNodeDefaultsResponse } from "@server/routers/remoteExitNode/types"; const paramsSchema = z .object({ diff --git a/server/routers/private/remoteExitNode/quickStartRemoteExitNode.ts b/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts similarity index 97% rename from server/routers/private/remoteExitNode/quickStartRemoteExitNode.ts rename to server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts index 689580b9..4d368152 100644 --- a/server/routers/private/remoteExitNode/quickStartRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts @@ -12,7 +12,7 @@ */ import { NextFunction, Request, Response } from "express"; -import { db, exitNodes, exitNodeOrgs } from "@server/db"; +import { db } from "@server/db"; import HttpCode from "@server/types/HttpCode"; import { remoteExitNodes } from "@server/db"; import createHttpError from "http-errors"; @@ -24,11 +24,7 @@ import { hashPassword } from "@server/auth/password"; import logger from "@server/logger"; import z from "zod"; import { fromError } from "zod-validation-error"; - -export type QuickStartRemoteExitNodeResponse = { - remoteExitNodeId: string; - secret: string; -}; +import { QuickStartRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; const INSTALLER_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"; diff --git a/src/lib/types/privateThemeTypes.tsx b/server/private/routers/ws/index.ts similarity index 94% rename from src/lib/types/privateThemeTypes.tsx rename to server/private/routers/ws/index.ts index de0b2d2b..4d803a3a 100644 --- a/src/lib/types/privateThemeTypes.tsx +++ b/server/private/routers/ws/index.ts @@ -11,3 +11,4 @@ * This file is not licensed under the AGPLv3. */ +export * from "./ws"; \ No newline at end of file diff --git a/server/private/routers/ws/messageHandlers.ts b/server/private/routers/ws/messageHandlers.ts new file mode 100644 index 00000000..71c2b253 --- /dev/null +++ b/server/private/routers/ws/messageHandlers.ts @@ -0,0 +1,26 @@ +/* + * 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 { + handleRemoteExitNodeRegisterMessage, + handleRemoteExitNodePingMessage, + startRemoteExitNodeOfflineChecker +} from "#private/routers/remoteExitNode"; +import { MessageHandler } from "@server/routers/ws"; + +export const messageHandlers: Record = { + "remoteExitNode/register": handleRemoteExitNodeRegisterMessage, + "remoteExitNode/ping": handleRemoteExitNodePingMessage +}; + +startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes \ No newline at end of file diff --git a/server/routers/ws/privateWs.ts b/server/private/routers/ws/ws.ts similarity index 94% rename from server/routers/ws/privateWs.ts rename to server/private/routers/ws/ws.ts index d94ccf5e..0122126f 100644 --- a/server/routers/ws/privateWs.ts +++ b/server/private/routers/ws/ws.ts @@ -14,7 +14,6 @@ import { Router, Request, Response } from "express"; import { Server as HttpServer } from "http"; import { WebSocket, WebSocketServer } from "ws"; -import { IncomingMessage } from "http"; import { Socket } from "net"; import { Newt, @@ -31,73 +30,20 @@ import { eq } from "drizzle-orm"; import { db } from "@server/db"; import { validateNewtSessionToken } from "@server/auth/sessions/newt"; import { validateOlmSessionToken } from "@server/auth/sessions/olm"; -import { messageHandlers } from "./messageHandlers"; import logger from "@server/logger"; -import redisManager from "@server/db/private/redis"; +import redisManager from "#private/lib/redis"; import { v4 as uuidv4 } from "uuid"; -import { validateRemoteExitNodeSessionToken } from "@server/auth/sessions/privateRemoteExitNode"; -import { rateLimitService } from "@server/db/private/rateLimit"; +import { validateRemoteExitNodeSessionToken } from "#private/auth/sessions/remoteExitNode"; +import { rateLimitService } from "#private/lib/rateLimit"; +import { messageHandlers } from "@server/routers/ws/messageHandlers"; +import { messageHandlers as privateMessageHandlers } from "#private/routers/ws/messageHandlers"; +import { AuthenticatedWebSocket, ClientType, WSMessage, TokenPayload, WebSocketRequest, RedisMessage } from "@server/routers/ws"; + +// Merge public and private message handlers +Object.assign(messageHandlers, privateMessageHandlers); const MAX_PENDING_MESSAGES = 50; // Maximum messages to queue during connection setup -// Custom interfaces -interface WebSocketRequest extends IncomingMessage { - token?: string; -} - -type ClientType = "newt" | "olm" | "remoteExitNode"; - -interface AuthenticatedWebSocket extends WebSocket { - client?: Newt | Olm | RemoteExitNode; - clientType?: ClientType; - connectionId?: string; - isFullyConnected?: boolean; - pendingMessages?: Buffer[]; -} - -interface TokenPayload { - client: Newt | Olm | RemoteExitNode; - session: NewtSession | OlmSession | RemoteExitNodeSession; - clientType: ClientType; -} - -interface WSMessage { - type: string; - data: any; -} - -interface HandlerResponse { - message: WSMessage; - broadcast?: boolean; - excludeSender?: boolean; - targetClientId?: string; -} - -interface HandlerContext { - message: WSMessage; - senderWs: WebSocket; - client: Newt | Olm | RemoteExitNode | undefined; - clientType: ClientType; - sendToClient: (clientId: string, message: WSMessage) => Promise; - broadcastToAllExcept: ( - message: WSMessage, - excludeClientId?: string - ) => Promise; - connectedClients: Map; -} - -interface RedisMessage { - type: "direct" | "broadcast"; - targetClientId?: string; - excludeClientId?: string; - message: WSMessage; - fromNodeId: string; -} - -export type MessageHandler = ( - context: HandlerContext -) => Promise; - // Helper function to process a single message const processMessage = async ( ws: AuthenticatedWebSocket, @@ -875,10 +821,6 @@ const cleanup = async (): Promise => { } }; -// Handle process termination -process.on("SIGTERM", cleanup); -process.on("SIGINT", cleanup); - export { router, handleWSUpgrade, diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index 9db5931a..754478fc 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -10,10 +10,7 @@ export * from "./resetPassword"; export * from "./requestPasswordReset"; export * from "./setServerAdmin"; export * from "./initialSetupComplete"; -export * from "./privateQuickStart"; export * from "./validateSetupToken"; export * from "./changePassword"; export * from "./checkResourceSession"; -export * from "./securityKey"; -export * from "./privateGetSessionTransferToken"; -export * from "./privateTransferSession"; +export * from "./securityKey"; \ No newline at end of file diff --git a/server/routers/auth/requestTotpSecret.ts b/server/routers/auth/requestTotpSecret.ts index e6ae4fe4..7c122a44 100644 --- a/server/routers/auth/requestTotpSecret.ts +++ b/server/routers/auth/requestTotpSecret.ts @@ -110,10 +110,12 @@ export async function requestTotpSecret( ); } + const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config + const hex = crypto.getRandomValues(new Uint8Array(20)); const secret = encodeHex(hex); const uri = createTOTPKeyURI( - config.getRawPrivateConfig().branding?.app_name || "Pangolin", + appName, user.email!, hex ); diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 0d4f6865..1f361b79 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -21,12 +21,12 @@ import { hashPassword } from "@server/auth/password"; import { checkValidInvite } from "@server/auth/checkValidInvite"; import { passwordSchema } from "@server/auth/passwordSchema"; import { UserType } from "@server/types/UserTypes"; -import { createUserAccountOrg } from "@server/lib/private/createUserAccountOrg"; +import { createUserAccountOrg } from "@server/lib/createUserAccountOrg"; import { build } from "@server/build"; import resend, { AudienceIds, moveEmailToAudience -} from "@server/lib/private/resend"; +} from "#dynamic/lib/resend"; export const signupBodySchema = z.object({ email: z.string().toLowerCase().email(), diff --git a/server/routers/auth/types.ts b/server/routers/auth/types.ts new file mode 100644 index 00000000..bb5a1b4e --- /dev/null +++ b/server/routers/auth/types.ts @@ -0,0 +1,8 @@ +export type TransferSessionResponse = { + valid: boolean; + cookie?: string; +}; + +export type GetSessionTransferTokenRenponse = { + token: string; +}; \ No newline at end of file diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts index 010ddf28..47a81c0a 100644 --- a/server/routers/auth/verifyEmail.ts +++ b/server/routers/auth/verifyEmail.ts @@ -10,7 +10,7 @@ import { eq } from "drizzle-orm"; import { isWithinExpirationDate } from "oslo"; import config from "@server/lib/config"; import logger from "@server/logger"; -import { freeLimitSet, limitsService } from "@server/lib/private/billing"; +import { freeLimitSet, limitsService } from "@server/lib/billing"; import { build } from "@server/build"; export const verifyEmailBody = z diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index c380e679..523163e6 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -33,9 +33,9 @@ import createHttpError from "http-errors"; import NodeCache from "node-cache"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { getCountryCodeForIp, remoteGetCountryCodeForIp } from "@server/lib/geoip"; -import { getOrgTierData } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { getCountryCodeForIp } from "@server/lib/geoip"; +import { getOrgTierData } from "#dynamic/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; import { verifyPassword } from "@server/auth/password"; // We'll see if this speeds anything up @@ -106,23 +106,23 @@ export async function verifyResourceSession( const clientIp = requestIp ? (() => { - logger.debug("Request IP:", { requestIp }); - if (requestIp.startsWith("[") && requestIp.includes("]")) { - // if brackets are found, extract the IPv6 address from between the brackets - const ipv6Match = requestIp.match(/\[(.*?)\]/); - if (ipv6Match) { - return ipv6Match[1]; - } - } + logger.debug("Request IP:", { requestIp }); + if (requestIp.startsWith("[") && requestIp.includes("]")) { + // if brackets are found, extract the IPv6 address from between the brackets + const ipv6Match = requestIp.match(/\[(.*?)\]/); + if (ipv6Match) { + return ipv6Match[1]; + } + } - // ivp4 - // split at last colon - const lastColonIndex = requestIp.lastIndexOf(":"); - if (lastColonIndex !== -1) { - return requestIp.substring(0, lastColonIndex); - } - return requestIp; - })() + // ivp4 + // split at last colon + const lastColonIndex = requestIp.lastIndexOf(":"); + if (lastColonIndex !== -1) { + return requestIp.substring(0, lastColonIndex); + } + return requestIp; + })() : undefined; logger.debug("Client IP:", { clientIp }); @@ -137,11 +137,11 @@ export async function verifyResourceSession( const resourceCacheKey = `resource:${cleanHost}`; let resourceData: | { - resource: Resource | null; - pincode: ResourcePincode | null; - password: ResourcePassword | null; - headerAuth: ResourceHeaderAuth | null; - } + resource: Resource | null; + pincode: ResourcePincode | null; + password: ResourcePassword | null; + headerAuth: ResourceHeaderAuth | null; + } | undefined = cache.get(resourceCacheKey); if (!resourceData) { @@ -194,11 +194,13 @@ export async function verifyResourceSession( // otherwise its undefined and we pass } + // IMPORTANT: ADD NEW AUTH CHECKS HERE OR WHEN TURNING OFF ALL OTHER AUTH METHODS IT WILL JUST PASS if ( - !resource.sso && + !sso && !pincode && !password && - !resource.emailWhitelistEnabled + !resource.emailWhitelistEnabled && + !headerAuth ) { logger.debug("Resource allowed because no auth"); return allowed(res); @@ -213,21 +215,21 @@ export async function verifyResourceSession( headers && headers[ config.getRawConfig().server.resource_access_token_headers.id - ] && + ] && headers[ config.getRawConfig().server.resource_access_token_headers.token - ] + ] ) { const accessTokenId = headers[ config.getRawConfig().server.resource_access_token_headers .id - ]; + ]; const accessToken = headers[ config.getRawConfig().server.resource_access_token_headers .token - ]; + ]; const { valid, error, tokenItem } = await verifyResourceAccessToken( { @@ -293,15 +295,42 @@ export async function verifyResourceSession( } // check for HTTP Basic Auth header + const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`; if (headerAuth && clientHeaderAuth) { - if(cache.get(clientHeaderAuth)) { - logger.debug("Resource allowed because header auth is valid (cached)"); + if (cache.get(clientHeaderAuthKey)) { + logger.debug( + "Resource allowed because header auth is valid (cached)" + ); return allowed(res); - }else if(await verifyPassword(clientHeaderAuth, headerAuth.headerAuthHash)){ - cache.set(clientHeaderAuth, clientHeaderAuth); + } else if ( + await verifyPassword( + clientHeaderAuth, + headerAuth.headerAuthHash + ) + ) { + cache.set(clientHeaderAuthKey, clientHeaderAuth); logger.debug("Resource allowed because header auth is valid"); return allowed(res); } + + if ( // we dont want to redirect if this is the only auth method and we did not pass here + !sso && + !pincode && + !password && + !resource.emailWhitelistEnabled + ) { + return notAllowed(res); + } + } else if (headerAuth) { + // if there are no other auth methods we need to return unauthorized if nothing is provided + if ( + !sso && + !pincode && + !password && + !resource.emailWhitelistEnabled + ) { + return notAllowed(res); + } } if (!sessions) { @@ -477,7 +506,11 @@ function extractResourceSessionToken( return latest.token; } -async function notAllowed(res: Response, redirectPath?: string, orgId?: string) { +async function notAllowed( + res: Response, + redirectPath?: string, + orgId?: string +) { let loginPage: LoginPage | null = null; if (orgId) { const { tier } = await getOrgTierData(orgId); // returns null in oss @@ -491,14 +524,11 @@ async function notAllowed(res: Response, redirectPath?: string, orgId?: string) let endpoint: string; if (loginPage && loginPage.domainId && loginPage.fullDomain) { - const secure = config.getRawConfig().app.dashboard_url?.startsWith("https"); + const secure = config + .getRawConfig() + .app.dashboard_url?.startsWith("https"); const method = secure ? "https" : "http"; endpoint = `${method}://${loginPage.fullDomain}`; - } else if (config.isManagedMode()) { - endpoint = - config.getRawConfig().managed?.redirect_endpoint || - config.getRawConfig().managed?.endpoint || - ""; } else { endpoint = config.getRawConfig().app.dashboard_url!; } @@ -530,39 +560,6 @@ function allowed(res: Response, userData?: BasicUserData) { return response(res, data); } -async function createAccessTokenSession( - res: Response, - resource: Resource, - tokenItem: ResourceAccessToken -) { - const token = generateSessionToken(); - const sess = await createResourceSession({ - resourceId: resource.resourceId, - token, - accessTokenId: tokenItem.accessTokenId, - sessionLength: tokenItem.sessionLength, - expiresAt: tokenItem.expiresAt, - doNotExtend: tokenItem.expiresAt ? true : false - }); - const cookieName = `${config.getRawConfig().server.session_cookie_name}`; - const cookie = serializeResourceSessionCookie( - cookieName, - resource.fullDomain!, - token, - !resource.ssl, - new Date(sess.expiresAt) - ); - res.appendHeader("Set-Cookie", cookie); - logger.debug("Access token is valid, creating new session"); - return response(res, { - data: { valid: true }, - success: true, - error: false, - message: "Access allowed", - status: HttpCode.OK - }); -} - async function isUserAllowedToAccessResource( userSessionId: string, resource: Resource @@ -803,11 +800,7 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise { let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey); if (!cachedCountryCode) { - if (config.isManagedMode()) { - cachedCountryCode = await remoteGetCountryCodeForIp(ip); - } else { - cachedCountryCode = await getCountryCodeForIp(ip); // do it locally - } + cachedCountryCode = await getCountryCodeForIp(ip); // do it locally // Cache for longer since IP geolocation doesn't change frequently cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes } @@ -817,7 +810,9 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise { return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase(); } -function extractBasicAuth(headers: Record | undefined): string | undefined { +function extractBasicAuth( + headers: Record | undefined +): string | undefined { if (!headers || (!headers.authorization && !headers.Authorization)) { return; } @@ -833,8 +828,9 @@ function extractBasicAuth(headers: Record | undefined): string | try { // Extract the base64 encoded credentials return authHeader.slice("Basic ".length); - } catch (error) { - logger.debug("Basic Auth: Failed to decode credentials", { error: error instanceof Error ? error.message : "Unknown error" }); + logger.debug("Basic Auth: Failed to decode credentials", { + error: error instanceof Error ? error.message : "Unknown error" + }); } } diff --git a/server/routers/billing/types.ts b/server/routers/billing/types.ts new file mode 100644 index 00000000..2ec5a1b1 --- /dev/null +++ b/server/routers/billing/types.ts @@ -0,0 +1,17 @@ +import { Limit, Subscription, SubscriptionItem, Usage } from "@server/db"; + +export type GetOrgSubscriptionResponse = { + subscription: Subscription | null; + items: SubscriptionItem[]; +}; + +export type GetOrgUsageResponse = { + usage: Usage[]; + limits: Limit[]; +}; + +export type GetOrgTierResponse = { + tier: string | null; + active: boolean; +}; + diff --git a/server/routers/billing/webhooks.ts b/server/routers/billing/webhooks.ts new file mode 100644 index 00000000..0ca38a8a --- /dev/null +++ b/server/routers/billing/webhooks.ts @@ -0,0 +1,14 @@ +import createHttpError from "http-errors"; +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; + +export async function billingWebhookHandler( + req: Request, + res: Response, + next: NextFunction +): Promise { + // return not found + return next( + createHttpError(HttpCode.NOT_FOUND, "This endpoint is not in use") + ); +} \ No newline at end of file diff --git a/server/routers/certificates/createCertificate.ts b/server/routers/certificates/createCertificate.ts new file mode 100644 index 00000000..e160e644 --- /dev/null +++ b/server/routers/certificates/createCertificate.ts @@ -0,0 +1,5 @@ +import { db, Transaction } from "@server/db"; + +export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) { + return; +} \ No newline at end of file diff --git a/server/routers/certificates/types.ts b/server/routers/certificates/types.ts new file mode 100644 index 00000000..80136de8 --- /dev/null +++ b/server/routers/certificates/types.ts @@ -0,0 +1,13 @@ +export type GetCertificateResponse = { + certId: number; + domain: string; + domainId: string; + wildcard: boolean; + status: string; // pending, requested, valid, expired, failed + expiresAt: string | null; + lastRenewalAttempt: Date | null; + createdAt: string; + updatedAt: string; + errorMessage?: string | null; + renewalCount: number; +} \ No newline at end of file diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index e7762223..cb2bbd6e 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -24,7 +24,7 @@ import { hashPassword } from "@server/auth/password"; import { isValidCIDR, isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; import { OpenAPITags, registry } from "@server/openApi"; -import { listExitNodes } from "@server/lib/exitNodes"; +import { listExitNodes } from "#dynamic/lib/exitNodes"; const createClientParamsSchema = z .object({ diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index e34a23e9..e5c46d70 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -1,4 +1,4 @@ -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; export async function addTargets( newtId: string, diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index f60f14a0..884a9864 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -17,7 +17,7 @@ import { addPeer as olmAddPeer, deletePeer as olmDeletePeer } from "../olm/peers"; -import { sendToExitNode } from "@server/lib/exitNodes"; +import { sendToExitNode } from "#dynamic/lib/exitNodes"; const updateClientParamsSchema = z .object({ @@ -29,7 +29,7 @@ const updateClientSchema = z .object({ name: z.string().min(1).max(255).optional(), siteIds: z - .array(z.string().transform(Number).pipe(z.number())) + .array(z.number().int().positive()) .optional() }) .strict(); diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 3744f044..d0e8a72b 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -9,8 +9,8 @@ import { fromError } from "zod-validation-error"; import { subdomainSchema } from "@server/lib/schemas"; import { generateId } from "@server/auth/sessions/app"; import { eq, and } from "drizzle-orm"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators"; import { build } from "@server/build"; import config from "@server/lib/config"; @@ -82,7 +82,7 @@ export async function createOrgDomain( ) ); } - } else if (build == "enterprise" || build == "saas") { + } else if (build == "saas") { if (type !== "ns" && type !== "cname") { return next( createHttpError( @@ -92,6 +92,7 @@ export async function createOrgDomain( ); } } + // allow wildacard, cname, and ns in enterprise // Validate organization exists if (!isValidDomain(baseDomain)) { @@ -104,7 +105,7 @@ export async function createOrgDomain( // many providers dont allow cname for this. Lets prevent it for the user for now return next( createHttpError( - HttpCode.BAD_REQUEST, + HttpCode.BAD_REQUEST, "You cannot create a CNAME record on a root domain. RFC 1912 § 2.4 prohibits CNAME records at the zone apex. Please use a subdomain." ) ); @@ -253,7 +254,7 @@ export async function createOrgDomain( domainId, baseDomain, type, - verified: build == "oss" ? true : false + verified: type === "wildcard" ? true : false }) .returning(); diff --git a/server/routers/domain/deleteOrgDomain.ts b/server/routers/domain/deleteOrgDomain.ts index 8932733c..8836584b 100644 --- a/server/routers/domain/deleteOrgDomain.ts +++ b/server/routers/domain/deleteOrgDomain.ts @@ -7,8 +7,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; const paramsSchema = z .object({ diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts index e833e532..c0cafafe 100644 --- a/server/routers/domain/index.ts +++ b/server/routers/domain/index.ts @@ -1,6 +1,4 @@ export * from "./listDomains"; export * from "./createOrgDomain"; export * from "./deleteOrgDomain"; -export * from "./privateListDomainNamespaces"; -export * from "./privateCheckDomainNamespaceAvailability"; export * from "./restartOrgDomain"; \ No newline at end of file diff --git a/server/routers/domain/types.ts b/server/routers/domain/types.ts new file mode 100644 index 00000000..4ae48fb1 --- /dev/null +++ b/server/routers/domain/types.ts @@ -0,0 +1,8 @@ +export type CheckDomainAvailabilityResponse = { + available: boolean; + options: { + domainNamespaceId: string; + domainId: string; + fullDomain: string; + }[]; +}; \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 3a96bb03..8bd72f62 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -13,7 +13,6 @@ import * as siteResource from "./siteResource"; import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; -import * as license from "./license"; import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { @@ -38,25 +37,13 @@ import { verifyUserIsOrgOwner, verifySiteResourceAccess } from "@server/middlewares"; -import { - verifyCertificateAccess, - verifyRemoteExitNodeAccess, - verifyIdpAccess, - verifyLoginPageAccess -} from "@server/middlewares/private"; -import { createStore } from "@server/lib/private/rateLimitStore"; import { ActionsEnum } from "@server/auth/actions"; import { createNewt, getNewtToken } from "./newt"; import { getOlmToken } from "./olm"; import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import createHttpError from "http-errors"; -import * as certificates from "./private/certificates"; -import * as billing from "@server/routers/private/billing"; -import { quickStart } from "./auth/privateQuickStart"; import { build } from "@server/build"; -import * as remoteExitNode from "@server/routers/private/remoteExitNode"; -import * as loginPage from "@server/routers/private/loginPage"; -import * as orgIdp from "@server/routers/private/orgIdp"; +import { createStore } from "#dynamic/lib/rateLimitStore"; // Root routes export const unauthenticated = Router(); @@ -65,45 +52,6 @@ unauthenticated.get("/", (_, res) => { res.status(HttpCode.OK).json({ message: "Healthy" }); }); -if (build === "saas") { - unauthenticated.post( - "/quick-start", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 100, - keyGenerator: (req) => req.path, - handler: (req, res, next) => { - const message = `We're too busy right now. Please try again later.`; - return next( - createHttpError(HttpCode.TOO_MANY_REQUESTS, message) - ); - }, - store: createStore() - }), - quickStart - ); -} - -if (build !== "oss") { - unauthenticated.post( - "/remote-exit-node/quick-start", - rateLimit({ - windowMs: 60 * 60 * 1000, - max: 5, - keyGenerator: (req) => - `${req.path}:${ipKeyGenerator(req.ip || "")}`, - handler: (req, res, next) => { - const message = `You can only create 5 remote exit nodes every hour. Please try again later.`; - return next( - createHttpError(HttpCode.TOO_MANY_REQUESTS, message) - ); - }, - store: createStore() - }), - remoteExitNode.quickStartRemoteExitNode - ); -} - // Authenticated Root routes export const authenticated = Router(); authenticated.use(verifySessionUserMiddleware); @@ -727,45 +675,7 @@ authenticated.post( idp.updateOidcIdp ); -if (build !== "oss") { - authenticated.put( - "/org/:orgId/idp/oidc", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createIdp), - orgIdp.createOrgOidcIdp - ); - authenticated.post( - "/org/:orgId/idp/:idpId/oidc", - verifyOrgAccess, - verifyIdpAccess, - verifyUserHasAction(ActionsEnum.updateIdp), - orgIdp.updateOrgOidcIdp - ); - - authenticated.delete( - "/org/:orgId/idp/:idpId", - verifyOrgAccess, - verifyIdpAccess, - verifyUserHasAction(ActionsEnum.deleteIdp), - idp.deleteIdp - ); - - authenticated.get( - "/org/:orgId/idp/:idpId", - verifyOrgAccess, - verifyIdpAccess, - verifyUserHasAction(ActionsEnum.getIdp), - orgIdp.getOrgIdp - ); - - authenticated.get( - "/org/:orgId/idp", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.listIdps), - orgIdp.listOrgIdps - ); -} authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); @@ -795,36 +705,10 @@ authenticated.get( idp.listIdpOrgPolicies ); -if (build !== "oss") { - authenticated.get("/org/:orgId/idp", orgIdp.listOrgIdps); // anyone can see this; it's just a list of idp names and ids -} + authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); -authenticated.post( - "/license/activate", - verifyUserIsServerAdmin, - license.activateLicense -); - -authenticated.get( - "/license/keys", - verifyUserIsServerAdmin, - license.listLicenseKeys -); - -authenticated.delete( - "/license/:licenseKey", - verifyUserIsServerAdmin, - license.deleteLicenseKey -); - -authenticated.post( - "/license/recheck", - verifyUserIsServerAdmin, - license.recheckStatus -); - authenticated.get( `/api-key/:apiKeyId`, verifyUserIsServerAdmin, @@ -930,126 +814,6 @@ authenticated.delete( domain.deleteAccountDomain ); -if (build !== "oss") { - authenticated.get( - "/org/:orgId/certificate/:domainId/:domain", - verifyOrgAccess, - verifyCertificateAccess, - verifyUserHasAction(ActionsEnum.getCertificate), - certificates.getCertificate - ); - - authenticated.post( - "/org/:orgId/certificate/:certId/restart", - verifyOrgAccess, - verifyCertificateAccess, - verifyUserHasAction(ActionsEnum.restartCertificate), - certificates.restartCertificate - ); - - authenticated.post( - "/org/:orgId/billing/create-checkout-session", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.billing), - billing.createCheckoutSession - ); - - authenticated.post( - "/org/:orgId/billing/create-portal-session", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.billing), - billing.createPortalSession - ); - - authenticated.get( - "/org/:orgId/billing/subscription", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.billing), - billing.getOrgSubscription - ); - - authenticated.get( - "/org/:orgId/billing/usage", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.billing), - billing.getOrgUsage - ); - - authenticated.get("/domain/namespaces", domain.listDomainNamespaces); - - authenticated.get( - "/domain/check-namespace-availability", - domain.checkDomainNamespaceAvailability - ); - - authenticated.put( - "/org/:orgId/remote-exit-node", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createRemoteExitNode), - remoteExitNode.createRemoteExitNode - ); - - authenticated.get( - "/org/:orgId/remote-exit-nodes", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.listRemoteExitNode), - remoteExitNode.listRemoteExitNodes - ); - - authenticated.get( - "/org/:orgId/remote-exit-node/:remoteExitNodeId", - verifyOrgAccess, - verifyRemoteExitNodeAccess, - verifyUserHasAction(ActionsEnum.getRemoteExitNode), - remoteExitNode.getRemoteExitNode - ); - - authenticated.get( - "/org/:orgId/pick-remote-exit-node-defaults", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createRemoteExitNode), - remoteExitNode.pickRemoteExitNodeDefaults - ); - - authenticated.delete( - "/org/:orgId/remote-exit-node/:remoteExitNodeId", - verifyOrgAccess, - verifyRemoteExitNodeAccess, - verifyUserHasAction(ActionsEnum.deleteRemoteExitNode), - remoteExitNode.deleteRemoteExitNode - ); - - authenticated.put( - "/org/:orgId/login-page", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createLoginPage), - loginPage.createLoginPage - ); - - authenticated.post( - "/org/:orgId/login-page/:loginPageId", - verifyOrgAccess, - verifyLoginPageAccess, - verifyUserHasAction(ActionsEnum.updateLoginPage), - loginPage.updateLoginPage - ); - - authenticated.delete( - "/org/:orgId/login-page/:loginPageId", - verifyOrgAccess, - verifyLoginPageAccess, - verifyUserHasAction(ActionsEnum.deleteLoginPage), - loginPage.deleteLoginPage - ); - - authenticated.get( - "/org/:orgId/login-page", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.getLoginPage), - loginPage.getLoginPage - ); -} - // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); @@ -1129,26 +893,6 @@ authRouter.post( getOlmToken ); -if (build !== "oss") { - authRouter.post( - "/remoteExitNode/get-token", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 900, - keyGenerator: (req) => - `remoteExitNodeGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`, - handler: (req, res, next) => { - const message = `You can only request an remoteExitNodeToken token ${900} times every ${15} minutes. Please try again later.`; - return next( - createHttpError(HttpCode.TOO_MANY_REQUESTS, message) - ); - }, - store: createStore() - }), - remoteExitNode.getRemoteExitNodeToken - ); -} - authRouter.post( "/2fa/enable", rateLimit({ @@ -1316,26 +1060,6 @@ authRouter.post( resource.authWithWhitelist ); -if (build !== "oss") { - authRouter.post( - "/transfer-session-token", - rateLimit({ - windowMs: 1 * 60 * 1000, - max: 60, - keyGenerator: (req) => - `transferSessionToken:${ipKeyGenerator(req.ip || "")}`, - handler: (req, res, next) => { - const message = `You can only transfer a session token ${5} times every ${1} minute. Please try again later.`; - return next( - createHttpError(HttpCode.TOO_MANY_REQUESTS, message) - ); - }, - store: createStore() - }), - auth.transferSession - ); -} - authRouter.post( "/resource/:resourceId/access-token", resource.authWithAccessToken diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts new file mode 100644 index 00000000..4c5efed7 --- /dev/null +++ b/server/routers/generatedLicense/types.ts @@ -0,0 +1,30 @@ +export type GeneratedLicenseKey = { + instanceName: string | null; + licenseKey: string; + expiresAt: string; + isValid: boolean; + createdAt: string; + tier: string; + type: string; +}; + +export type ListGeneratedLicenseKeysResponse = GeneratedLicenseKey[]; + +export type NewLicenseKey = { + licenseKey: { + id: number; + instanceName: string | null; + instanceId: string; + licenseKey: string; + tier: string; + type: string; + quantity: number; + isValid: boolean; + updatedAt: string; + createdAt: string; + expiresAt: string; + orgId: string; + }; +}; + +export type GenerateNewLicenseResponse = NewLicenseKey; \ No newline at end of file diff --git a/server/routers/gerbil/createExitNode.ts b/server/routers/gerbil/createExitNode.ts index d4e6d43a..5903cdd4 100644 --- a/server/routers/gerbil/createExitNode.ts +++ b/server/routers/gerbil/createExitNode.ts @@ -30,6 +30,7 @@ export async function createExitNode(publicKey: string, reachableAt: string | un publicKey, endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`, address, + online: true, listenPort, reachableAt, name: exitNodeName diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index afae4009..bb581ced 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -1,19 +1,15 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { sites, resources, targets, exitNodes, ExitNode } from "@server/db"; +import { sites, exitNodes, ExitNode } from "@server/db"; import { db } from "@server/db"; import { eq, isNotNull, and } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import config from "@server/lib/config"; -import { getUniqueExitNodeEndpointName } from "../../db/names"; -import { findNextAvailableCidr } from "@server/lib/ip"; import { fromError } from "zod-validation-error"; import { getAllowedIps } from "../target/helpers"; -import { proxyToRemote } from "@server/lib/remoteProxy"; -import { getNextAvailableSubnet } from "@server/lib/exitNodes"; -import { createExitNode } from "./privateCreateExitNode"; +import { createExitNode } from "#dynamic/routers/gerbil/createExitNode"; // Define Zod schema for request validation const getConfigSchema = z.object({ @@ -66,16 +62,6 @@ export async function getConfig( ); } - // STOP HERE IN HYBRID MODE - if (config.isManagedMode()) { - req.body = { - ...req.body, - endpoint: exitNode.endpoint, - listenPort: exitNode.listenPort - }; - return proxyToRemote(req, res, next, "hybrid/gerbil/get-config"); - } - const configResponse = await generateGerbilConfig(exitNode); logger.debug("Sending config: ", configResponse); diff --git a/server/routers/gerbil/getResolvedHostname.ts b/server/routers/gerbil/getResolvedHostname.ts index 17067c55..f06cd1b8 100644 --- a/server/routers/gerbil/getResolvedHostname.ts +++ b/server/routers/gerbil/getResolvedHostname.ts @@ -4,9 +4,9 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { resolveExitNodes } from "@server/lib/exitNodes"; -import config from "@server/lib/config"; +import { resolveExitNodes } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; +import config from "@server/lib/config"; // Define Zod schema for request validation const getResolvedHostnameSchema = z.object({ @@ -36,7 +36,15 @@ export async function getResolvedHostname( const { hostname, publicKey } = parsedParams.data; - const baseDomain = config.getRawPrivateConfig().app.base_domain; + const dashboardUrl = config.getRawConfig().app.dashboard_url; + + // extract the domain removing the http and stuff + const baseDomain = dashboardUrl + ? dashboardUrl + .replace("http://", "") + .replace("https://", "") + .split("/")[0] + : null; // if the hostname ends with the base domain then send back a empty array if (baseDomain && hostname.endsWith(baseDomain)) { @@ -50,6 +58,13 @@ export async function getResolvedHostname( publicKey ); + if (resourceExitNodes.length === 0) { + // no exit nodes found, return empty array to force local routing + return res.status(HttpCode.OK).send({ + endpoints: [] // this should force to route locally + }); + } + endpoints = resourceExitNodes.map((node) => node.endpoint); } diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts index 1cdc9184..44af7fbd 100644 --- a/server/routers/gerbil/peers.ts +++ b/server/routers/gerbil/peers.ts @@ -2,7 +2,7 @@ import logger from "@server/logger"; import { db } from "@server/db"; import { exitNodes } from "@server/db"; import { eq } from "drizzle-orm"; -import { sendToExitNode } from "@server/lib/exitNodes"; +import { sendToExitNode } from "#dynamic/lib/exitNodes"; export async function addPeer( exitNodeId: number, diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index d10141b9..3661dedd 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -6,9 +6,9 @@ import logger from "@server/logger"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing/features"; -import { checkExitNodeOrg } from "@server/lib/exitNodes"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing/features"; +import { checkExitNodeOrg } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; // Track sites that are already offline to avoid unnecessary queries diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 65217178..34bc2c6b 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -18,8 +18,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { validateNewtSessionToken } from "@server/auth/sessions/newt"; import { validateOlmSessionToken } from "@server/auth/sessions/olm"; -import axios from "axios"; -import { checkExitNodeOrg } from "@server/lib/exitNodes"; +import { checkExitNodeOrg } from "#dynamic/lib/exitNodes"; // Define Zod schema for request validation const updateHolePunchSchema = z.object({ diff --git a/server/routers/hybrid.ts b/server/routers/hybrid.ts new file mode 100644 index 00000000..235961f1 --- /dev/null +++ b/server/routers/hybrid.ts @@ -0,0 +1,4 @@ +import { Router } from "express"; + +// Root routes +export const hybridRouter = Router(); \ No newline at end of file diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 223a08b8..67357d76 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -11,7 +11,6 @@ import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -import license from "@server/license/license"; const paramsSchema = z.object({}).strict(); diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 90144816..3c81ce0b 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -14,8 +14,8 @@ import jsonwebtoken from "jsonwebtoken"; import config from "@server/lib/config"; import { decrypt } from "@server/lib/crypto"; import { build } from "@server/build"; -import { getOrgTierData } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { getOrgTierData } from "#dynamic/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; const paramsSchema = z .object({ diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 904d0d9e..53ece68e 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -11,7 +11,6 @@ import { idp, idpOidcConfig } from "@server/db"; import { eq } from "drizzle-orm"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; -import license from "@server/license/license"; const paramsSchema = z .object({ diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index fec21e41..98bdfe44 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, Org } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -30,8 +30,9 @@ import { } from "@server/auth/sessions/app"; import { decrypt } from "@server/lib/crypto"; import { UserType } from "@server/types/UserTypes"; -import { FeatureId } from "@server/lib/private/billing"; -import { usageService } from "@server/lib/private/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { build } from "@server/build"; const ensureTrailingSlash = (url: string): string => { return url; @@ -255,7 +256,18 @@ export async function validateOidcCallback( ); if (existingIdp.idp.autoProvision) { - const allOrgs = await db.select().from(orgs); + let allOrgs: Org[] = []; + + if (build === "saas") { + const idpOrgs = await db + .select() + .from(idpOrg) + .where(eq(idpOrg.idpId, existingIdp.idp.idpId)) + .innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId)); + allOrgs = idpOrgs.map((o) => o.orgs); + } else { + allOrgs = await db.select().from(orgs); + } const defaultRoleMapping = existingIdp.idp.defaultRoleMapping; const defaultOrgMapping = existingIdp.idp.defaultOrgMapping; @@ -292,6 +304,8 @@ export async function validateOidcCallback( } } + // user could be allowed in this org, now find the role + const roleMapping = idpOrgRes?.roleMapping || defaultRoleMapping; if (roleMapping) { @@ -336,6 +350,24 @@ export async function validateOidcCallback( let existingUserId = existingUser?.userId; + if (!userOrgInfo.length) { + if (existingUser) { + // delete the user + // cascade will also delete org users + + await db + .delete(users) + .where(eq(users.userId, existingUser.userId)); + } + + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + `No policies matched for ${userIdentifier}. This user must be added to an organization before logging in.` + ) + ); + } + const orgUserCounts: { orgId: string; userCount: number }[] = []; // sync the user with the orgs and roles diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 879075c7..d0c7c5d5 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -555,13 +555,6 @@ authenticated.post( idp.updateOidcIdp ); -authenticated.delete( - "/idp/:idpId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteIdp), - idp.deleteIdp -); - authenticated.get( "/idp", verifyApiKeyIsRoot, @@ -604,15 +597,6 @@ authenticated.get( idp.listIdpOrgPolicies ); -if (build == "saas") { - authenticated.post( - `/org/:orgId/send-usage-notification`, - verifyApiKeyIsRoot, // We are the only ones who can use root key so its fine - verifyApiKeyHasAction(ActionsEnum.sendUsageNotification), - org.sendUsageNotification - ); -} - authenticated.get( "/org/:orgId/pick-client-defaults", verifyClientsEnabled, diff --git a/server/routers/internal.ts b/server/routers/internal.ts index e4525118..2fa5239c 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -5,22 +5,15 @@ import * as resource from "./resource"; import * as badger from "./badger"; import * as auth from "@server/routers/auth"; import * as supporterKey from "@server/routers/supporterKey"; -import * as license from "@server/routers/license"; import * as idp from "@server/routers/idp"; -import * as loginPage from "@server/routers/private/loginPage"; -import { proxyToRemote } from "@server/lib/remoteProxy"; -import config from "@server/lib/config"; import HttpCode from "@server/types/HttpCode"; import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares"; -import { build } from "@server/build"; -import * as billing from "@server/routers/private/billing"; -import * as orgIdp from "@server/routers/private/orgIdp"; // Root routes -const internalRouter = Router(); +export const internalRouter = Router(); internalRouter.get("/", (_, res) => { res.status(HttpCode.OK).json({ message: "Healthy" }); @@ -45,50 +38,19 @@ internalRouter.get( supporterKey.isSupporterKeyVisible ); -internalRouter.get(`/license/status`, license.getLicenseStatus); - internalRouter.get("/idp", idp.listIdps); internalRouter.get("/idp/:idpId", idp.getIdp); -if (build !== "oss") { - internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps); - - internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier); -} - // Gerbil routes const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); -if (config.isManagedMode()) { - // Use proxy router to forward requests to remote cloud server - // Proxy endpoints for each gerbil route - gerbilRouter.post("/receive-bandwidth", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth") - ); - - gerbilRouter.post("/update-hole-punch", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/gerbil/update-hole-punch") - ); - - gerbilRouter.post("/get-all-relays", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays") - ); - - gerbilRouter.post("/get-resolved-hostname", (req, res, next) => - proxyToRemote(req, res, next, `hybrid/gerbil/get-resolved-hostname`) - ); - - // GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER - // SO IT CAN REGISTER THE LOCAL EXIT NODE -} else { - // Use local gerbil endpoints - gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); - gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); - gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); - gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname); -} +// Use local gerbil endpoints +gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); +gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); +gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); +gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname); // WE HANDLE THE PROXY INSIDE OF THIS FUNCTION // SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL @@ -100,22 +62,4 @@ internalRouter.use("/badger", badgerRouter); badgerRouter.post("/verify-session", badger.verifyResourceSession); -if (config.isManagedMode()) { - badgerRouter.post("/exchange-session", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/badger/exchange-session") - ); -} else { - badgerRouter.post("/exchange-session", badger.exchangeSession); -} - -if (build !== "oss") { - internalRouter.get("/login-page", loginPage.loadLoginPage); - - internalRouter.post( - "/get-session-transfer-token", - verifySessionUserMiddleware, - auth.getSessionTransferToken - ); -} - -export default internalRouter; +badgerRouter.post("/exchange-session", badger.exchangeSession); diff --git a/server/routers/license/types.ts b/server/routers/license/types.ts new file mode 100644 index 00000000..945bd368 --- /dev/null +++ b/server/routers/license/types.ts @@ -0,0 +1,11 @@ +import { LicenseStatus, LicenseKeyCache } from "@server/license/license"; + +export type ActivateLicenseStatus = LicenseStatus; + +export type DeleteLicenseKeyResponse = LicenseStatus; + +export type GetLicenseStatusResponse = LicenseStatus; + +export type ListLicenseKeysResponse = LicenseKeyCache[]; + +export type RecheckStatusResponse = LicenseStatus; \ No newline at end of file diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts new file mode 100644 index 00000000..26f59cab --- /dev/null +++ b/server/routers/loginPage/types.ts @@ -0,0 +1,11 @@ +import { LoginPage } from "@server/db"; + +export type CreateLoginPageResponse = LoginPage; + +export type DeleteLoginPageResponse = LoginPage; + +export type GetLoginPageResponse = LoginPage; + +export type UpdateLoginPageResponse = LoginPage; + +export type LoadLoginPageResponse = LoginPage & { orgId: string }; \ No newline at end of file diff --git a/server/routers/newt/dockerSocket.ts b/server/routers/newt/dockerSocket.ts index 0c59d354..3847d9a4 100644 --- a/server/routers/newt/dockerSocket.ts +++ b/server/routers/newt/dockerSocket.ts @@ -1,5 +1,5 @@ import NodeCache from "node-cache"; -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; export const dockerSocketCache = new NodeCache({ stdTTL: 3600 // seconds diff --git a/server/routers/newt/handleApplyBlueprintMessage.ts b/server/routers/newt/handleApplyBlueprintMessage.ts index 68158799..62802fff 100644 --- a/server/routers/newt/handleApplyBlueprintMessage.ts +++ b/server/routers/newt/handleApplyBlueprintMessage.ts @@ -1,5 +1,5 @@ import { db, newts } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db"; import { eq, and, sql, inArray } from "drizzle-orm"; import logger from "@server/logger"; diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 2b65fd06..3eba94b9 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { @@ -14,7 +14,7 @@ import { import { clients, clientSites, Newt, sites } from "@server/db"; import { eq, and, inArray } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; -import { sendToExitNode } from "@server/lib/exitNodes"; +import { sendToExitNode } from "#dynamic/lib/exitNodes"; const inputSchema = z.object({ publicKey: z.string(), diff --git a/server/routers/newt/handleNewtPingRequestMessage.ts b/server/routers/newt/handleNewtPingRequestMessage.ts index aeb7a155..fea157fd 100644 --- a/server/routers/newt/handleNewtPingRequestMessage.ts +++ b/server/routers/newt/handleNewtPingRequestMessage.ts @@ -1,10 +1,9 @@ import { db, sites } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { exitNodes, Newt } from "@server/db"; import logger from "@server/logger"; -import config from "@server/lib/config"; import { ne, eq, or, and, count } from "drizzle-orm"; -import { listExitNodes } from "@server/lib/exitNodes"; +import { listExitNodes } from "#dynamic/lib/exitNodes"; export const handleNewtPingRequestMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 021527ad..372f3677 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -1,5 +1,5 @@ import { db, exitNodeOrgs, newts } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db"; import { targetHealthCheck } from "@server/db"; import { eq, and, sql, inArray } from "drizzle-orm"; @@ -10,12 +10,12 @@ import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; import { selectBestExitNode, verifyExitNodeOrgAccess -} from "@server/lib/exitNodes"; +} from "#dynamic/lib/exitNodes"; import { fetchContainers } from "./dockerSocket"; export type ExitNodePingResult = { diff --git a/server/routers/newt/handleReceiveBandwidthMessage.ts b/server/routers/newt/handleReceiveBandwidthMessage.ts index 89b24f78..f5170feb 100644 --- a/server/routers/newt/handleReceiveBandwidthMessage.ts +++ b/server/routers/newt/handleReceiveBandwidthMessage.ts @@ -1,5 +1,5 @@ import { db } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, Newt } from "@server/db"; import { eq } from "drizzle-orm"; import logger from "@server/logger"; diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts index aceca37d..0491393f 100644 --- a/server/routers/newt/handleSocketMessages.ts +++ b/server/routers/newt/handleSocketMessages.ts @@ -1,4 +1,4 @@ -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import logger from "@server/logger"; import { dockerSocketCache } from "./dockerSocket"; import { Newt } from "@server/db"; diff --git a/server/routers/newt/peers.ts b/server/routers/newt/peers.ts index ff57e6fd..03dc3460 100644 --- a/server/routers/newt/peers.ts +++ b/server/routers/newt/peers.ts @@ -1,7 +1,7 @@ import { db } from "@server/db"; import { newts, sites } from "@server/db"; import { eq } from "drizzle-orm"; -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; export async function addPeer( diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index a886b00b..97e4030d 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -1,5 +1,5 @@ import { Target, TargetHealthCheck, db, targetHealthCheck } from "@server/db"; -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { eq, inArray } from "drizzle-orm"; diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 6c4b5600..6f00640d 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -1,5 +1,5 @@ import { db } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, Olm } from "@server/db"; import { eq, lt, isNull, and, or } from "drizzle-orm"; import logger from "@server/logger"; diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 33d7f9cb..66128f0e 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,10 +1,10 @@ import { db, ExitNode } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, clientSites, exitNodes, Olm, olms, sites } from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; -import { listExitNodes } from "@server/lib/exitNodes"; +import { listExitNodes } from "#dynamic/lib/exitNodes"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.info("Handling register olm message!"); diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index cefc5b91..9b31754c 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -1,5 +1,5 @@ import { db, exitNodes, sites } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, clientSites, Olm } from "@server/db"; import { and, eq } from "drizzle-orm"; import { updatePeer } from "../newt/peers"; diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index a8d6be9c..396866a1 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -1,7 +1,7 @@ import { db } from "@server/db"; import { clients, olms, newts, sites } from "@server/db"; import { eq } from "drizzle-orm"; -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; export async function addPeer( diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index a12520a4..d8bcb9da 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -3,8 +3,6 @@ import { z } from "zod"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import { - apiKeyOrg, - apiKeys, domains, Org, orgDomains, @@ -24,9 +22,9 @@ import { fromError } from "zod-validation-error"; import { defaultRoleAllowedActions } from "../role"; import { OpenAPITags, registry } from "@server/openApi"; import { isValidCIDR } from "@server/lib/validators"; -import { createCustomer } from "@server/routers/private/billing/createCustomer"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { createCustomer } from "#dynamic/lib/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; const createOrgSchema = z diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index 63e9abb0..8a424e5b 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -9,7 +9,7 @@ import createHttpError from "http-errors"; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; import { deletePeer } from "../gerbil/peers"; import { OpenAPITags, registry } from "@server/openApi"; diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index e467374f..89c77f13 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -75,10 +75,6 @@ export async function getOrg( } } - logger.info( - `returning data: ${JSON.stringify({ ...org[0], settings: parsedSettings })}` - ); - return response(res, { data: { org: { diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index 013f6c6d..7887fcac 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -7,5 +7,4 @@ export * from "./checkId"; export * from "./getOrgOverview"; export * from "./listOrgs"; export * from "./pickOrgDefaults"; -export * from "./privateSendUsageNotifications"; export * from "./applyBlueprint"; diff --git a/server/routers/orgIdp/types.ts b/server/routers/orgIdp/types.ts new file mode 100644 index 00000000..a8e205cc --- /dev/null +++ b/server/routers/orgIdp/types.ts @@ -0,0 +1,27 @@ +import { Idp, IdpOidcConfig } from "@server/db"; + +export type CreateOrgIdpResponse = { + idpId: number; + redirectUrl: string; +}; + +export type GetOrgIdpResponse = { + idp: Idp, + idpOidcConfig: IdpOidcConfig | null, + redirectUrl: string +} + +export type ListOrgIdpsResponse = { + idps: { + idpId: number; + orgId: string; + name: string; + type: string; + variant: string; + }[], + pagination: { + total: number; + limit: number; + offset: number; + }; +}; diff --git a/server/routers/remoteExitNode/types.ts b/server/routers/remoteExitNode/types.ts new file mode 100644 index 00000000..55d0a286 --- /dev/null +++ b/server/routers/remoteExitNode/types.ts @@ -0,0 +1,34 @@ +import { RemoteExitNode } from "@server/db"; + +export type CreateRemoteExitNodeResponse = { + token: string; + remoteExitNodeId: string; + secret: string; +}; + +export type PickRemoteExitNodeDefaultsResponse = { + remoteExitNodeId: string; + secret: string; +}; + +export type QuickStartRemoteExitNodeResponse = { + remoteExitNodeId: string; + secret: string; +}; + +export type ListRemoteExitNodesResponse = { + remoteExitNodes: { + remoteExitNodeId: string; + dateCreated: string; + version: string | null; + exitNodeId: number | null; + name: string; + address: string; + endpoint: string; + online: boolean; + type: string | null; + }[]; + pagination: { total: number; limit: number; offset: number }; +}; + +export type GetRemoteExitNodeResponse = { remoteExitNodeId: string; dateCreated: string; version: string | null; exitNodeId: number | null; name: string; address: string; endpoint: string; online: boolean; type: string | null; } \ No newline at end of file diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 53af0b72..2a4e67a7 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -21,7 +21,7 @@ import { subdomainSchema } from "@server/lib/schemas"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; import { build } from "@server/build"; -import { createCertificate } from "../private/certificates/createCertificate"; +import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { getUniqueResourceName } from "@server/db/names"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; @@ -37,7 +37,8 @@ const createHttpResourceSchema = z subdomain: z.string().nullable().optional(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - domainId: z.string() + domainId: z.string(), + stickySession: z.boolean().optional(), }) .strict() .refine( @@ -191,6 +192,7 @@ async function createHttpResource( const { name, domainId } = parsedBody.data; const subdomain = parsedBody.data.subdomain; + const stickySession=parsedBody.data.stickySession; // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( @@ -254,7 +256,8 @@ async function createHttpResource( subdomain: finalSubdomain, http: true, protocol: "tcp", - ssl: true + ssl: true, + stickySession: stickySession }) .returning(); diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index b7775251..834da7b3 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -103,18 +103,18 @@ export async function getResourceAuthInfo( .limit(1); const resource = result?.resources; - const pincode = result?.resourcePincode; - const password = result?.resourcePassword; - const headerAuth = result?.resourceHeaderAuth; - - const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; - if (!resource) { return next( createHttpError(HttpCode.NOT_FOUND, "Resource not found") ); } + const pincode = result?.resourcePincode; + const password = result?.resourcePassword; + const headerAuth = result?.resourceHeaderAuth; + + const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; + return response(res, { data: { niceId: resource.niceId, diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 27605be6..22a10605 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, resourceHeaderAuth } from "@server/db"; import { resources, userResources, @@ -56,7 +56,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { proxyPort: resources.proxyPort, enabled: resources.enabled, domainId: resources.domainId, - niceId: resources.niceId + niceId: resources.niceId, + headerAuthId: resourceHeaderAuth.headerAuthId }) .from(resources) .leftJoin( @@ -67,6 +68,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { resourcePincode, eq(resourcePincode.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuth, + eq(resourceHeaderAuth.resourceId, resources.resourceId) + ) .where( and( inArray(resources.resourceId, accessibleResourceIds), diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 83fcf6f1..a9c3b5de 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -20,7 +20,7 @@ import { tlsNameSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas"; import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; -import { createCertificate } from "../private/certificates/createCertificate"; +import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateHeaders } from "@server/lib/validators"; import { build } from "@server/build"; diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 5ffa6954..0ffc5956 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -16,8 +16,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import { hashPassword } from "@server/auth/password"; import { isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; -import config from "@server/lib/config"; -import { verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; +import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes"; const createSiteParamsSchema = z .object({ diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 4af2feae..7a12e24a 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -9,7 +9,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { deletePeer } from "../gerbil/peers"; import { fromError } from "zod-validation-error"; -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; import { OpenAPITags, registry } from "@server/openApi"; const deleteSiteSchema = z diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 46d3c53b..c4b3a087 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -15,7 +15,7 @@ import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; import { fromError } from "zod-validation-error"; import { z } from "zod"; -import { listExitNodes } from "@server/lib/exitNodes"; +import { listExitNodes } from "#dynamic/lib/exitNodes"; export type PickSiteDefaultsResponse = { exitNodeId: number; diff --git a/server/routers/site/socketIntegration.ts b/server/routers/site/socketIntegration.ts index 34084a0a..7b5160cb 100644 --- a/server/routers/site/socketIntegration.ts +++ b/server/routers/site/socketIntegration.ts @@ -9,7 +9,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import stoi from "@server/lib/stoi"; -import { sendToClient } from "../ws"; +import { sendToClient } from "#dynamic/routers/ws"; import { fetchContainers, dockerSocketCache, diff --git a/server/routers/supporterKey/isSupporterKeyVisible.ts b/server/routers/supporterKey/isSupporterKeyVisible.ts index 0e958889..da995447 100644 --- a/server/routers/supporterKey/isSupporterKeyVisible.ts +++ b/server/routers/supporterKey/isSupporterKeyVisible.ts @@ -7,7 +7,6 @@ import config from "@server/lib/config"; import { db } from "@server/db"; import { count } from "drizzle-orm"; import { users } from "@server/db"; -import license from "@server/license/license"; import { build } from "@server/build"; export type IsSupporterKeyVisibleResponse = { @@ -28,12 +27,6 @@ export async function isSupporterKeyVisible( let visible = !hidden && key?.valid !== true; - const licenseStatus = await license.check(); - - if (licenseStatus.isLicenseValid) { - visible = false; - } - if (key?.tier === "Limited Supporter") { const [numUsers] = await db.select({ count: count() }).from(users); @@ -45,7 +38,7 @@ export async function isSupporterKeyVisible( } } - if (config.getRawPrivateConfig().flags?.hide_supporter_key && build != "oss") { + if (build !== "oss") { visible = false; } diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts index d726b5af..ee4e7950 100644 --- a/server/routers/target/handleHealthcheckStatusMessage.ts +++ b/server/routers/target/handleHealthcheckStatusMessage.ts @@ -1,5 +1,5 @@ import { db, targets, resources, sites, targetHealthCheck } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { Newt } from "@server/db"; import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; diff --git a/server/routers/traefik/traefikConfigProvider.ts b/server/routers/traefik/traefikConfigProvider.ts index 50ec0770..6c9404e9 100644 --- a/server/routers/traefik/traefikConfigProvider.ts +++ b/server/routers/traefik/traefikConfigProvider.ts @@ -3,7 +3,7 @@ import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import config from "@server/lib/config"; import { build } from "@server/build"; -import { getTraefikConfig } from "@server/lib/traefik"; +import { getTraefikConfig } from "#dynamic/lib/traefik"; import { getCurrentExitNodeId } from "@server/lib/exitNodes"; const badgerMiddlewareName = "badger"; diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts index b553bd19..5e4264f9 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -10,8 +10,8 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { checkValidInvite } from "@server/auth/checkValidInvite"; import { verifySession } from "@server/auth/sessions/verifySession"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; const acceptInviteBodySchema = z .object({ diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index b8f681c3..29f94641 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -10,11 +10,11 @@ import { db, UserOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db"; import { generateId } from "@server/auth/sessions/app"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; -import { getOrgTierData } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { getOrgTierData } from "#dynamic/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; const paramsSchema = z .object({ diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 746a383b..56098bea 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -17,8 +17,8 @@ import { sendEmail } from "@server/emails"; import SendInviteLink from "@server/emails/templates/SendInviteLink"; import { OpenAPITags, registry } from "@server/openApi"; import { UserType } from "@server/types/UserTypes"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; const regenerateTracker = new NodeCache({ stdTTL: 3600, checkperiod: 600 }); diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index 6d0c5359..babccdd0 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -9,8 +9,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { usageService } from "@server/lib/private/billing/usageService"; -import { FeatureId } from "@server/lib/private/billing"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; import { UserType } from "@server/types/UserTypes"; diff --git a/server/routers/ws/index.ts b/server/routers/ws/index.ts index 376a960b..16440ec9 100644 --- a/server/routers/ws/index.ts +++ b/server/routers/ws/index.ts @@ -1,24 +1,2 @@ -import { build } from "@server/build"; - -// Import both modules -import * as wsModule from "./ws"; -import * as privateWsModule from "./privateWs"; - -// Conditionally export WebSocket implementation based on build type -const wsImplementation = build === "oss" ? wsModule : privateWsModule; - -// Re-export all items from the selected implementation -export const { - router, - handleWSUpgrade, - sendToClient, - broadcastToAllExcept, - connectedClients, - hasActiveConnections, - getActiveNodes, - NODE_ID, - cleanup -} = wsImplementation; - -// Re-export the MessageHandler type (both modules have the same type signature) -export type { MessageHandler } from "./privateWs"; \ No newline at end of file +export * from "./ws"; +export * from "./types"; \ No newline at end of file diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index 5b111eec..cbb023b3 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -13,10 +13,8 @@ import { handleOlmPingMessage, startOlmOfflineChecker } from "../olm"; -import { handleRemoteExitNodeRegisterMessage, handleRemoteExitNodePingMessage, startRemoteExitNodeOfflineChecker } from "@server/routers/private/remoteExitNode"; -import { MessageHandler } from "./privateWs"; import { handleHealthcheckStatusMessage } from "../target"; -import { build } from "@server/build"; +import { MessageHandler } from "./types"; export const messageHandlers: Record = { "newt/wg/register": handleNewtRegisterMessage, @@ -30,12 +28,6 @@ export const messageHandlers: Record = { "newt/ping/request": handleNewtPingRequestMessage, "newt/blueprint/apply": handleApplyBlueprintMessage, "newt/healthcheck/status": handleHealthcheckStatusMessage, - - "remoteExitNode/register": handleRemoteExitNodeRegisterMessage, - "remoteExitNode/ping": handleRemoteExitNodePingMessage, }; -startOlmOfflineChecker(); // this is to handle the offline check for olms -if (build != "oss") { - startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes -} \ No newline at end of file +startOlmOfflineChecker(); // this is to handle the offline check for olms \ No newline at end of file diff --git a/server/routers/ws/types.ts b/server/routers/ws/types.ts new file mode 100644 index 00000000..7063bc87 --- /dev/null +++ b/server/routers/ws/types.ts @@ -0,0 +1,70 @@ +import { + Newt, + newts, + NewtSession, + olms, + Olm, + OlmSession, + RemoteExitNode, + RemoteExitNodeSession, + remoteExitNodes +} from "@server/db"; +import { IncomingMessage } from "http"; +import { WebSocket } from "ws"; + +// Custom interfaces +export interface WebSocketRequest extends IncomingMessage { + token?: string; +} + +export type ClientType = "newt" | "olm" | "remoteExitNode"; + +export interface AuthenticatedWebSocket extends WebSocket { + client?: Newt | Olm | RemoteExitNode; + clientType?: ClientType; + connectionId?: string; + isFullyConnected?: boolean; + pendingMessages?: Buffer[]; +} + +export interface TokenPayload { + client: Newt | Olm | RemoteExitNode; + session: NewtSession | OlmSession | RemoteExitNodeSession; + clientType: ClientType; +} + +export interface WSMessage { + type: string; + data: any; +} + +export interface HandlerResponse { + message: WSMessage; + broadcast?: boolean; + excludeSender?: boolean; + targetClientId?: string; +} + +export interface HandlerContext { + message: WSMessage; + senderWs: WebSocket; + client: Newt | Olm | RemoteExitNode | undefined; + clientType: ClientType; + sendToClient: (clientId: string, message: WSMessage) => Promise; + broadcastToAllExcept: ( + message: WSMessage, + excludeClientId?: string + ) => Promise; + connectedClients: Map; +} + +export type MessageHandler = (context: HandlerContext) => Promise; + +// Redis message type for cross-node communication +export interface RedisMessage { + type: "direct" | "broadcast"; + targetClientId?: string; + excludeClientId?: string; + message: WSMessage; + fromNodeId: string; +} \ No newline at end of file diff --git a/server/routers/ws/ws.ts b/server/routers/ws/ws.ts index 8fb773d3..9bba41dc 100644 --- a/server/routers/ws/ws.ts +++ b/server/routers/ws/ws.ts @@ -1,7 +1,6 @@ import { Router, Request, Response } from "express"; import { Server as HttpServer } from "http"; import { WebSocket, WebSocketServer } from "ws"; -import { IncomingMessage } from "http"; import { Socket } from "net"; import { Newt, newts, NewtSession, olms, Olm, OlmSession } from "@server/db"; import { eq } from "drizzle-orm"; @@ -11,50 +10,15 @@ import { validateOlmSessionToken } from "@server/auth/sessions/olm"; import { messageHandlers } from "./messageHandlers"; import logger from "@server/logger"; import { v4 as uuidv4 } from "uuid"; +import { ClientType, TokenPayload, WebSocketRequest, WSMessage, AuthenticatedWebSocket } from "./types"; -// Custom interfaces -interface WebSocketRequest extends IncomingMessage { - token?: string; -} - -type ClientType = 'newt' | 'olm'; - -interface AuthenticatedWebSocket extends WebSocket { - client?: Newt | Olm; - clientType?: ClientType; - connectionId?: string; -} - -interface TokenPayload { +// Subset of TokenPayload for public ws.ts (newt and olm only) +interface PublicTokenPayload { client: Newt | Olm; session: NewtSession | OlmSession; - clientType: ClientType; + clientType: "newt" | "olm"; } -interface WSMessage { - type: string; - data: any; -} - -interface HandlerResponse { - message: WSMessage; - broadcast?: boolean; - excludeSender?: boolean; - targetClientId?: string; -} - -interface HandlerContext { - message: WSMessage; - senderWs: WebSocket; - client: Newt | Olm | undefined; - clientType: ClientType; - sendToClient: (clientId: string, message: WSMessage) => Promise; - broadcastToAllExcept: (message: WSMessage, excludeClientId?: string) => Promise; - connectedClients: Map; -} - -export type MessageHandler = (context: HandlerContext) => Promise; - const router: Router = Router(); const wss: WebSocketServer = new WebSocketServer({ noServer: true }); @@ -153,7 +117,7 @@ const getActiveNodes = async (clientType: ClientType, clientId: string): Promise }; // Token verification middleware -const verifyToken = async (token: string, clientType: ClientType): Promise => { +const verifyToken = async (token: string, clientType: ClientType): Promise => { try { if (clientType === 'newt') { @@ -169,7 +133,7 @@ try { return null; } return { client: existingNewt[0], session, clientType }; - } else { + } else if (clientType === 'olm') { const { session, olm } = await validateOlmSessionToken(token); if (!session || !olm) { return null; @@ -183,13 +147,15 @@ try { } return { client: existingOlm[0], session, clientType }; } + + return null; } catch (error) { logger.error("Token verification failed:", error); return null; } }; -const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, clientType: ClientType): Promise => { +const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, clientType: "newt" | "olm"): Promise => { logger.info("Establishing websocket connection"); if (!client) { logger.error("Connection attempt without client"); @@ -323,10 +289,6 @@ const cleanup = async (): Promise => { } }; -// Handle process termination -process.on('SIGTERM', cleanup); -process.on('SIGINT', cleanup); - export { router, handleWSUpgrade, diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 078c99ee..1734b5e6 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -3,7 +3,6 @@ import { eq } from "drizzle-orm"; import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; import moment from "moment"; import logger from "@server/logger"; -import config from "@server/lib/config"; const random: RandomReader = { read(bytes: Uint8Array): void { @@ -23,11 +22,6 @@ function generateId(length: number): string { } export async function ensureSetupToken() { - if (config.isManagedMode()) { - // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID - return; - } - try { // Check if a server admin already exists const [existingAdmin] = await db diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 5c748c89..de3785f3 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -11,7 +11,7 @@ import m3 from "./scriptsPg/1.8.0"; import m4 from "./scriptsPg/1.9.0"; import m5 from "./scriptsPg/1.10.0"; import m6 from "./scriptsPg/1.10.2"; -import m7 from "./scriptsPg/1.10.4"; +import m7 from "./scriptsPg/1.11.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -24,7 +24,7 @@ const migrations = [ { version: "1.9.0", run: m4 }, { version: "1.10.0", run: m5 }, { version: "1.10.2", run: m6 }, - { version: "1.10.4", run: m7 }, + { version: "1.11.0", run: m7 }, // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 80d2139d..b987b833 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -29,7 +29,7 @@ import m24 from "./scriptsSqlite/1.9.0"; import m25 from "./scriptsSqlite/1.10.0"; import m26 from "./scriptsSqlite/1.10.1"; import m27 from "./scriptsSqlite/1.10.2"; -import m28 from "./scriptsSqlite/1.10.4"; +import m28 from "./scriptsSqlite/1.11.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -58,7 +58,7 @@ const migrations = [ { version: "1.10.0", run: m25 }, { version: "1.10.1", run: m26 }, { version: "1.10.2", run: m27 }, - { version: "1.10.4", run: m28 }, + { version: "1.11.0", run: m28 }, // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.10.4.ts b/server/setup/scriptsPg/1.10.4.ts deleted file mode 100644 index da862412..00000000 --- a/server/setup/scriptsPg/1.10.4.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { sql } from "drizzle-orm"; -import { isoBase64URL } from "@simplewebauthn/server/helpers"; -import { randomUUID } from "crypto"; - -const version = "1.10.4"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - try { - await db.execute(sql`BEGIN`); - - const webauthnCredentialsQuery = await db.execute( - sql`SELECT "credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated" FROM "webauthnCredentials"` - ); - - const webauthnCredentials = webauthnCredentialsQuery.rows as { - credentialId: string; - publicKey: string; - userId: string; - signCount: number; - transports: string | null; - name: string | null; - lastUsed: string; - dateCreated: string; - }[]; - - for (const webauthnCredential of webauthnCredentials) { - const newCredentialId = isoBase64URL.fromBuffer( - new Uint8Array( - Buffer.from(webauthnCredential.credentialId, "base64") - ) - ); - const newPublicKey = isoBase64URL.fromBuffer( - new Uint8Array( - Buffer.from(webauthnCredential.publicKey, "base64") - ) - ); - - // Delete the old record - await db.execute(sql` - DELETE FROM "webauthnCredentials" - WHERE "credentialId" = ${webauthnCredential.credentialId} - `); - - // Insert the updated record with converted values - await db.execute(sql` - INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated") - VALUES (${newCredentialId}, ${newPublicKey}, ${webauthnCredential.userId}, ${webauthnCredential.signCount}, ${webauthnCredential.transports}, ${webauthnCredential.name}, ${webauthnCredential.lastUsed}, ${webauthnCredential.dateCreated}) - `); - } - - // 1. Add the column with placeholder so NOT NULL is satisfied - await db.execute(sql` - ALTER TABLE "resources" - ADD COLUMN IF NOT EXISTS "resourceGuid" varchar(36) NOT NULL DEFAULT 'PLACEHOLDER' - `); - - // 2. Fetch every row to backfill UUIDs - const rows = await db.execute( - sql`SELECT "resourceId" FROM "resources" WHERE "resourceGuid" = 'PLACEHOLDER'` - ); - const resources = rows.rows as { resourceId: number }[]; - - for (const r of resources) { - await db.execute(sql` - UPDATE "resources" - SET "resourceGuid" = ${randomUUID()} - WHERE "resourceId" = ${r.resourceId} - `); - } - - // 3. Add UNIQUE constraint now that values are filled - await db.execute(sql` - ALTER TABLE "resources" - ADD CONSTRAINT "resources_resourceGuid_unique" UNIQUE("resourceGuid") - `); - - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN IF NOT EXISTS "settings" text`); - - await db.execute(sql`COMMIT`); - console.log(`Updated credentialId and publicKey`); - } catch (e) { - await db.execute(sql`ROLLBACK`); - console.log("Unable to update credentialId and publicKey"); - console.log(e); - throw e; - } - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsPg/1.11.0.ts b/server/setup/scriptsPg/1.11.0.ts new file mode 100644 index 00000000..13186b4f --- /dev/null +++ b/server/setup/scriptsPg/1.11.0.ts @@ -0,0 +1,392 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; +import { isoBase64URL } from "@simplewebauthn/server/helpers"; +import { randomUUID } from "crypto"; + +const version = "1.11.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql`BEGIN`); + + await db.execute(sql` + CREATE TABLE "account" ( + "accountId" serial PRIMARY KEY NOT NULL, + "userId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "accountDomains" ( + "accountId" integer NOT NULL, + "domainId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "certificates" ( + "certId" serial PRIMARY KEY NOT NULL, + "domain" varchar(255) NOT NULL, + "domainId" varchar, + "wildcard" boolean DEFAULT false, + "status" varchar(50) DEFAULT 'pending' NOT NULL, + "expiresAt" bigint, + "lastRenewalAttempt" bigint, + "createdAt" bigint NOT NULL, + "updatedAt" bigint NOT NULL, + "orderId" varchar(500), + "errorMessage" text, + "renewalCount" integer DEFAULT 0, + "certFile" text, + "keyFile" text, + CONSTRAINT "certificates_domain_unique" UNIQUE("domain") + ); + `); + + await db.execute(sql` + CREATE TABLE "customers" ( + "customerId" varchar(255) PRIMARY KEY NOT NULL, + "orgId" varchar(255) NOT NULL, + "email" varchar(255), + "name" varchar(255), + "phone" varchar(50), + "address" text, + "createdAt" bigint NOT NULL, + "updatedAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "dnsChallenges" ( + "dnsChallengeId" serial PRIMARY KEY NOT NULL, + "domain" varchar(255) NOT NULL, + "token" varchar(255) NOT NULL, + "keyAuthorization" varchar(1000) NOT NULL, + "createdAt" bigint NOT NULL, + "expiresAt" bigint NOT NULL, + "completed" boolean DEFAULT false + ); + `); + + await db.execute(sql` + CREATE TABLE "domainNamespaces" ( + "domainNamespaceId" varchar(255) PRIMARY KEY NOT NULL, + "domainId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "exitNodeOrgs" ( + "exitNodeId" integer NOT NULL, + "orgId" text NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "limits" ( + "limitId" varchar(255) PRIMARY KEY NOT NULL, + "featureId" varchar(255) NOT NULL, + "orgId" varchar NOT NULL, + "value" real, + "description" text + ); + `); + + await db.execute(sql` + CREATE TABLE "loginPage" ( + "loginPageId" serial PRIMARY KEY NOT NULL, + "subdomain" varchar, + "fullDomain" varchar, + "exitNodeId" integer, + "domainId" varchar + ); + `); + + await db.execute(sql` + CREATE TABLE "loginPageOrg" ( + "loginPageId" integer NOT NULL, + "orgId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "remoteExitNodeSession" ( + "id" varchar PRIMARY KEY NOT NULL, + "remoteExitNodeId" varchar NOT NULL, + "expiresAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "remoteExitNode" ( + "id" varchar PRIMARY KEY NOT NULL, + "secretHash" varchar NOT NULL, + "dateCreated" varchar NOT NULL, + "version" varchar, + "exitNodeId" integer + ); + `); + + await db.execute(sql` + CREATE TABLE "sessionTransferToken" ( + "token" varchar PRIMARY KEY NOT NULL, + "sessionId" varchar NOT NULL, + "encryptedSession" text NOT NULL, + "expiresAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "subscriptionItems" ( + "subscriptionItemId" serial PRIMARY KEY NOT NULL, + "subscriptionId" varchar(255) NOT NULL, + "planId" varchar(255) NOT NULL, + "priceId" varchar(255), + "meterId" varchar(255), + "unitAmount" real, + "tiers" text, + "interval" varchar(50), + "currentPeriodStart" bigint, + "currentPeriodEnd" bigint, + "name" varchar(255) + ); + `); + + await db.execute(sql` + CREATE TABLE "subscriptions" ( + "subscriptionId" varchar(255) PRIMARY KEY NOT NULL, + "customerId" varchar(255) NOT NULL, + "status" varchar(50) DEFAULT 'active' NOT NULL, + "canceledAt" bigint, + "createdAt" bigint NOT NULL, + "updatedAt" bigint, + "billingCycleAnchor" bigint + ); + `); + + await db.execute(sql` + CREATE TABLE "usage" ( + "usageId" varchar(255) PRIMARY KEY NOT NULL, + "featureId" varchar(255) NOT NULL, + "orgId" varchar NOT NULL, + "meterId" varchar(255), + "instantaneousValue" real, + "latestValue" real NOT NULL, + "previousValue" real, + "updatedAt" bigint NOT NULL, + "rolledOverAt" bigint, + "nextRolloverAt" bigint + ); + `); + + await db.execute(sql` + CREATE TABLE "usageNotifications" ( + "notificationId" serial PRIMARY KEY NOT NULL, + "orgId" varchar NOT NULL, + "featureId" varchar(255) NOT NULL, + "limitId" varchar(255) NOT NULL, + "notificationType" varchar(50) NOT NULL, + "sentAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "resourceHeaderAuth" ( + "headerAuthId" serial PRIMARY KEY NOT NULL, + "resourceId" integer NOT NULL, + "headerAuthHash" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "targetHealthCheck" ( + "targetHealthCheckId" serial PRIMARY KEY NOT NULL, + "targetId" integer NOT NULL, + "hcEnabled" boolean DEFAULT false NOT NULL, + "hcPath" varchar, + "hcScheme" varchar, + "hcMode" varchar DEFAULT 'http', + "hcHostname" varchar, + "hcPort" integer, + "hcInterval" integer DEFAULT 30, + "hcUnhealthyInterval" integer DEFAULT 30, + "hcTimeout" integer DEFAULT 5, + "hcHeaders" varchar, + "hcFollowRedirects" boolean DEFAULT true, + "hcMethod" varchar DEFAULT 'GET', + "hcStatus" integer, + "hcHealth" text DEFAULT 'unknown' + ); + `); + + await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "settings" text;`); + await db.execute( + sql`ALTER TABLE "targets" ADD COLUMN "rewritePath" text;` + ); + await db.execute( + sql`ALTER TABLE "targets" ADD COLUMN "rewritePathType" text;` + ); + await db.execute( + sql`ALTER TABLE "targets" ADD COLUMN "priority" integer DEFAULT 100 NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "accountDomains" ADD CONSTRAINT "accountDomains_accountId_account_accountId_fk" FOREIGN KEY ("accountId") REFERENCES "public"."account"("accountId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "accountDomains" ADD CONSTRAINT "accountDomains_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "certificates" ADD CONSTRAINT "certificates_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "customers" ADD CONSTRAINT "customers_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "domainNamespaces" ADD CONSTRAINT "domainNamespaces_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE set null ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "exitNodeOrgs" ADD CONSTRAINT "exitNodeOrgs_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "exitNodeOrgs" ADD CONSTRAINT "exitNodeOrgs_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "limits" ADD CONSTRAINT "limits_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPage" ADD CONSTRAINT "loginPage_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE set null ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPage" ADD CONSTRAINT "loginPage_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE set null ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPageOrg" ADD CONSTRAINT "loginPageOrg_loginPageId_loginPage_loginPageId_fk" FOREIGN KEY ("loginPageId") REFERENCES "public"."loginPage"("loginPageId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPageOrg" ADD CONSTRAINT "loginPageOrg_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "remoteExitNodeSession" ADD CONSTRAINT "remoteExitNodeSession_remoteExitNodeId_remoteExitNode_id_fk" FOREIGN KEY ("remoteExitNodeId") REFERENCES "public"."remoteExitNode"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "remoteExitNode" ADD CONSTRAINT "remoteExitNode_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "sessionTransferToken" ADD CONSTRAINT "sessionTransferToken_sessionId_session_id_fk" FOREIGN KEY ("sessionId") REFERENCES "public"."session"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "subscriptionItems" ADD CONSTRAINT "subscriptionItems_subscriptionId_subscriptions_subscriptionId_fk" FOREIGN KEY ("subscriptionId") REFERENCES "public"."subscriptions"("subscriptionId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_customerId_customers_customerId_fk" FOREIGN KEY ("customerId") REFERENCES "public"."customers"("customerId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "usage" ADD CONSTRAINT "usage_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "usageNotifications" ADD CONSTRAINT "usageNotifications_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "resourceHeaderAuth" ADD CONSTRAINT "resourceHeaderAuth_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "targetHealthCheck" ADD CONSTRAINT "targetHealthCheck_targetId_targets_targetId_fk" FOREIGN KEY ("targetId") REFERENCES "public"."targets"("targetId") ON DELETE cascade ON UPDATE no action;` + ); + + const webauthnCredentialsQuery = await db.execute( + sql`SELECT "credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated" FROM "webauthnCredentials"` + ); + + const webauthnCredentials = webauthnCredentialsQuery.rows as { + credentialId: string; + publicKey: string; + userId: string; + signCount: number; + transports: string | null; + name: string | null; + lastUsed: string; + dateCreated: string; + }[]; + + for (const webauthnCredential of webauthnCredentials) { + const newCredentialId = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.credentialId, "base64") + ) + ); + const newPublicKey = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.publicKey, "base64") + ) + ); + + // Delete the old record + await db.execute(sql` + DELETE FROM "webauthnCredentials" + WHERE "credentialId" = ${webauthnCredential.credentialId} + `); + + // Insert the updated record with converted values + await db.execute(sql` + INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated") + VALUES (${newCredentialId}, ${newPublicKey}, ${webauthnCredential.userId}, ${webauthnCredential.signCount}, ${webauthnCredential.transports}, ${webauthnCredential.name}, ${webauthnCredential.lastUsed}, ${webauthnCredential.dateCreated}) + `); + } + + // 1. Add the column with placeholder so NOT NULL is satisfied + await db.execute(sql` + ALTER TABLE "resources" + ADD COLUMN IF NOT EXISTS "resourceGuid" varchar(36) NOT NULL DEFAULT 'PLACEHOLDER' + `); + + // 2. Fetch every row to backfill UUIDs + const rows = await db.execute( + sql`SELECT "resourceId" FROM "resources" WHERE "resourceGuid" = 'PLACEHOLDER'` + ); + const resources = rows.rows as { resourceId: number }[]; + + for (const r of resources) { + await db.execute(sql` + UPDATE "resources" + SET "resourceGuid" = ${randomUUID()} + WHERE "resourceId" = ${r.resourceId} + `); + } + + // get all of the targets + const targetsQuery = await db.execute( + sql`SELECT "targetId" FROM "targets"` + ); + const targets = targetsQuery.rows as { + targetId: number; + }[]; + + for (const target of targets) { + await db.execute(sql` + INSERT INTO "targetHealthCheck" ("targetId") + VALUES (${target.targetId}) + `); + } + + // 3. Add UNIQUE constraint now that values are filled + await db.execute(sql` + ALTER TABLE "resources" + ADD CONSTRAINT "resources_resourceGuid_unique" UNIQUE("resourceGuid") + `); + + await db.execute(sql`COMMIT`); + console.log(`Updated credentialId and publicKey`); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Unable to update credentialId and publicKey"); + console.log(e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsSqlite/1.10.4.ts b/server/setup/scriptsSqlite/1.10.4.ts deleted file mode 100644 index 94f355a8..00000000 --- a/server/setup/scriptsSqlite/1.10.4.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import path from "path"; -import { isoBase64URL } from "@simplewebauthn/server/helpers"; -import { randomUUID } from "crypto"; - -const version = "1.10.4"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - db.transaction(() => { - const webauthnCredentials = db - .prepare( - `SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'` - ) - .all() as { - credentialId: string; - publicKey: string; - userId: string; - signCount: number; - transports: string | null; - name: string | null; - lastUsed: string; - dateCreated: string; - }[]; - - for (const webauthnCredential of webauthnCredentials) { - const newCredentialId = isoBase64URL.fromBuffer( - new Uint8Array( - Buffer.from(webauthnCredential.credentialId, "base64") - ) - ); - const newPublicKey = isoBase64URL.fromBuffer( - new Uint8Array( - Buffer.from(webauthnCredential.publicKey, "base64") - ) - ); - - // Delete the old record - db.prepare( - `DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?` - ).run(webauthnCredential.credentialId); - - // Insert the updated record with converted values - db.prepare( - `INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` - ).run( - newCredentialId, - newPublicKey, - webauthnCredential.userId, - webauthnCredential.signCount, - webauthnCredential.transports, - webauthnCredential.name, - webauthnCredential.lastUsed, - webauthnCredential.dateCreated - ); - } - - // 1. Add the column (nullable or with placeholder) if it doesn’t exist yet - db.prepare( - `ALTER TABLE resources ADD COLUMN resourceGuid TEXT DEFAULT 'PLACEHOLDER';` - ).run(); - - // 2. Select all rows - const rows = db.prepare(`SELECT resourceId FROM resources`).all() as { - resourceId: number; - }[]; - - // 3. Prefill with random UUIDs - const updateStmt = db.prepare( - `UPDATE resources SET resourceGuid = ? WHERE resourceId = ?` - ); - - for (const row of rows) { - updateStmt.run(randomUUID(), row.resourceId); - } - - db.prepare( - `CREATE UNIQUE INDEX resources_resourceGuid_unique ON resources ('resourceGuid');` - ).run(); - - db.prepare(`ALTER TABLE "orgs" ADD COLUMN IF NOT EXISTS "settings" text`).run(); - })(); - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsSqlite/1.11.0.ts b/server/setup/scriptsSqlite/1.11.0.ts new file mode 100644 index 00000000..1247eee9 --- /dev/null +++ b/server/setup/scriptsSqlite/1.11.0.ts @@ -0,0 +1,342 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; +import { isoBase64URL } from "@simplewebauthn/server/helpers"; +import { randomUUID } from "crypto"; + +const version = "1.11.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + db.transaction(() => { + + db.prepare(` + CREATE TABLE 'account' ( + 'accountId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'userId' text NOT NULL, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'accountDomains' ( + 'accountId' integer NOT NULL, + 'domainId' text NOT NULL, + FOREIGN KEY ('accountId') REFERENCES 'account'('accountId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'certificates' ( + 'certId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'domain' text NOT NULL, + 'domainId' text, + 'wildcard' integer DEFAULT false, + 'status' text DEFAULT 'pending' NOT NULL, + 'expiresAt' integer, + 'lastRenewalAttempt' integer, + 'createdAt' integer NOT NULL, + 'updatedAt' integer NOT NULL, + 'orderId' text, + 'errorMessage' text, + 'renewalCount' integer DEFAULT 0, + 'certFile' text, + 'keyFile' text, + FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(`CREATE UNIQUE INDEX 'certificates_domain_unique' ON 'certificates' ('domain');`).run(); + + db.prepare(` + CREATE TABLE 'customers' ( + 'customerId' text PRIMARY KEY NOT NULL, + 'orgId' text NOT NULL, + 'email' text, + 'name' text, + 'phone' text, + 'address' text, + 'createdAt' integer NOT NULL, + 'updatedAt' integer NOT NULL, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'dnsChallenges' ( + 'dnsChallengeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'domain' text NOT NULL, + 'token' text NOT NULL, + 'keyAuthorization' text NOT NULL, + 'createdAt' integer NOT NULL, + 'expiresAt' integer NOT NULL, + 'completed' integer DEFAULT false + ); + `).run(); + + db.prepare(` + CREATE TABLE 'domainNamespaces' ( + 'domainNamespaceId' text PRIMARY KEY NOT NULL, + 'domainId' text NOT NULL, + FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null + ); + `).run(); + + db.prepare(` + CREATE TABLE 'exitNodeOrgs' ( + 'exitNodeId' integer NOT NULL, + 'orgId' text NOT NULL, + FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'loginPage' ( + 'loginPageId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'subdomain' text, + 'fullDomain' text, + 'exitNodeId' integer, + 'domainId' text, + FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null, + FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null + ); + `).run(); + + db.prepare(` + CREATE TABLE 'loginPageOrg' ( + 'loginPageId' integer NOT NULL, + 'orgId' text NOT NULL, + FOREIGN KEY ('loginPageId') REFERENCES 'loginPage'('loginPageId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'remoteExitNodeSession' ( + 'id' text PRIMARY KEY NOT NULL, + 'remoteExitNodeId' text NOT NULL, + 'expiresAt' integer NOT NULL, + FOREIGN KEY ('remoteExitNodeId') REFERENCES 'remoteExitNode'('id') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'remoteExitNode' ( + 'id' text PRIMARY KEY NOT NULL, + 'secretHash' text NOT NULL, + 'dateCreated' text NOT NULL, + 'version' text, + 'exitNodeId' integer, + FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'sessionTransferToken' ( + 'token' text PRIMARY KEY NOT NULL, + 'sessionId' text NOT NULL, + 'encryptedSession' text NOT NULL, + 'expiresAt' integer NOT NULL, + FOREIGN KEY ('sessionId') REFERENCES 'session'('id') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'subscriptionItems' ( + 'subscriptionItemId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'subscriptionId' text NOT NULL, + 'planId' text NOT NULL, + 'priceId' text, + 'meterId' text, + 'unitAmount' real, + 'tiers' text, + 'interval' text, + 'currentPeriodStart' integer, + 'currentPeriodEnd' integer, + 'name' text, + FOREIGN KEY ('subscriptionId') REFERENCES 'subscriptions'('subscriptionId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'subscriptions' ( + 'subscriptionId' text PRIMARY KEY NOT NULL, + 'customerId' text NOT NULL, + 'status' text DEFAULT 'active' NOT NULL, + 'canceledAt' integer, + 'createdAt' integer NOT NULL, + 'updatedAt' integer, + 'billingCycleAnchor' integer, + FOREIGN KEY ('customerId') REFERENCES 'customers'('customerId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'usage' ( + 'usageId' text PRIMARY KEY NOT NULL, + 'featureId' text NOT NULL, + 'orgId' text NOT NULL, + 'meterId' text, + 'instantaneousValue' real, + 'latestValue' real NOT NULL, + 'previousValue' real, + 'updatedAt' integer NOT NULL, + 'rolledOverAt' integer, + 'nextRolloverAt' integer, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'usageNotifications' ( + 'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'orgId' text NOT NULL, + 'featureId' text NOT NULL, + 'limitId' text NOT NULL, + 'notificationType' text NOT NULL, + 'sentAt' integer NOT NULL, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'resourceHeaderAuth' ( + 'headerAuthId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'resourceId' integer NOT NULL, + 'headerAuthHash' text NOT NULL, + FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'targetHealthCheck' ( + 'targetHealthCheckId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'targetId' integer NOT NULL, + 'hcEnabled' integer DEFAULT false NOT NULL, + 'hcPath' text, + 'hcScheme' text, + 'hcMode' text DEFAULT 'http', + 'hcHostname' text, + 'hcPort' integer, + 'hcInterval' integer DEFAULT 30, + 'hcUnhealthyInterval' integer DEFAULT 30, + 'hcTimeout' integer DEFAULT 5, + 'hcHeaders' text, + 'hcFollowRedirects' integer DEFAULT true, + 'hcMethod' text DEFAULT 'GET', + 'hcStatus' integer, + 'hcHealth' text DEFAULT 'unknown', + FOREIGN KEY ('targetId') REFERENCES 'targets'('targetId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(`DROP TABLE 'limits';`).run(); + + db.prepare(` + CREATE TABLE 'limits' ( + 'limitId' text PRIMARY KEY NOT NULL, + 'featureId' text NOT NULL, + 'orgId' text NOT NULL, + 'value' real, + 'description' text, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(`ALTER TABLE 'orgs' ADD 'settings' text;`).run(); + db.prepare(`ALTER TABLE 'targets' ADD 'rewritePath' text;`).run(); + db.prepare(`ALTER TABLE 'targets' ADD 'rewritePathType' text;`).run(); + db.prepare(`ALTER TABLE 'targets' ADD 'priority' integer DEFAULT 100 NOT NULL;`).run(); + + const webauthnCredentials = db + .prepare( + `SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'` + ) + .all() as { + credentialId: string; + publicKey: string; + userId: string; + signCount: number; + transports: string | null; + name: string | null; + lastUsed: string; + dateCreated: string; + }[]; + + for (const webauthnCredential of webauthnCredentials) { + const newCredentialId = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.credentialId, "base64") + ) + ); + const newPublicKey = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.publicKey, "base64") + ) + ); + + // Delete the old record + db.prepare( + `DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?` + ).run(webauthnCredential.credentialId); + + // Insert the updated record with converted values + db.prepare( + `INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + newCredentialId, + newPublicKey, + webauthnCredential.userId, + webauthnCredential.signCount, + webauthnCredential.transports, + webauthnCredential.name, + webauthnCredential.lastUsed, + webauthnCredential.dateCreated + ); + } + + // 1. Add the column (nullable or with placeholder) if it doesn’t exist yet + db.prepare( + `ALTER TABLE resources ADD COLUMN resourceGuid TEXT DEFAULT 'PLACEHOLDER';` + ).run(); + + // 2. Select all rows + const resources = db.prepare(`SELECT resourceId FROM resources`).all() as { + resourceId: number; + }[]; + + // 3. Prefill with random UUIDs + const updateStmt = db.prepare( + `UPDATE resources SET resourceGuid = ? WHERE resourceId = ?` + ); + + for (const row of resources) { + updateStmt.run(randomUUID(), row.resourceId); + } + + // get all of the targets + const targets = db.prepare(`SELECT targetId FROM targets`).all() as { + targetId: number; + }[]; + + const insertTargetHealthCheckStmt = db.prepare( + `INSERT INTO targetHealthCheck (targetId) VALUES (?)` + ); + + for (const target of targets) { + insertTargetHealthCheckStmt.run(target.targetId); + } + + db.prepare( + `CREATE UNIQUE INDEX resources_resourceGuid_unique ON resources ('resourceGuid');` + ).run(); + })(); + + console.log(`${version} migration complete`); +} diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index ecc7de01..0f67fc83 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -1,16 +1,14 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; -import ProfileIcon from "@app/components/ProfileIcon"; import { verifySession } from "@app/lib/auth/verifySession"; -import UserProvider from "@app/providers/UserProvider"; 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 SetLastOrgCookie from "@app/components/SetLastOrgCookie"; -import PrivateSubscriptionStatusProvider from "@app/providers/PrivateSubscriptionStatusProvider"; -import { GetOrgSubscriptionResponse } from "@server/routers/private/billing/getOrgSubscription"; +import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider"; +import { GetOrgSubscriptionResponse } from "@server/routers/billing/types"; import { pullEnv } from "@app/lib/pullEnv"; import { build } from "@server/build"; @@ -56,7 +54,7 @@ export default async function OrgLayout(props: { } let subscriptionStatus = null; - if (build != "oss") { + if (build === "saas") { try { const getSubscription = cache(() => internal.get>( @@ -73,13 +71,13 @@ export default async function OrgLayout(props: { } return ( - {props.children} - + ); } diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 4c3ac07b..25b3de1f 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -1,7 +1,6 @@ import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; import { cache } from "react"; -import OrganizationLandingCard from "../../components/OrganizationLandingCard"; import MemberResourcesPortal from "../../components/MemberResourcesPortal"; import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview"; import { internal } from "@app/lib/api"; diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index 8559ddf4..538c7fde 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -1,16 +1,3 @@ -/* - * 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 { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; @@ -73,13 +60,6 @@ export default async function BillingSettingsPage({ const t = await getTranslations(); - const navItems = [ - { - title: t('billing'), - href: `/{orgId}/settings/billing`, - }, - ]; - return ( <> diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index 1270c30f..a2841fb4 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { Button } from "@app/components/ui/button"; @@ -50,7 +37,7 @@ import { InfoPopup } from "@/components/ui/info-popup"; import { GetOrgSubscriptionResponse, GetOrgUsageResponse -} from "@server/routers/private/billing"; +} from "@server/routers/billing/types"; import { useTranslations } from "use-intl"; import Link from "next/link"; diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx index 59f7aa85..1d0a682f 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -144,7 +131,7 @@ export default function GeneralPage() { }; const form = useForm({ - resolver: zodResolver(getFormSchema()) as any, // is this right? + resolver: zodResolver(getFormSchema()) as any, // is this right? defaultValues: { name: "", clientId: "", @@ -236,17 +223,11 @@ export default function GeneralPage() { let tenantId = ""; if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) { // Azure URL format: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize - console.log( - "Azure authUrl:", - data.idpOidcConfig.authUrl - ); const tenantMatch = data.idpOidcConfig.authUrl.match( /login\.microsoftonline\.com\/([^\/]+)\/oauth2/ ); - console.log("Tenant match:", tenantMatch); if (tenantMatch) { tenantId = tenantMatch[1]; - console.log("Extracted tenantId:", tenantId); } } @@ -262,8 +243,6 @@ export default function GeneralPage() { : matchingRoleId || null }; - console.log(formData); - // Add variant-specific fields if (idpVariant === "oidc") { formData.authUrl = data.idpOidcConfig.authUrl; @@ -276,7 +255,6 @@ export default function GeneralPage() { formData.scopes = data.idpOidcConfig.scopes; } else if (idpVariant === "azure") { formData.tenantId = tenantId; - console.log("Setting tenantId in formData:", tenantId); } form.reset(formData); @@ -284,7 +262,7 @@ export default function GeneralPage() { // Set the role mapping mode based on the data // Default to "expression" unless it's a simple roleId or basic '{role name}' pattern setRoleMappingMode( - isRoleId || isRoleName ? "role" : "expression" + matchingRoleId && isRoleName ? "role" : "expression" ); } } catch (e) { @@ -851,7 +829,7 @@ export default function GeneralPage() { {t( "idpJmespathAboutDescription" - )} + )}{" "} diff --git a/src/app/[orgId]/settings/(private)/license/layout.tsx b/src/app/[orgId]/settings/(private)/license/layout.tsx new file mode 100644 index 00000000..9083bb81 --- /dev/null +++ b/src/app/[orgId]/settings/(private)/license/layout.tsx @@ -0,0 +1,42 @@ +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { verifySession } from "@app/lib/auth/verifySession"; +import { redirect } from "next/navigation"; +import { cache } from "react"; +import { getTranslations } from "next-intl/server"; +import { build } from "@server/build"; + +type LicensesSettingsProps = { + children: React.ReactNode; + params: Promise<{ orgId: string }>; +}; + +export default async function LicensesSetingsLayoutProps({ + children, + params +}: LicensesSettingsProps) { + const { orgId } = await params; + + if (build !== "saas") { + redirect(`/${orgId}/settings`); + } + + const getUser = cache(verifySession); + const user = await getUser(); + + if (!user) { + redirect(`/`); + } + + const t = await getTranslations(); + + return ( + <> + + + {children} + + ); +} diff --git a/src/app/[orgId]/settings/(private)/license/page.tsx b/src/app/[orgId]/settings/(private)/license/page.tsx new file mode 100644 index 00000000..1ecc94c1 --- /dev/null +++ b/src/app/[orgId]/settings/(private)/license/page.tsx @@ -0,0 +1,25 @@ +import GenerateLicenseKeysTable from "@app/components/GenerateLicenseKeysTable"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types"; +import { AxiosResponse } from "axios"; + +type Props = { + params: Promise<{ orgId: string }>; +}; + +export const dynamic = "force-dynamic"; + +export default async function Page({ params }: Props) { + const { orgId } = await params; + + let licenseKeys: ListGeneratedLicenseKeysResponse = []; + try { + const data = await internal.get< + AxiosResponse + >(`/org/${orgId}/license`, await authCookieHeader()); + licenseKeys = data.data.data; + } catch {} + + return ; +} diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx index 4a1db4ea..a1bb69c0 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx index 11e0bcfc..c7e332d7 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx index b5835e1b..191ce3f3 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - export default function GeneralPage() { return <>; } 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 653444e8..7a7b3611 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 @@ -1,24 +1,11 @@ -/* - * 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 { internal } from "@app/lib/api"; -import { GetRemoteExitNodeResponse } from "@server/routers/private/remoteExitNode"; +import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; -import RemoteExitNodeProvider from "@app/providers/PrivateRemoteExitNodeProvider"; +import RemoteExitNodeProvider from "@app/providers/RemoteExitNodeProvider"; interface SettingsLayoutProps { children: React.ReactNode; diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx index badf0971..6b39c1de 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx @@ -1,16 +1,3 @@ -/* - * 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 { redirect } from "next/navigation"; export default async function RemoteExitNodePage(props: { diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx index 5daaa493..ca3e0cba 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { @@ -43,7 +30,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { QuickStartRemoteExitNodeResponse, PickRemoteExitNodeDefaultsResponse -} from "@server/routers/private/remoteExitNode"; +} from "@server/routers/remoteExitNode/types"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; import { useParams, useRouter, useSearchParams } from "next/navigation"; 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 b18df692..632dc0ad 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx @@ -1,19 +1,6 @@ -/* - * 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 { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; -import { ListRemoteExitNodesResponse } from "@server/routers/private/remoteExitNode"; +import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { AxiosResponse } from "axios"; import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index 3e6d2458..d789b2e2 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -47,8 +47,8 @@ import { ListIdpsResponse } from "@server/routers/idp"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import Image from "next/image"; -import { usePrivateSubscriptionStatusContext } from "@app/hooks/privateUseSubscriptionStatusContext"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { TierId } from "@server/lib/billing/tiers"; type UserType = "internal" | "oidc"; @@ -76,10 +76,11 @@ export default function Page() { const api = createApiClient({ env }); const t = useTranslations(); - const subscription = usePrivateSubscriptionStatusContext(); - const subscribed = subscription?.getTier() === TierId.STANDARD; + const subscription = useSubscriptionStatusContext(); - const [selectedOption, setSelectedOption] = useState("internal"); + const [selectedOption, setSelectedOption] = useState( + "internal" + ); const [inviteLink, setInviteLink] = useState(null); const [loading, setLoading] = useState(false); const [expiresInDays, setExpiresInDays] = useState(1); @@ -204,7 +205,13 @@ export default function Page() { googleAzureForm.reset(); genericOidcForm.reset(); } - }, [selectedOption, env.email.emailEnabled, internalForm, googleAzureForm, genericOidcForm]); + }, [ + selectedOption, + env.email.emailEnabled, + internalForm, + googleAzureForm, + genericOidcForm + ]); useEffect(() => { if (!selectedOption) { @@ -232,7 +239,7 @@ export default function Page() { } async function fetchIdps() { - if (build === "saas" && !subscribed) { + if (build === "saas" && !subscription?.subscribed) { return; } @@ -345,7 +352,9 @@ export default function Page() { async function onSubmitGoogleAzure( values: z.infer ) { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + const selectedUserOption = userOptions.find( + (opt) => opt.id === selectedOption + ); if (!selectedUserOption?.idpId) return; setLoading(true); @@ -385,7 +394,9 @@ export default function Page() { async function onSubmitGenericOidc( values: z.infer ) { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + const selectedUserOption = userOptions.find( + (opt) => opt.id === selectedOption + ); if (!selectedUserOption?.idpId) return; setLoading(true); @@ -675,214 +686,284 @@ export default function Page() { )} - {selectedOption && selectedOption !== "internal" && dataLoaded && ( - - - - {t("userSettings")} - - - {t("userSettingsDescription")} - - - - - {/* Google/Azure Form */} - {(() => { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); - return selectedUserOption?.variant === "google" || selectedUserOption?.variant === "azure"; - })() && ( -
- + + + {t("userSettings")} + + + {t("userSettingsDescription")} + + + + + {/* Google/Azure Form */} + {(() => { + const selectedUserOption = + userOptions.find( + (opt) => + opt.id === + selectedOption + ); + return ( + selectedUserOption?.variant === + "google" || + selectedUserOption?.variant === + "azure" + ); + })() && ( + + + ( + + + {t("email")} + + + + + + )} - className="space-y-4" - id="create-user-form" - > - ( - - - {t("email")} - - - - - - - )} - /> + /> - ( - - - {t("nameOptional")} - - - - - - - )} - /> + ( + + + {t( + "nameOptional" + )} + + + + + + + )} + /> - ( - - - {t("role")} - - + + + + + + + {roles.map( + ( + role + ) => ( - {role.name} + { + role.name + } - ))} - - - - - )} - /> - - - )} - - {/* Generic OIDC Form */} - {(() => { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); - return selectedUserOption?.variant !== "google" && selectedUserOption?.variant !== "azure"; - })() && ( -
- + + + )} - className="space-y-4" - id="create-user-form" - > - ( - - - {t("username")} - - - - -

- {t("usernameUniq")} -

- -
- )} - /> + /> + + + )} - ( - - - {t("emailOptional")} - - - - - - - )} - /> + {/* Generic OIDC Form */} + {(() => { + const selectedUserOption = + userOptions.find( + (opt) => + opt.id === + selectedOption + ); + return ( + selectedUserOption?.variant !== + "google" && + selectedUserOption?.variant !== + "azure" + ); + })() && ( +
+ + ( + + + {t( + "username" + )} + + + + +

+ {t( + "usernameUniq" + )} +

+ +
+ )} + /> - ( - - - {t("nameOptional")} - - - - - - - )} - /> + ( + + + {t( + "emailOptional" + )} + + + + + + + )} + /> - ( - - - {t("role")} - - + + + + )} + /> + + ( + + + {t("role")} + + - - - )} - /> - - - )} -
-
-
- )} + ) + )} + + + + + )} + /> + + + )} + + + + )}
diff --git a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx index 55d7c0d3..c7171c8d 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx @@ -115,7 +115,7 @@ export default function GeneralPage() { try { await api.post(`/client/${client?.clientId}`, { name: data.name, - siteIds: data.siteIds.map(site => site.id) + siteIds: data.siteIds.map(site => parseInt(site.id)) }); updateClient({ name: data.name }); diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx index 994b1d56..0813ad3c 100644 --- a/src/app/[orgId]/settings/clients/page.tsx +++ b/src/app/[orgId]/settings/clients/page.tsx @@ -5,6 +5,7 @@ import { ClientRow } from "../../../../components/ClientsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { ListClientsResponse } from "@server/routers/client"; import ClientsTable from "../../../../components/ClientsTable"; +import { getTranslations } from "next-intl/server"; type ClientsPageProps = { params: Promise<{ orgId: string }>; @@ -13,6 +14,8 @@ type ClientsPageProps = { export const dynamic = "force-dynamic"; export default async function ClientsPage(props: ClientsPageProps) { + const t = await getTranslations(); + const params = await props.params; let clients: ListClientsResponse["clients"] = []; try { @@ -48,8 +51,8 @@ export default async function ClientsPage(props: ClientsPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index a7948536..1801bcf2 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -1,6 +1,8 @@ "use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import AuthPageSettings, { AuthPageSettingsRef } from "@app/components/private/AuthPageSettings"; +import AuthPageSettings, { + AuthPageSettingsRef +} from "@app/components/private/AuthPageSettings"; import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; @@ -134,7 +136,10 @@ export default function GeneralPage() { }); // Also save auth page settings if they have unsaved changes - if (build === "saas" && authPageSettingsRef.current?.hasUnsavedChanges()) { + if ( + build === "saas" && + authPageSettingsRef.current?.hasUnsavedChanges() + ) { await authPageSettingsRef.current.saveAuthSettings(); } @@ -239,7 +244,9 @@ export default function GeneralPage() { - {build === "saas" && } + {(build === "saas") && ( + + )} {/* Save Button */}
@@ -276,7 +283,6 @@ export default function GeneralPage() { )} - ); } diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx index 31cfbc5d..56c989c3 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx @@ -60,8 +60,8 @@ import { } from "@app/components/ui/select"; import { Separator } from "@app/components/ui/separator"; import { build } from "@server/build"; -import { usePrivateSubscriptionStatusContext } from "@app/hooks/privateUseSubscriptionStatusContext"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { TierId } from "@server/lib/billing/tiers"; const UsersRolesFormSchema = z.object({ roles: z.array( @@ -98,8 +98,7 @@ export default function ResourceAuthenticationPage() { const router = useRouter(); const t = useTranslations(); - const subscription = usePrivateSubscriptionStatusContext(); - const subscribed = subscription?.getTier() === TierId.STANDARD; + const subscription = useSubscriptionStatusContext(); const [pageLoading, setPageLoading] = useState(true); @@ -141,8 +140,10 @@ export default function ResourceAuthenticationPage() { useState(false); const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] = useState(false); - const [loadingRemoveResourceHeaderAuth, setLoadingRemoveResourceHeaderAuth] = - useState(false); + const [ + loadingRemoveResourceHeaderAuth, + setLoadingRemoveResourceHeaderAuth + ] = useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); @@ -234,7 +235,7 @@ export default function ResourceAuthenticationPage() { ); if (build === "saas") { - if (subscribed) { + if (subscription?.subscribed) { setAllIdps( idpsResponse.data.data.idps.map((idp) => ({ id: idp.idpId, @@ -835,11 +836,11 @@ export default function ResourceAuthenticationPage() { > - {t("resourceHeaderAuthProtection", { - status: authInfo.headerAuth - ? t("enabled") - : t("disabled") - })} + {authInfo.headerAuth + ? t("resourceHeaderAuthProtectionEnabled") + : t( + "resourceHeaderAuthProtectionDisabled" + )}
- } - /> - - - {/* */} -
- ) : ( - - updateTarget(row.original.targetId, config) - } - trigger={ - - } - /> - ); - } - }, - { - accessorKey: "siteId", - header: t("site"), - cell: ({ row }) => { - const selectedSite = sites.find( - (site) => site.siteId === row.original.siteId - ); - - const handleContainerSelectForTarget = ( - hostname: string, - port?: number - ) => { - updateTarget(row.original.targetId, { - ...row.original, - ip: hostname - }); - if (port) { - updateTarget(row.original.targetId, { - ...row.original, - port: port - }); - } - }; - - return ( -
- - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - { - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ); - }} - > - - {site.name} - - ))} - - - - - - {selectedSite && - selectedSite.type === "newt" && - (() => { - const dockerState = getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })()} -
- ); - } - }, - ...(resource.http - ? [ - { - accessorKey: "method", - header: t("method"), - cell: ({ row }: { row: Row }) => ( - - ) - } - ] - : []), - { - accessorKey: "ip", - header: t("targetAddr"), - cell: ({ row }) => ( - { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); - - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); - if (parsed) { - updateTarget(row.original.targetId, { - ...row.original, - method: hasProtocol - ? parsed.protocol - : row.original.method, - ip: parsed.host, - port: hasPort - ? parsed.port - : row.original.port - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - }} - /> - ) - }, - { - accessorKey: "port", - header: t("targetPort"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) - } - /> - ) - }, - { - accessorKey: "rewritePath", - header: t("rewritePath"), - cell: ({ row }) => { - const hasRewritePath = !!( - row.original.rewritePath || row.original.rewritePathType - ); - const noPathMatch = - !row.original.path && !row.original.pathMatchType; - - return hasRewritePath && !noPathMatch ? ( -
- {/* */} - - updateTarget(row.original.targetId, config) - } - trigger={ - - } - /> - -
- ) : ( - - updateTarget(row.original.targetId, config) - } - trigger={ - - } - disabled={noPathMatch} - /> - ); - } - }, - - // { - // accessorKey: "protocol", - // header: t('targetProtocol'), - // cell: ({ row }) => ( - // - // ), - // }, - { + const healthCheckColumn: ColumnDef = { accessorKey: "healthCheck", header: t("healthCheck"), cell: ({ row }) => { @@ -1115,74 +903,440 @@ export default function ReverseProxyTargets(props: { }; return ( - <> +
{row.original.siteType === "newt" ? ( -
+ -
+ + ) : ( - - {t("healthCheckNotAvailable")} - + - )} - +
); - } - }, - { + }, + size: 200, + minSize: 180, + maxSize: 250 + }; + + const matchPathColumn: ColumnDef = { + accessorKey: "path", + header: t("matchPath"), + cell: ({ row }) => { + const hasPathMatch = !!( + row.original.path || row.original.pathMatchType + ); + + return ( +
+ {hasPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const addressColumn: ColumnDef = { + accessorKey: "address", + header: t("address"), + cell: ({ row }) => { + const selectedSite = sites.find( + (site) => site.siteId === row.original.siteId + ); + + const handleContainerSelectForTarget = ( + hostname: string, + port?: number + ) => { + updateTarget(row.original.targetId, { + ...row.original, + ip: hostname + }); + if (port) { + updateTarget(row.original.targetId, { + ...row.original, + port: port + }); + } + }; + + return ( +
+
+ {selectedSite && + selectedSite.type === "newt" && + (() => { + const dockerState = getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })()} + + + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + + updateTarget( + row.original + .targetId, + { + siteId: site.siteId + } + ) + } + > + + {site.name} + + ))} + + + + + + + + +
+ {"://"} +
+ + { + const input = e.target.value.trim(); + const hasProtocol = + /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + updateTarget( + row.original.targetId, + { + ...row.original, + method: hasProtocol + ? parsed.protocol + : row.original.method, + ip: parsed.host, + port: hasPort + ? parsed.port + : row.original.port + } + ); + } else { + updateTarget( + row.original.targetId, + { + ...row.original, + ip: input + } + ); + } + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + }} + /> +
+ {":"} +
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0) { + updateTarget(row.original.targetId, { + ...row.original, + port: value + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + port: 0 + }); + } + }} + /> +
+
+ ); + }, + size: 400, + minSize: 350, + maxSize: 500 + }; + + const rewritePathColumn: ColumnDef = { + accessorKey: "rewritePath", + header: t("rewritePath"), + cell: ({ row }) => { + const hasRewritePath = !!( + row.original.rewritePath || row.original.rewritePathType + ); + const noPathMatch = + !row.original.path && !row.original.pathMatchType; + + return ( +
+ {hasRewritePath && !noPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + disabled={noPathMatch} + /> + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const enabledColumn: ColumnDef = { accessorKey: "enabled", header: t("enabled"), cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - enabled: val - }) - } - /> - ) - }, - { +
+ + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + const actionsColumn: ColumnDef = { id: "actions", cell: ({ row }) => ( - <> -
- {/* */} +
+ +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; - -
- - ) + if (isAdvancedMode) { + return [ + matchPathColumn, + addressColumn, + rewritePathColumn, + priorityColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; + } else { + return [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; } - ]; + }; + + const columns = getColumns(); const table = useReactTable({ data: targets, @@ -1213,352 +1367,9 @@ export default function ReverseProxyTargets(props: { -
-
- -
- ( - - - {t("site")} - -
- - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - addTargetForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - {field.value && - (() => { - const selectedSite = - sites.find( - (site) => - site.siteId === - field.value - ); - return selectedSite && - selectedSite.type === - "newt" - ? (() => { - const dockerState = - getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })() - : null; - })()} -
- -
- )} - /> - - {resource.http && ( - ( - - - {t("method")} - - - - - - - )} - /> - )} - - ( - - - {t("targetAddr")} - - - { - const input = - e.target.value.trim(); - const hasProtocol = - /^(https?|h2c):\/\//.test( - input - ); - const hasPort = - /:\d+(?:\/|$)/.test( - input - ); - - if ( - hasProtocol || - hasPort - ) { - const parsed = - parseHostTarget( - input - ); - if (parsed) { - if ( - hasProtocol || - !addTargetForm.getValues( - "method" - ) - ) { - addTargetForm.setValue( - "method", - parsed.protocol - ); - } - addTargetForm.setValue( - "ip", - parsed.host - ); - if ( - hasPort || - !addTargetForm.getValues( - "port" - ) - ) { - addTargetForm.setValue( - "port", - parsed.port - ); - } - } - } else { - field.onBlur(); - } - }} - /> - - - - )} - /> - ( - - - {t("targetPort")} - - - - - - - )} - /> - -
-
- -
- {targets.length > 0 ? ( <> -
- {t("targetsList")} -
- -
- - ( - - - { - field.onChange( - val - ); - }} - /> - - - )} - /> - - -
-
+
{table @@ -1626,12 +1437,40 @@ export default function ReverseProxyTargets(props: { {/* */}
+
+
+ +
+ + +
+
+
) : ( -
-

+

+

{t("targetNoOne")}

+
)} @@ -1669,6 +1508,9 @@ export default function ReverseProxyTargets(props: { label={t( "proxyEnableSSL" )} + description={t( + "proxyEnableSSLDescription" + )} defaultChecked={ field.value } @@ -1709,6 +1551,46 @@ export default function ReverseProxyTargets(props: { + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + +
+
( - + {t("customHeaders")} diff --git a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx index 8d5ad7d3..b8459293 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx @@ -117,8 +117,8 @@ export default function ResourceRules(props: { const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); const router = useRouter(); const t = useTranslations(); - const env = useEnvContext(); - const isMaxmindAvailable = env.env.server.maxmind_db_path && env.env.server.maxmind_db_path.length > 0; + const { env } = useEnvContext(); + const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0; const RuleAction = { ACCEPT: t('alwaysAllow'), diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index 55a7a7be..e12da03a 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -58,7 +58,7 @@ import { } from "@app/components/ui/popover"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; -import { ArrowRight, Info, MoveRight, Plus, SquareArrowOutUpRight } from "lucide-react"; +import { ArrowRight, CircleCheck, CircleX, Info, MoveRight, Plus, Settings, SquareArrowOutUpRight } from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; import Link from "next/link"; import { useTranslations } from "next-intl"; @@ -94,6 +94,9 @@ import { DomainRow } from "../../../../../components/DomainsTable"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; +import { Badge } from "@app/components/ui/badge"; +import HealthCheckDialog from "@app/components/HealthCheckDialog"; +import { SwitchInput } from "@app/components/SwitchInput"; const baseResourceFormSchema = z.object({ @@ -112,6 +115,11 @@ const tcpUdpResourceFormSchema = z.object({ // enableProxy: z.boolean().default(false) }); +const targetsSettingsSchema = z.object({ + stickySession: z.boolean() +}); + + const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), @@ -215,6 +223,64 @@ export default function Page() { const [targetsToRemove, setTargetsToRemove] = useState([]); const [dockerStates, setDockerStates] = useState>(new Map()); + const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = + useState(null); + const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); + + const [isAdvancedMode, setIsAdvancedMode] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("create-advanced-mode"); + return saved === "true"; + } + return false; + }); + + // Save advanced mode preference to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem( + "create-advanced-mode", + isAdvancedMode.toString() + ); + } + }, [isAdvancedMode]); + + function addNewTarget() { + const newTarget: LocalTarget = { + targetId: -Date.now(), // Use negative timestamp as temporary ID + ip: "", + method: baseForm.watch("http") ? "http" : null, + port: 0, + siteId: sites.length > 0 ? sites[0].siteId : 0, + path: null, + pathMatchType: null, + rewritePath: null, + rewritePathType: null, + priority: 100, + enabled: true, + resourceId: 0, + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null, + siteType: sites.length > 0 ? sites[0].type : null, + new: true, + updated: false + }; + + setTargets((prev) => [...prev, newTarget]); + } + const resourceTypes: ReadonlyArray = [ { id: "http", @@ -268,6 +334,13 @@ export default function Page() { } as z.infer }); + const targetsSettingsForm = useForm({ + resolver: zodResolver(targetsSettingsSchema), + defaultValues: { + stickySession: false + } + }); + const watchedIp = addTargetForm.watch("ip"); const watchedPort = addTargetForm.watch("port"); const watchedSiteId = addTargetForm.watch("siteId"); @@ -405,11 +478,13 @@ export default function Page() { const baseData = baseForm.getValues(); const isHttp = baseData.http; + const stickySessionData = targetsSettingsForm.getValues(); try { const payload = { name: baseData.name, - http: baseData.http + http: baseData.http, + stickySession: stickySessionData.stickySession }; let sanitizedSubdomain: string | undefined; @@ -603,19 +678,43 @@ export default function Page() { load(); }, []); - const columns: ColumnDef[] = [ - { + function TargetHealthCheck(targetId: number, config: any) { + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...config, + updated: true + } + : target + ) + ); + } + + const openHealthCheckDialog = (target: LocalTarget) => { + console.log(target); + setSelectedTargetForHealthCheck(target); + setHealthCheckDialogOpen(true); + }; + + const getColumns = (): ColumnDef[] => { + const baseColumns: ColumnDef[] = []; + + const priorityColumn: ColumnDef = { id: "priority", header: () => (
- Priority + {t("priority")} -

Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.

+

+ {t("priorityDescription")} +

@@ -623,13 +722,13 @@ export default function Page() { ), cell: ({ row }) => { return ( -
+
{ const value = parseInt(e.target.value, 10); if (value >= 1 && value <= 1000) { @@ -642,76 +741,149 @@ export default function Page() { />
); - } - }, - { + }, + size: 120, + minSize: 100, + maxSize: 150 + }; + + const healthCheckColumn: ColumnDef = { + accessorKey: "healthCheck", + header: t("healthCheck"), + cell: ({ row }) => { + const status = row.original.hcHealth || "unknown"; + const isEnabled = row.original.hcEnabled; + + const getStatusColor = (status: string) => { + switch (status) { + case "healthy": + return "green"; + case "unhealthy": + return "red"; + case "unknown": + default: + return "secondary"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "healthy": + return t("healthCheckHealthy"); + case "unhealthy": + return t("healthCheckUnhealthy"); + case "unknown": + default: + return t("healthCheckUnknown"); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "healthy": + return ; + case "unhealthy": + return ; + case "unknown": + default: + return null; + } + }; + + return ( +
+ {row.original.siteType === "newt" ? ( + + ) : ( + - + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 250 + }; + + const matchPathColumn: ColumnDef = { accessorKey: "path", header: t("matchPath"), cell: ({ row }) => { - const hasPathMatch = !!(row.original.path || row.original.pathMatchType); + const hasPathMatch = !!( + row.original.path || row.original.pathMatchType + ); - return hasPathMatch ? ( -
- updateTarget(row.original.targetId, config)} - trigger={ - - } - /> - - - {/* */} + return ( +
+ {hasPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + )}
- ) : ( - updateTarget(row.original.targetId, config)} - trigger={ - - } - /> ); }, - }, - { - accessorKey: "siteId", - header: t("site"), + size: 200, + minSize: 180, + maxSize: 200 + }; + + const addressColumn: ColumnDef = { + accessorKey: "address", + header: t("address"), cell: ({ row }) => { const selectedSite = sites.find( (site) => site.siteId === row.original.siteId @@ -734,260 +906,324 @@ export default function Page() { }; return ( -
- - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - { - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ); - }} - > - +
+ + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + + updateTarget( row.original - .siteId - ? "opacity-100" - : "opacity-0" - )} - /> - {site.name} - - ))} - - - - - - {selectedSite && selectedSite.type === "newt" && (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })()} + .targetId, + { + siteId: site.siteId + } + ) + } + > + + {site.name} + + ))} + + + + + + {selectedSite && + selectedSite.type === "newt" && + (() => { + const dockerState = getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })()} + + + +
+ {"://"} +
+ + { + const input = e.target.value.trim(); + const hasProtocol = + /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + updateTarget( + row.original.targetId, + { + ...row.original, + method: hasProtocol + ? parsed.protocol + : row.original.method, + ip: parsed.host, + port: hasPort + ? parsed.port + : row.original.port + } + ); + } else { + updateTarget( + row.original.targetId, + { + ...row.original, + ip: input + } + ); + } + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + }} + /> +
+ {":"} +
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0) { + updateTarget(row.original.targetId, { + ...row.original, + port: value + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + port: 0 + }); + } + }} + /> +
); - } - }, - ...(baseForm.watch("http") - ? [ - { - accessorKey: "method", - header: t("method"), - cell: ({ row }: { row: Row }) => ( - - ) - } - ] - : []), - { - accessorKey: "ip", - header: t("targetAddr"), - cell: ({ row }) => ( - { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); + }, + size: 400, + minSize: 350, + maxSize: 500 + }; - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); - if (parsed) { - updateTarget(row.original.targetId, { - ...row.original, - method: hasProtocol ? parsed.protocol : row.original.method, - ip: parsed.host, - port: hasPort ? parsed.port : row.original.port - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - }} - /> - ) - }, - { - accessorKey: "port", - header: t("targetPort"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) - } - /> - ) - }, - { + const rewritePathColumn: ColumnDef = { accessorKey: "rewritePath", header: t("rewritePath"), cell: ({ row }) => { - const hasRewritePath = !!(row.original.rewritePath || row.original.rewritePathType); - const noPathMatch = !row.original.path && !row.original.pathMatchType; + const hasRewritePath = !!( + row.original.rewritePath || row.original.rewritePathType + ); + const noPathMatch = + !row.original.path && !row.original.pathMatchType; - return hasRewritePath && !noPathMatch ? ( -
- {/* */} - updateTarget(row.original.targetId, config)} - trigger={ - - } - /> - + return ( +
+ {hasRewritePath && !noPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + disabled={noPathMatch} + /> + )}
- ) : ( - updateTarget(row.original.targetId, config)} - trigger={ - - } - disabled={noPathMatch} - /> ); }, - }, - { + size: 200, + minSize: 180, + maxSize: 200 + }; + + const enabledColumn: ColumnDef = { accessorKey: "enabled", header: t("enabled"), cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - enabled: val - }) - } - /> - ) - }, - { +
+ + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + const actionsColumn: ColumnDef = { id: "actions", cell: ({ row }) => ( - <> -
- -
- - ) +
+ +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + if (isAdvancedMode) { + return [ + matchPathColumn, + addressColumn, + rewritePathColumn, + priorityColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; + } else { + return [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; } - ]; + }; + + const columns = getColumns(); const table = useReactTable({ data: targets, @@ -1303,378 +1539,110 @@ export default function Page() { -
- - -
- ( - - - {t("site")} - -
- - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - addTargetForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - {field.value && - (() => { - const selectedSite = - sites.find( - ( - site - ) => - site.siteId === - field.value - ); - return selectedSite && - selectedSite.type === - "newt" ? (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })() : null; - })()} -
- -
- )} - /> - - {baseForm.watch("http") && ( - ( - - - {t( - "method" - )} - - - - - - - )} - /> - )} - - ( - - {t("targetAddr")} - - { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); - - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); - if (parsed) { - if (hasProtocol || !addTargetForm.getValues("method")) { - addTargetForm.setValue("method", parsed.protocol); - } - addTargetForm.setValue("ip", parsed.host); - if (hasPort || !addTargetForm.getValues("port")) { - addTargetForm.setValue("port", parsed.port); - } - } - } else { - field.onBlur(); - } - }} - /> - - - - )} - /> - ( - - - {t( - "targetPort" - )} - - - - - - - )} - /> - -
- - -
- {targets.length > 0 ? ( <> -
- {t("targetsList")} -
-
+
{table .getHeaderGroups() - .map( - ( - headerGroup - ) => ( - - {headerGroup.headers.map( - ( - header - ) => ( - - {header.isPlaceholder - ? null - : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} - - ) - )} - - ) - )} + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} - {table.getRowModel() - .rows?.length ? ( + {table.getRowModel().rows?.length ? ( table .getRowModel() - .rows.map( - (row) => ( - - {row - .getVisibleCells() - .map( - ( + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( cell - ) => ( - - {flexRender( - cell - .column - .columnDef - .cell, - cell.getContext() - )} - - ) - )} - - ) - ) + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) ) : ( - {t( - "targetNoOne" - )} + {t("targetNoOne")} )} + {/* */} + {/* {t('targetNoOneDescription')} */} + {/* */}
+
+
+ +
+ + +
+
+
) : ( -
-

+

+

{t("targetNoOne")}

+
)} @@ -1713,6 +1681,55 @@ export default function Page() { {t("resourceCreate")}
+ {selectedTargetForHealthCheck && ( + { + if (selectedTargetForHealthCheck) { + console.log(config); + TargetHealthCheck( + selectedTargetForHealthCheck.targetId, + config + ); + } + }} + /> + )} ) : ( diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index 97abdd4c..f4ba9d16 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -86,7 +86,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) { : resource.sso || resource.pincodeId !== null || resource.passwordId !== null || - resource.whitelist + resource.whitelist || + resource.headerAuthId ? "protected" : "not_protected", enabled: resource.enabled, diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 78fbfc0d..c0b8c2a6 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -53,7 +53,7 @@ import { CreateSiteResponse, PickSiteDefaultsResponse } from "@server/routers/site"; -import { ListRemoteExitNodesResponse } from "@server/routers/private/remoteExitNode"; +import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; import { useParams, useRouter } from "next/navigation"; diff --git a/src/app/admin/license/layout.tsx b/src/app/admin/license/layout.tsx new file mode 100644 index 00000000..6c6e8baf --- /dev/null +++ b/src/app/admin/license/layout.tsx @@ -0,0 +1,17 @@ +import { build } from "@server/build"; +import { redirect } from "next/navigation"; + +export const dynamic = "force-dynamic"; + +interface LayoutProps { + children: React.ReactNode; +} + +export default async function AdminLicenseLayout(props: LayoutProps) { + if (build !== "enterprise") { + redirect(`/admin`); + } + + return props.children; +} + diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index a871b8e0..665212fc 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -31,7 +31,6 @@ import { CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; -import { useRouter } from "next/navigation"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { SettingsContainer, @@ -43,14 +42,10 @@ import { SettingsSectionFooter } from "@app/components/Settings"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { Badge } from "@app/components/ui/badge"; -import { Check, Heart, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react"; +import { Check, Heart, InfoIcon } from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; -import { Progress } from "@app/components/ui/progress"; -import { MinusCircle, PlusCircle } from "lucide-react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { SitePriceCalculator } from "../../../components/SitePriceCalculator"; -import Link from "next/link"; import { Checkbox } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; @@ -70,13 +65,11 @@ export default function LicensePage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedLicenseKey, setSelectedLicenseKey] = useState(null); - const router = useRouter(); + const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext(); const [hostLicense, setHostLicense] = useState(null); const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false); - const [purchaseMode, setPurchaseMode] = useState< - "license" | "additional-sites" - >("license"); + const [purchaseMode, setPurchaseMode] = useState<"license">("license"); // Separate loading states for different actions const [isInitialLoading, setIsInitialLoading] = useState(true); @@ -90,10 +83,10 @@ export default function LicensePage() { const formSchema = z.object({ licenseKey: z .string() - .nonempty({ message: t('licenseKeyRequired') }) + .nonempty({ message: t("licenseKeyRequired") }) .max(255), agreeToTerms: z.boolean().refine((val) => val === true, { - message: t('licenseTermsAgree') + message: t("licenseTermsAgree") }) }); @@ -122,7 +115,7 @@ export default function LicensePage() { ); const keys = response.data.data; setRows(keys); - const hostKey = keys.find((key) => key.type === "HOST"); + const hostKey = keys.find((key) => key.type === "host"); if (hostKey) { setHostLicense(hostKey.licenseKey); } else { @@ -130,10 +123,10 @@ export default function LicensePage() { } } catch (e) { toast({ - title: t('licenseErrorKeyLoad'), + title: t("licenseErrorKeyLoad"), description: formatAxiosError( e, - t('licenseErrorKeyLoadDescription') + t("licenseErrorKeyLoadDescription") ) }); } @@ -149,16 +142,16 @@ export default function LicensePage() { } await loadLicenseKeys(); toast({ - title: t('licenseKeyDeleted'), - description: t('licenseKeyDeletedDescription') + title: t("licenseKeyDeleted"), + description: t("licenseKeyDeletedDescription") }); setIsDeleteModalOpen(false); } catch (e) { toast({ - title: t('licenseErrorKeyDelete'), + title: t("licenseErrorKeyDelete"), description: formatAxiosError( e, - t('licenseErrorKeyDeleteDescription') + t("licenseErrorKeyDeleteDescription") ) }); } finally { @@ -175,15 +168,15 @@ export default function LicensePage() { } await loadLicenseKeys(); toast({ - title: t('licenseErrorKeyRechecked'), - description: t('licenseErrorKeyRecheckedDescription') + title: t("licenseErrorKeyRechecked"), + description: t("licenseErrorKeyRecheckedDescription") }); } catch (e) { toast({ - title: t('licenseErrorKeyRecheck'), + title: t("licenseErrorKeyRecheck"), description: formatAxiosError( e, - t('licenseErrorKeyRecheckDescription') + t("licenseErrorKeyRecheckDescription") ) }); } finally { @@ -202,8 +195,8 @@ export default function LicensePage() { } toast({ - title: t('licenseKeyActivated'), - description: t('licenseKeyActivatedDescription') + title: t("licenseKeyActivated"), + description: t("licenseKeyActivatedDescription") }); setIsCreateModalOpen(false); @@ -212,10 +205,10 @@ export default function LicensePage() { } catch (e) { toast({ variant: "destructive", - title: t('licenseErrorKeyActivate'), + title: t("licenseErrorKeyActivate"), description: formatAxiosError( e, - t('licenseErrorKeyActivateDescription') + t("licenseErrorKeyActivateDescription") ) }); } finally { @@ -246,9 +239,9 @@ export default function LicensePage() { > - {t('licenseActivateKey')} + {t("licenseActivateKey")} - {t('licenseActivateKeyDescription')} + {t("licenseActivateKeyDescription")} @@ -263,7 +256,9 @@ export default function LicensePage() { name="licenseKey" render={({ field }) => ( - {t('licenseKey')} + + {t("licenseKey")} + @@ -286,16 +281,7 @@ export default function LicensePage() {
- {t('licenseAgreement')} - {/*
*/} - {/* */} - {/* {t('fossorialLicense')} */} - {/* */} + {t("licenseAgreement")}
@@ -307,7 +293,7 @@ export default function LicensePage() {
- +
@@ -331,187 +317,98 @@ export default function LicensePage() { dialog={

- {t('licenseQuestionRemove', {selectedKey: obfuscateLicenseKey(selectedLicenseKey.licenseKey)})} + {t("licenseQuestionRemove", { + selectedKey: obfuscateLicenseKey( + selectedLicenseKey.licenseKey + ) + })}

- - {t('licenseMessageRemove')} - -

-

- {t('licenseMessageConfirm')} + {t("licenseMessageRemove")}

+

{t("licenseMessageConfirm")}

} - buttonText={t('licenseKeyDeleteConfirm')} + buttonText={t("licenseKeyDeleteConfirm")} onConfirm={async () => deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted) } string={selectedLicenseKey.licenseKey} - title={t('licenseKeyDelete')} + title={t("licenseKeyDelete")} /> )} - - - - {t('licenseAbout')} - - - {t('licenseAboutDescription')} - - + {/* */} + {/* */} + {/* */} + {/* {t("licenseAbout")} */} + {/* */} + {/* */} + {/* {t("licenseAboutDescription")} */} + {/* */} + {/* */} - - - - {t('licenseHost')} - - {t('licenseHostDescription')} - - -
-
- {licenseStatus?.isLicenseValid ? ( -
-
- - {licenseStatus?.tier === - "PROFESSIONAL" - ? t('licenseTierCommercial') - : licenseStatus?.tier === - "ENTERPRISE" - ? t('licenseTierCommercial') - : t('licensed')} -
+ + + {t("licenseHost")} + + {t("licenseHostDescription")} + + +
+
+ {licenseStatus?.isLicenseValid ? ( +
+
+ + {t("licensed")}
- ) : ( -
- {supporterStatus?.visible ? ( -
- {t('communityEdition')} -
- ) : ( -
- - {t('communityEdition')} -
- )} -
- )} -
- {licenseStatus?.hostId && ( -
-
- {t('hostId')} -
-
- )} - {hostLicense && ( -
-
- {t('licenseKey')} -
- -
- )} -
- - - - - - - {t('licenseSiteUsage')} - - {t('licenseSiteUsageDecsription')} - - -
-
+ ) : (
- {t('licenseSitesUsed', {count: licenseStatus?.usedSites || 0})} -
-
- {!licenseStatus?.isHostLicensed && ( -

- {t('licenseNoSiteLimit')} -

- )} - {licenseStatus?.maxSites && ( -
-
- - {t('licenseSitesUsedMax', {usedSites: licenseStatus.usedSites || 0, maxSites: licenseStatus.maxSites})} - - - {Math.round( - ((licenseStatus.usedSites || - 0) / - licenseStatus.maxSites) * - 100 - )} - % - -
- + {t("unlicensed")}
)}
- {/* */} - {/* {!licenseStatus?.isHostLicensed ? ( */} - {/* <> */} - {/* */} - {/* */} - {/* ) : ( */} - {/* <> */} - {/* */} - {/* */} - {/* )} */} - {/* */} -
- + {licenseStatus?.hostId && ( +
+
+ {t("hostId")} +
+ +
+ )} + {hostLicense && ( +
+
+ {t("licenseKey")} +
+ +
+ )} +
+ + + +
{ diff --git a/src/app/admin/managed/page.tsx b/src/app/admin/managed/page.tsx deleted file mode 100644 index f7a8f70b..00000000 --- a/src/app/admin/managed/page.tsx +++ /dev/null @@ -1,180 +0,0 @@ -"use client"; - -import { - SettingsContainer, - SettingsSection, - SettingsSectionTitle as SectionTitle, - SettingsSectionBody, - SettingsSectionFooter -} from "@app/components/Settings"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { Alert } from "@app/components/ui/alert"; -import { Button } from "@app/components/ui/button"; -import { - Shield, - Zap, - RefreshCw, - Activity, - Wrench, - CheckCircle, - ExternalLink -} from "lucide-react"; -import Link from "next/link"; -import { useTranslations } from "next-intl"; - -export default function ManagedPage() { - const t = useTranslations(); - - return ( - <> - - - - - -

- {t("managedSelfHosted.introTitle")}{" "} - {t("managedSelfHosted.introDescription")} -

-

- {t("managedSelfHosted.introDetail")} -

- -
-
-
- -
-

- {t( - "managedSelfHosted.benefitSimplerOperations.title" - )} -

-

- {t( - "managedSelfHosted.benefitSimplerOperations.description" - )} -

-
-
- -
- -
-

- {t( - "managedSelfHosted.benefitAutomaticUpdates.title" - )} -

-

- {t( - "managedSelfHosted.benefitAutomaticUpdates.description" - )} -

-
-
- -
- -
-

- {t( - "managedSelfHosted.benefitLessMaintenance.title" - )} -

-

- {t( - "managedSelfHosted.benefitLessMaintenance.description" - )} -

-
-
-
- -
-
- -
-

- {t( - "managedSelfHosted.benefitCloudFailover.title" - )} -

-

- {t( - "managedSelfHosted.benefitCloudFailover.description" - )} -

-
-
-
- -
-

- {t( - "managedSelfHosted.benefitHighAvailability.title" - )} -

-

- {t( - "managedSelfHosted.benefitHighAvailability.description" - )} -

-
-
- -
- -
-

- {t( - "managedSelfHosted.benefitFutureEnhancements.title" - )} -

-

- {t( - "managedSelfHosted.benefitFutureEnhancements.description" - )} -

-
-
-
-
- - - {t("managedSelfHosted.docsAlert.text")}{" "} - - {t("managedSelfHosted.docsAlert.documentation")} - - - . - -
- - - - - -
-
- - ); -} diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/(private)/org/page.tsx index 0cbe101b..ff3604c3 100644 --- a/src/app/auth/(private)/org/page.tsx +++ b/src/app/auth/(private)/org/page.tsx @@ -1,16 +1,3 @@ -/* - * 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 { formatAxiosError, priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; @@ -19,13 +6,12 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; -import { ListOrgIdpsResponse } from "@server/routers/private/orgIdp"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import { build } from "@server/build"; import { headers } from "next/headers"; import { - GetLoginPageResponse, LoadLoginPageResponse -} from "@server/routers/private/loginPage"; +} from "@server/routers/loginPage/types"; import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; import { Card, @@ -37,11 +23,10 @@ import { import { Button } from "@app/components/ui/button"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; -import { GetSessionTransferTokenRenponse } from "@server/routers/auth/privateGetSessionTransferToken"; -import { TransferSessionResponse } from "@server/routers/auth/privateTransferSession"; +import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types"; import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; -import { GetOrgTierResponse } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { GetOrgTierResponse } from "@server/routers/billing/types"; +import { TierId } from "@server/lib/billing/tiers"; export const dynamic = "force-dynamic"; @@ -84,22 +69,33 @@ export default async function OrgAuthPage(props: { } catch (e) {} if (!loginPage) { + console.debug( + `No login page found for host ${host}, redirecting to dashboard` + ); redirect(env.app.dashboardUrl); } let subscriptionStatus: GetOrgTierResponse | null = null; - try { - const getSubscription = cache(() => - priv.get>( - `/org/${loginPage!.orgId}/billing/tier` - ) - ); - const subRes = await getSubscription(); - subscriptionStatus = subRes.data.data; - } catch {} - const subscribed = subscriptionStatus?.tier === TierId.STANDARD; + if (build === "saas") { + try { + const getSubscription = cache(() => + priv.get>( + `/org/${loginPage!.orgId}/billing/tier` + ) + ); + const subRes = await getSubscription(); + subscriptionStatus = subRes.data.data; + } catch {} + } + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; if (build === "saas" && !subscribed) { + console.log( + `Org ${loginPage.orgId} is not subscribed, redirecting to dashboard` + ); redirect(env.app.dashboardUrl); } @@ -126,6 +122,7 @@ export default async function OrgAuthPage(props: { } } } else { + console.log(`Host ${host} is the same`); redirect(env.app.dashboardUrl); } diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx index 5dfa72c3..a2432e3e 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx @@ -6,7 +6,7 @@ import { AxiosResponse } from "axios"; import { GetIdpResponse } from "@server/routers/idp"; import { getTranslations } from "next-intl/server"; import { pullEnv } from "@app/lib/pullEnv"; -import { LoadLoginPageResponse } from "@server/routers/private/loginPage"; +import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; import { redirect } from "next/navigation"; export const dynamic = "force-dynamic"; diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 0902baaa..88b0f07d 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -1,15 +1,5 @@ -import ProfileIcon from "@app/components/ProfileIcon"; import ThemeSwitcher from "@app/components/ThemeSwitcher"; -import { Separator } from "@app/components/ui/separator"; -import { priv } from "@app/lib/api"; -import { verifySession } from "@app/lib/auth/verifySession"; -import UserProvider from "@app/providers/UserProvider"; -import { GetLicenseStatusResponse } from "@server/routers/license"; -import { AxiosResponse } from "axios"; -import { ExternalLink } from "lucide-react"; import { Metadata } from "next"; -import { cache } from "react"; -import { getTranslations } from "next-intl/server"; export const metadata: Metadata = { title: `Auth - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, @@ -21,19 +11,6 @@ type AuthLayoutProps = { }; export default async function AuthLayout({ children }: AuthLayoutProps) { - const getUser = cache(verifySession); - const user = await getUser(); - const t = await getTranslations(); - const hideFooter = true; - - const licenseStatusRes = await cache( - async () => - await priv.get>( - "/license/status" - ) - )(); - const licenseStatus = licenseStatusRes.data.data; - return (
); } diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index 3eaf1e86..c6231c69 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -15,13 +15,13 @@ import AccessToken from "@app/components/AccessToken"; import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; import { ListIdpsResponse } from "@server/routers/idp"; -import { ListOrgIdpsResponse } from "@server/routers/private/orgIdp"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import AutoLoginHandler from "@app/components/AutoLoginHandler"; import { build } from "@server/build"; import { headers } from "next/headers"; -import { GetLoginPageResponse } from "@server/routers/private/loginPage"; -import { GetOrgTierResponse } from "@server/routers/private/billing"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { GetLoginPageResponse } from "@server/routers/loginPage/types"; +import { GetOrgTierResponse } from "@server/routers/billing/types"; +import { TierId } from "@server/lib/billing/tiers"; export const dynamic = "force-dynamic"; @@ -73,7 +73,10 @@ export default async function ResourceAuthPage(props: { subscriptionStatus = subRes.data.data; } catch {} } - const subscribed = subscriptionStatus?.tier === TierId.STANDARD; + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; const allHeaders = await headers(); const host = allHeaders.get("host"); @@ -207,7 +210,12 @@ export default async function ResourceAuthPage(props: { })) as LoginFormIDP[]; } - if (authInfo.skipToIdpId && authInfo.skipToIdpId !== null) { + if ( + !userIsUnauthorized && + isSSOOnly && + authInfo.skipToIdpId && + authInfo.skipToIdpId !== null + ) { const idp = loginIdps.find((idp) => idp.idpId === authInfo.skipToIdpId); if (idp) { return ( diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 49c5a2c5..2e027f77 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -1,11 +1,6 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import { verifySession } from "@app/lib/auth/verifySession"; -import { AcceptInviteResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import InviteStatusCard from "../../components/InviteStatusCard"; -import { formatAxiosError } from "@app/lib/api"; import { getTranslations } from "next-intl/server"; export default async function InvitePage(props: { @@ -27,8 +22,8 @@ export default async function InvitePage(props: { if (parts.length !== 2) { return ( <> -

{t('inviteInvalid')}

-

{t('inviteInvalidDescription')}

+

{t("inviteInvalid")}

+

{t("inviteInvalidDescription")}

); } @@ -36,58 +31,15 @@ export default async function InvitePage(props: { const inviteId = parts[0]; const token = parts[1]; - let error = ""; - const res = await internal - .post>( - `/invite/accept`, - { - inviteId, - token, - }, - await authCookieHeader() - ) - .catch((e) => { - error = formatAxiosError(e); - console.error(error); - }); - - if (res && res.status === 200) { - redirect(`/${res.data.data.orgId}`); - } - - function cardType() { - if (error.includes("Invite is not for this user")) { - return "wrong_user"; - } else if ( - error.includes("User does not exist. Please create an account first.") - ) { - return "user_does_not_exist"; - } else if (error.includes("You must be logged in to accept an invite")) { - return "not_logged_in"; - } else { - return "rejected"; - } - } - - const type = cardType(); - - if (!user && type === "user_does_not_exist") { - const redirectUrl = emailParam - ? `/auth/signup?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` - : `/auth/signup?redirect=/invite?token=${params.token}`; - redirect(redirectUrl); - } - - if (!user && type === "not_logged_in") { - const redirectUrl = emailParam - ? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` - : `/auth/login?redirect=/invite?token=${params.token}`; - redirect(redirectUrl); - } - return ( <> - + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 48170da5..fe1fb31b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,19 +4,20 @@ import { Inter } from "next/font/google"; import { ThemeProvider } from "@app/providers/ThemeProvider"; import EnvProvider from "@app/providers/EnvProvider"; import { pullEnv } from "@app/lib/pullEnv"; -import ThemeDataProvider from "@app/providers/PrivateThemeDataProvider"; +import ThemeDataProvider from "@app/providers/ThemeDataProvider"; import SplashImage from "@app/components/private/SplashImage"; import SupportStatusProvider from "@app/providers/SupporterStatusProvider"; import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey"; import LicenseStatusProvider from "@app/providers/LicenseStatusProvider"; -import { GetLicenseStatusResponse } from "@server/routers/license"; +import { GetLicenseStatusResponse } from "@server/routers/license/types"; import LicenseViolation from "@app/components/LicenseViolation"; import { cache } from "react"; import { NextIntlClientProvider } from "next-intl"; import { getLocale } from "next-intl/server"; import { Toaster } from "@app/components/ui/toaster"; +import { build } from "@server/build"; export const metadata: Metadata = { title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, @@ -57,13 +58,22 @@ export default async function RootLayout({ supporterData.visible = res.data.data.visible; supporterData.tier = res.data.data.tier; - const licenseStatusRes = await cache( - async () => + let licenseStatus: GetLicenseStatusResponse; + if (build === "enterprise") { + const licenseStatusRes = await cache( + async () => await priv.get>( "/license/status" ) - )(); - const licenseStatus = licenseStatusRes.data.data; + )(); + licenseStatus = licenseStatusRes.data.data; + } else { + licenseStatus = { + isHostLicensed: false, + isLicenseValid: false, + hostId: "" + }; + } return ( diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 369de1d4..2d6aaec8 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -15,7 +15,8 @@ import { Globe, // Added from 'dev' branch MonitorUp, // Added from 'dev' branch Server, - Zap + Zap, + CreditCard } from "lucide-react"; export type SidebarNavSection = { @@ -54,7 +55,8 @@ export const orgNavSections = ( { title: "sidebarClients", href: "/{orgId}/settings/clients", - icon: + icon: , + isBeta: true } ] : []), @@ -63,7 +65,8 @@ export const orgNavSections = ( { title: "sidebarRemoteExitNodes", href: "/{orgId}/settings/remote-exit-nodes", - icon: + icon: , + showEE: true } ] : []), @@ -97,7 +100,8 @@ export const orgNavSections = ( { title: "sidebarIdentityProviders", href: "/{orgId}/settings/idp", - icon: + icon: , + showEE: true } ] : []), @@ -121,6 +125,15 @@ export const orgNavSections = ( { title: "sidebarBilling", href: "/{orgId}/settings/billing", + icon: + } + ] + : []), + ...(build == "saas" + ? [ + { + title: "sidebarEnterpriseLicenses", + href: "/{orgId}/settings/license", icon: } ] @@ -138,15 +151,6 @@ export const adminNavSections: SidebarNavSection[] = [ { heading: "Admin", items: [ - ...(build == "oss" - ? [ - { - title: "managedSelfhosted", - href: "/admin/managed", - icon: - } - ] - : []), { title: "sidebarAllUsers", href: "/admin/users", diff --git a/src/components/AdminIdpDataTable.tsx b/src/components/AdminIdpDataTable.tsx index 2efd9e7c..63a0b4bb 100644 --- a/src/components/AdminIdpDataTable.tsx +++ b/src/components/AdminIdpDataTable.tsx @@ -8,11 +8,15 @@ import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function IdpDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const router = useRouter(); const t = useTranslations(); @@ -29,6 +33,8 @@ export function IdpDataTable({ onAdd={() => { router.push("/admin/idp/create"); }} + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx index 8849ba25..2db1415e 100644 --- a/src/components/AdminIdpTable.tsx +++ b/src/components/AdminIdpTable.tsx @@ -39,8 +39,26 @@ export default function IdpTable({ idps }: Props) { const [selectedIdp, setSelectedIdp] = useState(null); const api = createApiClient(useEnvContext()); const router = useRouter(); + const [isRefreshing, setIsRefreshing] = useState(false); const t = useTranslations(); + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteIdp = async (idpId: number) => { try { await api.delete(`/idp/${idpId}`); @@ -194,7 +212,12 @@ export default function IdpTable({ idps }: Props) { /> )} - + ); } diff --git a/src/components/AdminUsersDataTable.tsx b/src/components/AdminUsersDataTable.tsx index fecba7fb..b0f38587 100644 --- a/src/components/AdminUsersDataTable.tsx +++ b/src/components/AdminUsersDataTable.tsx @@ -9,11 +9,15 @@ import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function UsersDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -26,6 +30,8 @@ export function UsersDataTable({ title={t('userServer')} searchPlaceholder={t('userSearch')} searchColumn="email" + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx index 8e75ff24..6bca4a74 100644 --- a/src/components/AdminUsersTable.tsx +++ b/src/components/AdminUsersTable.tsx @@ -46,6 +46,25 @@ export default function UsersTable({ users }: Props) { const api = createApiClient(useEnvContext()); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteUser = (id: string) => { api.delete(`/user/${id}`) .catch((e) => { @@ -168,7 +187,7 @@ export default function UsersTable({ users }: Props) {
{userRow.twoFactorEnabled || - userRow.twoFactorSetupRequested ? ( + userRow.twoFactorSetupRequested ? ( {t("enabled")} @@ -263,7 +282,12 @@ export default function UsersTable({ users }: Props) { /> )} - + ); } diff --git a/src/components/ApiKeysDataTable.tsx b/src/components/ApiKeysDataTable.tsx index 6ac8d68b..58ab9252 100644 --- a/src/components/ApiKeysDataTable.tsx +++ b/src/components/ApiKeysDataTable.tsx @@ -33,16 +33,20 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; addApiKey?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function ApiKeysDataTable({ addApiKey, columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); - + return ( ({ searchColumn="name" onAdd={addApiKey} addButtonText={t('apiKeysAdd')} + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx index 99094651..adc150cf 100644 --- a/src/components/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -43,6 +43,25 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteSite = (apiKeyId: string) => { api.delete(`/api-key/${apiKeyId}`) .catch((e) => { @@ -186,6 +205,8 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { addApiKey={() => { router.push(`/admin/api-keys/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/ClientsDataTable.tsx b/src/components/ClientsDataTable.tsx index 6242ba05..619f1fad 100644 --- a/src/components/ClientsDataTable.tsx +++ b/src/components/ClientsDataTable.tsx @@ -8,13 +8,17 @@ import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; addClient?: () => void; } export function ClientsDataTable({ columns, data, - addClient + addClient, + onRefresh, + isRefreshing }: DataTableProps) { return ( ({ searchPlaceholder="Search clients..." searchColumn="name" onAdd={addClient} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText="Add Client" /> ); diff --git a/src/components/ClientsTable.tsx b/src/components/ClientsTable.tsx index fc7c7c84..425b8395 100644 --- a/src/components/ClientsTable.tsx +++ b/src/components/ClientsTable.tsx @@ -25,6 +25,7 @@ import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; export type ClientRow = { id: number; @@ -53,6 +54,25 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { const [rows, setRows] = useState(clients); const api = createApiClient(useEnvContext()); + const [isRefreshing, setIsRefreshing] = useState(false); + const t = useTranslations(); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const deleteClient = (clientId: number) => { api.delete(`/client/${clientId}`) @@ -207,32 +227,32 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { return (
- - - - - - {/* */} - {/* */} - {/* View settings */} - {/* */} - {/* */} - { - setSelectedClient(clientRow); - setIsDeleteModalOpen(true); - }} - > - Delete - - - + + + + + + {/* */} + {/* */} + {/* View settings */} + {/* */} + {/* */} + { + setSelectedClient(clientRow); + setIsDeleteModalOpen(true); + }} + > + Delete + + + @@ -292,6 +312,8 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { addClient={() => { router.push(`/${orgId}/settings/clients/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx index 7ed31b62..b97e0eeb 100644 --- a/src/components/ContainersSelector.tsx +++ b/src/components/ContainersSelector.tsx @@ -81,9 +81,10 @@ export const ContainersSelector: FC = ({ <> diff --git a/src/components/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx index 77fdea9c..258aee49 100644 --- a/src/components/CreateDomainForm.tsx +++ b/src/components/CreateDomainForm.tsx @@ -119,12 +119,13 @@ export default function CreateDomainForm({ const t = useTranslations(); const { toast } = useToast(); const { org } = useOrgContext(); + const { env } = useEnvContext(); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { baseDomain: "", - type: build == "oss" ? "wildcard" : "ns" + type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns" } }); @@ -169,7 +170,7 @@ export default function CreateDomainForm({ }, [domainInputValue]); let domainOptions: any = []; - if (build == "enterprise" || build == "saas") { + if (build != "oss" && env.flags.usePangolinDns) { domainOptions = [ { id: "ns", @@ -182,7 +183,7 @@ export default function CreateDomainForm({ description: t("selectDomainTypeCnameDescription") } ]; - } else if (build == "oss") { + } else { domainOptions = [ { id: "wildcard", @@ -559,8 +560,7 @@ export default function CreateDomainForm({ )}
- {build == "saas" || - (build == "enterprise" && ( + {build != "oss" && env.flags.usePangolinDns && ( @@ -572,7 +572,7 @@ export default function CreateDomainForm({ )} - ))} + )} diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index dccef529..24f510dc 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -32,7 +32,7 @@ import { createApiClient, formatAxiosError } from "@/lib/api"; import { useEnvContext } from "@/hooks/useEnvContext"; import { toast } from "@/hooks/useToast"; import { ListDomainsResponse } from "@server/routers/domain/listDomains"; -import { CheckDomainAvailabilityResponse } from "@server/routers/domain/privateCheckDomainNamespaceAvailability"; +import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; import { AxiosResponse } from "axios"; import { cn } from "@/lib/cn"; import { useTranslations } from "next-intl"; @@ -93,6 +93,10 @@ export default function DomainPicker2({ const api = createApiClient({ env }); const t = useTranslations(); + if (!env.flags.usePangolinDns) { + hideFreeDomain = true; + } + const [subdomainInput, setSubdomainInput] = useState(""); const [selectedBaseDomain, setSelectedBaseDomain] = useState(null); diff --git a/src/components/GenerateLicenseKeyForm.tsx b/src/components/GenerateLicenseKeyForm.tsx new file mode 100644 index 00000000..17d593c0 --- /dev/null +++ b/src/components/GenerateLicenseKeyForm.tsx @@ -0,0 +1,1391 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { Checkbox } from "@app/components/ui/checkbox"; +import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AxiosResponse } from "axios"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types"; +import { useTranslations } from "next-intl"; +import React from "react"; +import { StrategySelect, StrategyOption } from "./StrategySelect"; +import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; +import { InfoIcon, Check } from "lucide-react"; +import { useUserContext } from "@app/hooks/useUserContext"; + +type FormProps = { + open: boolean; + setOpen: (open: boolean) => void; + orgId: string; + onGenerated?: () => void; +}; + +export default function GenerateLicenseKeyForm({ + open, + setOpen, + orgId, + onGenerated +}: FormProps) { + const t = useTranslations(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const { user } = useUserContext(); + + const [currentStep, setCurrentStep] = useState(1); + const [loading, setLoading] = useState(false); + const [generatedKey, setGeneratedKey] = useState(null); + const [formKey, setFormKey] = useState(0); + + // Step 1: Email & License Type + const step1Schema = z.object({ + email: z + .string() + .email(t("generateLicenseKeyForm.validation.emailRequired")), + useCaseType: z.enum(["personal", "business"], { + required_error: t( + "generateLicenseKeyForm.validation.useCaseTypeRequired" + ) + }) + }); + + // Step 2: Personal Information + const createStep2Schema = (useCaseType: string | undefined) => + z + .object({ + firstName: z + .string() + .min( + 1, + t("generateLicenseKeyForm.validation.firstNameRequired") + ), + lastName: z + .string() + .min( + 1, + t("generateLicenseKeyForm.validation.lastNameRequired") + ), + jobTitle: z.string().optional(), + primaryUse: z + .string() + .min( + 1, + t( + "generateLicenseKeyForm.validation.primaryUseRequired" + ) + ), + industry: z.string().optional(), + prospectiveUsers: z.coerce.number().optional(), + prospectiveSites: z.coerce.number().optional() + }) + .refine( + (data) => { + // If business use case, job title is required + if (useCaseType === "business") { + return data.jobTitle; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.jobTitleRequiredBusiness" + ), + path: ["jobTitle"] + } + ) + .refine( + (data) => { + // If business use case, industry is required + if (useCaseType === "business") { + return data.industry; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.industryRequiredBusiness" + ), + path: ["industry"] + } + ); + + // Step 3: Contact Information + const createStep3Schema = (useCaseType: string | undefined) => + z + .object({ + stateProvinceRegion: z + .string() + .min( + 1, + t( + "generateLicenseKeyForm.validation.stateProvinceRegionRequired" + ) + ), + postalZipCode: z + .string() + .min( + 1, + t( + "generateLicenseKeyForm.validation.postalZipCodeRequired" + ) + ), + country: z.string().optional(), + phoneNumber: z.string().optional(), + companyName: z.string().optional(), + countryOfResidence: z.string().optional(), + companyWebsite: z.string().optional(), + companyPhoneNumber: z.string().optional() + }) + .refine( + (data) => { + // If business use case, company name is required + if (useCaseType === "business") { + return data.companyName; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.companyNameRequiredBusiness" + ), + path: ["companyName"] + } + ) + .refine( + (data) => { + // If business use case, country of residence is required + if (useCaseType === "business") { + return data.countryOfResidence; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.countryOfResidenceRequiredBusiness" + ), + path: ["countryOfResidence"] + } + ) + .refine( + (data) => { + // If personal use case, country is required + if (useCaseType === "personal" && !data.country) { + return false; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.countryRequiredPersonal" + ), + path: ["country"] + } + ); + + // Step 4: Terms & Generate + const step4Schema = z.object({ + agreedToTerms: z + .boolean() + .refine( + (val) => val === true, + t("generateLicenseKeyForm.validation.agreeToTermsRequired") + ), + complianceConfirmed: z + .boolean() + .refine( + (val) => val === true, + t("generateLicenseKeyForm.validation.complianceConfirmationRequired") + ) + }); + + // Complete form schema for final submission with conditional validation + const createFormSchema = (useCaseType: string | undefined) => + z + .object({ + email: z.string().email("Please enter a valid email address"), + useCaseType: z.enum(["personal", "business"]), + firstName: z.string().min(1, "First name is required"), + lastName: z.string().min(1, "Last name is required"), + jobTitle: z.string().optional(), + primaryUse: z + .string() + .min(1, "Please describe your primary use"), + industry: z.string().optional(), + prospectiveUsers: z.coerce.number().optional(), + prospectiveSites: z.coerce.number().optional(), + stateProvinceRegion: z + .string() + .min( + 1, + t( + "generateLicenseKeyForm.validation.stateProvinceRegionRequired" + ) + ), + postalZipCode: z + .string() + .min( + 1, + t( + "generateLicenseKeyForm.validation.postalZipCodeRequired" + ) + ), + country: z.string().optional(), + phoneNumber: z.string().optional(), + companyName: z.string().optional(), + countryOfResidence: z.string().optional(), + companyWebsite: z.string().optional(), + companyPhoneNumber: z.string().optional(), + agreedToTerms: z + .boolean() + .refine( + (val) => val === true, + t( + "generateLicenseKeyForm.validation.agreeToTermsRequired" + ) + ), + complianceConfirmed: z + .boolean() + .refine( + (val) => val === true, + t("generateLicenseKeyForm.validation.complianceConfirmationRequired") + ) + }) + .refine( + (data) => { + // If business use case, job title is required + if (useCaseType === "business") { + return data.jobTitle; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.jobTitleRequiredBusiness" + ), + path: ["jobTitle"] + } + ) + .refine( + (data) => { + // If business use case, industry is required + if (useCaseType === "business") { + return data.industry; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.industryRequiredBusiness" + ), + path: ["industry"] + } + ) + .refine( + (data) => { + // If business use case, company name is required + if (useCaseType === "business") { + return data.companyName; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.companyNameRequiredBusiness" + ), + path: ["companyName"] + } + ) + .refine( + (data) => { + // If business use case, country of residence is required + if (useCaseType === "business") { + return data.countryOfResidence; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.countryOfResidenceRequiredBusiness" + ), + path: ["countryOfResidence"] + } + ) + .refine( + (data) => { + // If personal use case, country is required + if (useCaseType === "personal") { + return data.country; + } + return true; + }, + { + message: t( + "generateLicenseKeyForm.validation.countryRequiredPersonal" + ), + path: ["country"] + } + ); + + type FormData = z.infer>; + + // Base schema for form initialization (without conditional validation) + const baseFormSchema = z.object({ + email: z.string().email("Please enter a valid email address"), + useCaseType: z.enum(["personal", "business"]), + firstName: z.string().min(1, "First name is required"), + lastName: z.string().min(1, "Last name is required"), + jobTitle: z.string().optional(), + primaryUse: z.string().min(1, "Please describe your primary use"), + industry: z.string().optional(), + prospectiveUsers: z.coerce.number().optional(), + prospectiveSites: z.coerce.number().optional(), + stateProvinceRegion: z + .string() + .min(1, "State/Province/Region is required"), + postalZipCode: z.string().min(1, "Postal/ZIP Code is required"), + country: z.string().optional(), + phoneNumber: z.string().optional(), + companyName: z.string().optional(), + countryOfResidence: z.string().optional(), + companyWebsite: z.string().optional(), + companyPhoneNumber: z.string().optional(), + agreedToTerms: z + .boolean() + .refine( + (val) => val === true, + t("generateLicenseKeyForm.validation.agreeToTermsRequired") + ), + complianceConfirmed: z + .boolean() + .refine( + (val) => val === true, + t("generateLicenseKeyForm.validation.complianceConfirmationRequired") + ) + }); + + const form = useForm({ + resolver: zodResolver(baseFormSchema), + defaultValues: { + email: user?.email || "", + useCaseType: undefined, + firstName: "", + lastName: "", + jobTitle: "", + primaryUse: "", + industry: "", + prospectiveUsers: undefined, + prospectiveSites: undefined, + stateProvinceRegion: "", + postalZipCode: "", + country: "", + phoneNumber: "", + companyName: "", + countryOfResidence: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + } + }); + + const useCaseType = form.watch("useCaseType"); + const [previousUseCaseType, setPreviousUseCaseType] = useState< + string | undefined + >(undefined); + + // Reset form when use case type changes + React.useEffect(() => { + if ( + useCaseType !== previousUseCaseType && + useCaseType && + previousUseCaseType + ) { + // Reset fields that are specific to use case type + form.setValue("jobTitle", ""); + form.setValue("prospectiveUsers", undefined); + form.setValue("prospectiveSites", undefined); + form.setValue("companyName", ""); + form.setValue("countryOfResidence", ""); + form.setValue("companyWebsite", ""); + form.setValue("companyPhoneNumber", ""); + form.setValue("phoneNumber", ""); + form.setValue("country", ""); + + setPreviousUseCaseType(useCaseType); + } + }, [useCaseType, previousUseCaseType, form]); + + // Reset form when dialog opens + React.useEffect(() => { + if (open) { + form.reset({ + email: user?.email || "", + useCaseType: undefined, + firstName: "", + lastName: "", + jobTitle: "", + primaryUse: "", + industry: "", + prospectiveUsers: undefined, + prospectiveSites: undefined, + stateProvinceRegion: "", + postalZipCode: "", + country: "", + phoneNumber: "", + companyName: "", + countryOfResidence: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + }); + setCurrentStep(1); + setGeneratedKey(null); + setPreviousUseCaseType(undefined); + } + }, [open, form, user?.email]); + + const useCaseOptions: StrategyOption<"personal" | "business">[] = [ + { + id: "personal", + title: t("generateLicenseKeyForm.useCaseOptions.personal.title"), + description: ( +
+

+ {t( + "generateLicenseKeyForm.useCaseOptions.personal.description" + )} +

+
    +
  • + + + Home-lab enthusiasts and self-hosting hobbyists + +
  • +
  • + + + Personal projects, learning, and experimentation + +
  • +
  • + + + Individual developers and tech enthusiasts + +
  • +
+
+ ) + }, + { + id: "business", + title: t("generateLicenseKeyForm.useCaseOptions.business.title"), + description: ( +
+

+ {t( + "generateLicenseKeyForm.useCaseOptions.business.description" + )} +

+
    +
  • + + + Companies, startups, and organizations + +
  • +
  • + + + Professional services and client work + +
  • +
  • + + + Revenue-generating or commercial use cases + +
  • +
+
+ ) + } + ]; + + const steps = [ + { + title: t("generateLicenseKeyForm.steps.emailLicenseType.title"), + description: t( + "generateLicenseKeyForm.steps.emailLicenseType.description" + ) + }, + { + title: t("generateLicenseKeyForm.steps.personalInformation.title"), + description: t( + "generateLicenseKeyForm.steps.personalInformation.description" + ) + }, + { + title: t("generateLicenseKeyForm.steps.contactInformation.title"), + description: t( + "generateLicenseKeyForm.steps.contactInformation.description" + ) + }, + { + title: t("generateLicenseKeyForm.steps.termsGenerate.title"), + description: t( + "generateLicenseKeyForm.steps.termsGenerate.description" + ) + } + ]; + + const nextStep = async () => { + let isValid = false; + + try { + // Validate current step based on step number + switch (currentStep) { + case 1: + await step1Schema.parseAsync(form.getValues()); + isValid = true; + break; + case 2: + await createStep2Schema( + form.getValues("useCaseType") + ).parseAsync(form.getValues()); + isValid = true; + break; + case 3: + await createStep3Schema( + form.getValues("useCaseType") + ).parseAsync(form.getValues()); + isValid = true; + break; + case 4: + await step4Schema.parseAsync(form.getValues()); + isValid = true; + break; + default: + isValid = false; + } + } catch (error) { + if (error instanceof z.ZodError) { + // Set form errors for the current step fields + error.errors.forEach((err) => { + const fieldName = err.path[0] as keyof FormData; + form.setError(fieldName, { + type: "manual", + message: err.message + }); + }); + } + return; + } + + if (isValid && currentStep < steps.length) { + setCurrentStep(currentStep + 1); + } + }; + + const prevStep = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + } + }; + + const onSubmit = async (values: FormData) => { + // Validate with the dynamic schema before submission + try { + await createFormSchema(values.useCaseType).parseAsync(values); + } catch (error) { + if (error instanceof z.ZodError) { + // Set form errors for any validation failures + error.errors.forEach((err) => { + const fieldName = err.path[0] as keyof FormData; + form.setError(fieldName, { + type: "manual", + message: err.message + }); + }); + return; + } + } + + setLoading(true); + try { + const payload = { + email: values.email, + useCaseType: values.useCaseType, + personal: + values.useCaseType === "personal" + ? { + firstName: values.firstName, + lastName: values.lastName, + aboutYou: { + primaryUse: values.primaryUse + }, + personalInfo: { + stateProvinceRegion: + values.stateProvinceRegion, + postalZipCode: values.postalZipCode, + country: values.country, + phoneNumber: values.phoneNumber || "" + } + } + : undefined, + business: + values.useCaseType === "business" + ? { + firstName: values.firstName, + lastName: values.lastName, + jobTitle: values.jobTitle || "", + aboutYou: { + primaryUse: values.primaryUse, + industry: values.industry, + prospectiveUsers: + values.prospectiveUsers || undefined, + prospectiveSites: + values.prospectiveSites || undefined + }, + companyInfo: { + companyName: values.companyName || "", + countryOfResidence: + values.countryOfResidence || "", + stateProvinceRegion: + values.stateProvinceRegion, + postalZipCode: values.postalZipCode, + companyWebsite: values.companyWebsite || "", + companyPhoneNumber: + values.companyPhoneNumber || "" + } + } + : undefined, + consent: { + agreedToTerms: values.agreedToTerms, + acknowledgedPrivacyPolicy: values.agreedToTerms, + complianceConfirmed: values.complianceConfirmed + } + }; + + const response = await api.put< + AxiosResponse + >(`/org/${orgId}/license`, payload); + + if (response.data.data?.licenseKey?.licenseKey) { + setGeneratedKey(response.data.data.licenseKey.licenseKey); + onGenerated?.(); + toast({ + title: t("generateLicenseKeyForm.toasts.success.title"), + description: t( + "generateLicenseKeyForm.toasts.success.description" + ), + variant: "default" + }); + } + } catch (e) { + console.error(e); + toast({ + title: t("generateLicenseKeyForm.toasts.error.title"), + description: formatAxiosError( + e, + t("generateLicenseKeyForm.toasts.error.description") + ), + variant: "destructive" + }); + } + setLoading(false); + }; + + const handleClose = () => { + setOpen(false); + setCurrentStep(1); + setGeneratedKey(null); + setFormKey((prev) => prev + 1); // Force form reset by changing key + form.reset({ + email: user?.email || "", + useCaseType: undefined, + firstName: "", + lastName: "", + jobTitle: "", + primaryUse: "", + industry: "", + prospectiveUsers: undefined, + prospectiveSites: undefined, + stateProvinceRegion: "", + postalZipCode: "", + country: "", + phoneNumber: "", + companyName: "", + countryOfResidence: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + }); + }; + + const renderStepContent = () => { + switch (currentStep) { + case 1: + return ( +
+ + + + {t( + "generateLicenseKeyForm.alerts.commercialUseDisclosure.title" + )} + + + {t( + "generateLicenseKeyForm.alerts.commercialUseDisclosure.description" + ).split("Fossorial Commercial License Terms").map((part, index) => ( + + {part} + {index === 0 && ( + + Fossorial Commercial License Terms + + )} + + ))} + + + + ( + + + {t( + "generateLicenseKeyForm.form.useCaseQuestion" + )} + + { + field.onChange(value); + // Reset form when use case type changes + form.reset({ + email: user?.email || "", + useCaseType: value, + firstName: "", + lastName: "", + jobTitle: "", + primaryUse: "", + industry: "", + prospectiveUsers: undefined, + prospectiveSites: undefined, + stateProvinceRegion: "", + postalZipCode: "", + country: "", + phoneNumber: "", + companyName: "", + countryOfResidence: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + }); + }} + cols={2} + /> + + + )} + /> +
+ ); + + case 2: + return ( +
+
+ ( + + + {t( + "generateLicenseKeyForm.form.firstName" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.lastName" + )} + + + + + + + )} + /> +
+ + {useCaseType === "business" && ( + ( + + + {t( + "generateLicenseKeyForm.form.jobTitle" + )} + + + + + + + )} + /> + )} + +
+ ( + + + {t( + "generateLicenseKeyForm.form.primaryUseQuestion" + )} + + + + + + + )} + /> + + {useCaseType === "business" && ( + <> + ( + + + {t( + "generateLicenseKeyForm.form.industryQuestion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.prospectiveUsersQuestion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.prospectiveSitesQuestion" + )} + + + + + + + )} + /> + + )} +
+
+ ); + + case 3: + return ( +
+ {useCaseType === "business" && ( +
+ ( + + + {t( + "generateLicenseKeyForm.form.companyName" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.countryOfResidence" + )} + + + + + + + )} + /> + +
+ ( + + + {t( + "generateLicenseKeyForm.form.stateProvinceRegion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.postalZipCode" + )} + + + + + + + )} + /> +
+ +
+ ( + + + {t( + "generateLicenseKeyForm.form.companyWebsite" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.companyPhoneNumber" + )} + + + + + + + )} + /> +
+
+ )} + + {useCaseType === "personal" && ( +
+
+ ( + + + {t( + "generateLicenseKeyForm.form.stateProvinceRegion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.postalZipCode" + )} + + + + + + + )} + /> +
+ +
+ ( + + + {t( + "generateLicenseKeyForm.form.country" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.phoneNumberOptional" + )} + + + + + + + )} + /> +
+
+ )} +
+ ); + + case 4: + return ( +
+ ( + + + + +
+ +
+ {t("signUpTerms.IAgreeToThe")}{" "} + + {t( + "signUpTerms.termsOfService" + )}{" "} + + {t("signUpTerms.and")}{" "} + + {t( + "signUpTerms.privacyPolicy" + )} + +
+
+ +
+
+ )} + /> + + ( + + + + +
+ +
+ I confirm that I am in compliance with the{" "} + + Fossorial Commercial License + {" "} + and that reporting inaccurate information or misidentifying use of the product is a violation of the license. +
+
+ +
+
+ )} + /> +
+ ); + + default: + return null; + } + }; + + return ( + + + + {t("generateLicenseKey")} + + {steps[currentStep - 1]?.description} + + + +
+ {/* Progress indicator */} +
+ {steps.map((step, index) => ( +
+
+ {index + 1} +
+ + {step.title} + +
+ ))} +
+ + {generatedKey ? ( +
+ {useCaseType === "business" && ( + + + {t( + "generateLicenseKeyForm.alerts.trialPeriodInformation.title" + )} + + + {t( + "generateLicenseKeyForm.alerts.trialPeriodInformation.description" + )} + + + )} + + +
+ ) : ( +
+ + {renderStepContent()} +
+ + )} +
+
+ + + + + + {!generatedKey && ( + <> + {currentStep > 1 && ( + + )} + + {currentStep < steps.length ? ( + + ) : ( + + )} + + )} + +
+
+ ); +} diff --git a/src/components/GenerateLicenseKeysTable.tsx b/src/components/GenerateLicenseKeysTable.tsx new file mode 100644 index 00000000..835bb70d --- /dev/null +++ b/src/components/GenerateLicenseKeysTable.tsx @@ -0,0 +1,203 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { ColumnDef } from "@tanstack/react-table"; +import { Button } from "./ui/button"; +import { ArrowUpDown } from "lucide-react"; +import CopyToClipboard from "./CopyToClipboard"; +import { Badge } from "./ui/badge"; +import moment from "moment"; +import { DataTable } from "./ui/data-table"; +import { GeneratedLicenseKey } from "@server/routers/generatedLicense/types"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import GenerateLicenseKeyForm from "./GenerateLicenseKeyForm"; + +type GnerateLicenseKeysTableProps = { + licenseKeys: GeneratedLicenseKey[]; + orgId: string; +}; + +function obfuscateLicenseKey(key: string): string { + if (key.length <= 8) return key; + const firstPart = key.substring(0, 4); + const lastPart = key.substring(key.length - 4); + return `${firstPart}••••••••••••••••••••${lastPart}`; +} + +export default function GenerateLicenseKeysTable({ + licenseKeys, + orgId +}: GnerateLicenseKeysTableProps) { + const t = useTranslations(); + const router = useRouter(); + + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const [isRefreshing, setIsRefreshing] = useState(false); + const [showGenerateForm, setShowGenerateForm] = useState(false); + + const handleLicenseGenerated = () => { + // Refresh the data after license is generated + refreshData(); + }; + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "licenseKey", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const licenseKey = row.original.licenseKey; + return ( + + ); + } + }, + { + accessorKey: "instanceName", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return row.original.instanceName || "-"; + } + }, + { + accessorKey: "valid", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return row.original.isValid ? ( + {t("yes")} + ) : ( + {t("no")} + ); + } + }, + { + accessorKey: "type", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const tier = row.original.tier; + return tier === "enterprise" + ? t("licenseTierEnterprise") + : t("licenseTierPersonal"); + } + }, + { + accessorKey: "terminateAt", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const termianteAt = row.original.expiresAt; + return moment(termianteAt).format("lll"); + } + } + ]; + + return ( + <> + { + setShowGenerateForm(true); + }} + /> + + + + ); +} diff --git a/src/components/HealthCheckDialog.tsx b/src/components/HealthCheckDialog.tsx index 1942e1d8..6fa36a5b 100644 --- a/src/components/HealthCheckDialog.tsx +++ b/src/components/HealthCheckDialog.tsx @@ -107,7 +107,7 @@ export default function HealthCheckDialog({ useEffect(() => { if (!open) return; - + // Determine default scheme from target method const getDefaultScheme = () => { if (initialConfig?.hcScheme) { @@ -177,7 +177,7 @@ export default function HealthCheckDialog({ render={({ field }) => (
- + {t("enableHealthChecks")} @@ -210,7 +210,7 @@ export default function HealthCheckDialog({ name="hcScheme" render={({ field }) => ( - + {t("healthScheme")} ( - + {t( "healthyIntervalSeconds" )} @@ -425,7 +425,7 @@ export default function HealthCheckDialog({ name="hcUnhealthyInterval" render={({ field }) => ( - + {t( "unhealthyIntervalSeconds" )} @@ -460,7 +460,7 @@ export default function HealthCheckDialog({ name="hcTimeout" render={({ field }) => ( - + {t("timeoutSeconds")} @@ -499,7 +499,7 @@ export default function HealthCheckDialog({ name="hcStatus" render={({ field }) => ( - + {t("expectedResponseCodes")} @@ -541,7 +541,7 @@ export default function HealthCheckDialog({ name="hcHeaders" render={({ field }) => ( - + {t("customHeaders")} diff --git a/src/components/InvitationsDataTable.tsx b/src/components/InvitationsDataTable.tsx index 396a3c20..d73ad2ca 100644 --- a/src/components/InvitationsDataTable.tsx +++ b/src/components/InvitationsDataTable.tsx @@ -9,11 +9,15 @@ import { useTranslations } from 'next-intl'; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function InvitationsDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -26,6 +30,8 @@ export function InvitationsDataTable({ title={t('invite')} searchPlaceholder={t('inviteSearch')} searchColumn="email" + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/InvitationsTable.tsx b/src/components/InvitationsTable.tsx index a97220f2..900003d7 100644 --- a/src/components/InvitationsTable.tsx +++ b/src/components/InvitationsTable.tsx @@ -19,6 +19,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; import moment from "moment"; +import { useRouter } from "next/navigation"; export type InvitationRow = { id: string; @@ -45,6 +46,25 @@ export default function InvitationsTable({ const api = createApiClient(useEnvContext()); const { org } = useOrgContext(); + const router = useRouter(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -185,7 +205,12 @@ export default function InvitationsTable({ }} /> - + ); } diff --git a/src/components/InviteStatusCard.tsx b/src/components/InviteStatusCard.tsx index 6d7db4dc..d394bd57 100644 --- a/src/components/InviteStatusCard.tsx +++ b/src/components/InviteStatusCard.tsx @@ -1,47 +1,119 @@ "use client"; -import { createApiClient } from "@app/lib/api"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { Button } from "@app/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, - CardTitle, + CardTitle } from "@app/components/ui/card"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { XCircle } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; +import { useEffect, useState } from "react"; +import { AxiosResponse } from "axios"; +import { AcceptInviteResponse, GetUserResponse } from "@server/routers/user"; +import { Loader2 } from "lucide-react"; type InviteStatusCardProps = { - type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in"; - token: string; + user: GetUserResponse | null; + tokenParam: string; + inviteId: string; + inviteToken: string; email?: string; }; export default function InviteStatusCard({ - type, - token, + inviteId, email, + user, + tokenParam, + inviteToken }: InviteStatusCardProps) { const router = useRouter(); const api = createApiClient(useEnvContext()); const t = useTranslations(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [type, setType] = useState< + "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in" + >("rejected"); + + useEffect(() => { + async function init() { + let error = ""; + const res = await api + .post>(`/invite/accept`, { + inviteId, + token: inviteToken + }) + .catch((e) => { + error = formatAxiosError(e); + console.log("Error accepting invite:", error); + setError(error); + // console.error(e); + }); + + if (res && res.status === 200) { + router.push(`/${res.data.data.orgId}`); + return; + } + + function cardType() { + if (error.includes("Invite is not for this user")) { + return "wrong_user"; + } else if ( + error.includes( + "User does not exist. Please create an account first." + ) + ) { + return "user_does_not_exist"; + } else if ( + error.includes("You must be logged in to accept an invite") + ) { + return "not_logged_in"; + } else { + return "rejected"; + } + } + + const type = cardType(); + setType(type); + + if (!user && type === "user_does_not_exist") { + const redirectUrl = email + ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=/invite?token=${tokenParam}`; + router.push(redirectUrl); + } else if (!user && type === "not_logged_in") { + const redirectUrl = email + ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/login?redirect=/invite?token=${tokenParam}`; + router.push(redirectUrl); + } else { + setLoading(false); + } + } + + init(); + }, []); + async function goToLogin() { await api.post("/auth/logout", {}); - const redirectUrl = email - ? `/auth/login?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` - : `/auth/login?redirect=/invite?token=${token}`; + const redirectUrl = email + ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/login?redirect=/invite?token=${tokenParam}`; router.push(redirectUrl); } async function goToSignup() { await api.post("/auth/logout", {}); - const redirectUrl = email - ? `/auth/signup?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` - : `/auth/signup?redirect=/invite?token=${token}`; + const redirectUrl = email + ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=/invite?token=${tokenParam}`; router.push(redirectUrl); } @@ -50,35 +122,27 @@ export default function InviteStatusCard({ return (

- {t('inviteErrorNotValid')} + {t("inviteErrorNotValid")}

    -
  • {t('inviteErrorExpired')}
  • -
  • {t('inviteErrorRevoked')}
  • -
  • {t('inviteErrorTypo')}
  • +
  • {t("inviteErrorExpired")}
  • +
  • {t("inviteErrorRevoked")}
  • +
  • {t("inviteErrorTypo")}
); } else if (type === "wrong_user") { return (
-

- {t('inviteErrorUser')} -

-

- {t('inviteLoginUser')} -

+

{t("inviteErrorUser")}

+

{t("inviteLoginUser")}

); } else if (type === "user_does_not_exist") { return (
-

- {t('inviteErrorNoUser')} -

-

- {t('inviteCreateUser')} -

+

{t("inviteErrorNoUser")}

+

{t("inviteCreateUser")}

); } @@ -92,37 +156,43 @@ export default function InviteStatusCard({ router.push("/"); }} > - {t('goHome')} + {t("goHome")} ); } else if (type === "wrong_user") { return ( - + ); } else if (type === "user_does_not_exist") { - return ; + return ; } } return ( -
+
- {/*
-
*/} - {t('inviteNotAccepted')} + {loading ? t("checkingInvite") : t("inviteNotAccepted")}
- {renderBody()} + + {loading && ( +
+
+ + {t("loading")} +
+
+ )} + {!loading && renderBody()} +
- - {renderFooter()} - + {!loading && ( + + {renderFooter()} + + )}
); diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index dafa31a9..2e4ec4ce 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -6,7 +6,15 @@ import { OrgSelector } from "@app/components/OrgSelector"; import { cn } from "@app/lib/cn"; import { ListUserOrgsResponse } from "@server/routers/org"; import SupporterStatus from "@app/components/SupporterStatus"; -import { ExternalLink, Server, BookOpenText, Zap } from "lucide-react"; +import { + ExternalLink, + Server, + BookOpenText, + Zap, + CreditCard, + FileText, + TicketCheck +} from "lucide-react"; import { FaDiscord, FaGithub } from "react-icons/fa"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -22,6 +30,7 @@ import { TooltipTrigger } from "@app/components/ui/tooltip"; import { build } from "@server/build"; +import SidebarLicenseButton from "./SidebarLicenseButton"; interface LayoutSidebarProps { orgId?: string; @@ -119,8 +128,20 @@ export function LayoutSidebar({ />
+
- + {build === "enterprise" && ( +
+ +
+ )} + {build === "oss" && ( +
+ +
+ )} {!isSidebarCollapsed && (
{loadFooterLinks() ? ( @@ -159,9 +180,11 @@ export function LayoutSidebar({ rel="noopener noreferrer" className="flex items-center justify-center gap-1" > - {!isUnlocked() + {build === "oss" ? t("communityEdition") - : t("commercialEdition")} + : build === "enterprise" + ? t("enterpriseEdition") + : "Pangolin Cloud"}
diff --git a/src/components/LicenseKeysDataTable.tsx b/src/components/LicenseKeysDataTable.tsx index 1def304b..0f279ffd 100644 --- a/src/components/LicenseKeysDataTable.tsx +++ b/src/components/LicenseKeysDataTable.tsx @@ -6,9 +6,9 @@ import { Button } from "@app/components/ui/button"; import { Badge } from "@app/components/ui/badge"; import { LicenseKeyCache } from "@server/license/license"; import { ArrowUpDown } from "lucide-react"; -import moment from "moment"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { useTranslations } from "next-intl"; +import moment from "moment"; type LicenseKeysDataTableProps = { licenseKeys: LicenseKeyCache[]; @@ -28,7 +28,6 @@ export function LicenseKeysDataTable({ onDelete, onCreate }: LicenseKeysDataTableProps) { - const t = useTranslations(); const columns: ColumnDef[] = [ @@ -42,7 +41,7 @@ export function LicenseKeysDataTable({ column.toggleSorting(column.getIsSorted() === "asc") } > - {t('licenseKey')} + {t("licenseKey")} ); @@ -67,42 +66,21 @@ export function LicenseKeysDataTable({ column.toggleSorting(column.getIsSorted() === "asc") } > - {t('valid')} + {t("valid")} ); }, cell: ({ row }) => { - return row.original.valid ? t('yes') : t('no'); - } - }, - { - accessorKey: "type", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const type = row.original.type; - const label = - type === "SITES" ? t('sitesAdditional') : t('licenseHost'); - const variant = type === "SITES" ? "secondary" : "default"; return row.original.valid ? ( - {label} - ) : null; + {t("yes")} + ) : ( + {t("no")} + ); } }, { - accessorKey: "numSites", + accessorKey: "tier", header: ({ column }) => { return ( ); + }, + cell: ({ row }) => { + const tier = row.original.tier; + return tier === "enterprise" + ? t("licenseTierEnterprise") + : t("licenseTierPersonal"); + } + }, + { + accessorKey: "terminateAt", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const termianteAt = row.original.terminateAt; + return moment(termianteAt).format("lll"); } }, { @@ -125,7 +129,7 @@ export function LicenseKeysDataTable({ variant="secondary" onClick={() => onDelete(row.original)} > - {t('delete')} + {t("delete")}
) @@ -137,11 +141,11 @@ export function LicenseKeysDataTable({ columns={columns} data={licenseKeys} persistPageSize="licenseKeys-table" - title={t('licenseKeys')} - searchPlaceholder={t('licenseKeySearch')} + title={t("licenseKeys")} + searchPlaceholder={t("licenseKeySearch")} searchColumn="licenseKey" onAdd={onCreate} - addButtonText={t('licenseKeyAdd')} + addButtonText={t("licenseKeyAdd")} /> ); } diff --git a/src/components/LicenseViolation.tsx b/src/components/LicenseViolation.tsx index ea025e4c..c5f7504d 100644 --- a/src/components/LicenseViolation.tsx +++ b/src/components/LicenseViolation.tsx @@ -32,29 +32,5 @@ export default function LicenseViolation() { ); } - // Show usage violation banner - if ( - licenseStatus.maxSites && - licenseStatus.usedSites && - licenseStatus.usedSites > licenseStatus.maxSites - ) { - return ( -
-
-

- {t('componentsLicenseViolation', {usedSites: licenseStatus.usedSites, maxSites: licenseStatus.maxSites})} -

- -
-
- ); - } - return null; } diff --git a/src/components/OrgApiKeysDataTable.tsx b/src/components/OrgApiKeysDataTable.tsx index 773b2141..b6ad4bc3 100644 --- a/src/components/OrgApiKeysDataTable.tsx +++ b/src/components/OrgApiKeysDataTable.tsx @@ -8,12 +8,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; addApiKey?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function OrgApiKeysDataTable({ addApiKey, columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -27,6 +31,8 @@ export function OrgApiKeysDataTable({ searchPlaceholder={t('searchApiKeys')} searchColumn="name" onAdd={addApiKey} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('apiKeysAdd')} /> ); diff --git a/src/components/OrgApiKeysTable.tsx b/src/components/OrgApiKeysTable.tsx index 52030b66..d4c81e80 100644 --- a/src/components/OrgApiKeysTable.tsx +++ b/src/components/OrgApiKeysTable.tsx @@ -46,6 +46,24 @@ export default function OrgApiKeysTable({ const api = createApiClient(useEnvContext()); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const deleteSite = (apiKeyId: string) => { api.delete(`/org/${orgId}/api-key/${apiKeyId}`) @@ -195,6 +213,8 @@ export default function OrgApiKeysTable({ addApiKey={() => { router.push(`/${orgId}/settings/api-keys/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/PathMatchRenameModal.tsx b/src/components/PathMatchRenameModal.tsx index 574c9c70..bbb3dbc0 100644 --- a/src/components/PathMatchRenameModal.tsx +++ b/src/components/PathMatchRenameModal.tsx @@ -1,293 +1,335 @@ -import { Pencil } from "lucide-react"; +import { Settings } from "lucide-react"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue } from "@/components/ui/select"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@app/components/ui/dialog"; + import { Badge } from "@app/components/ui/badge"; import { Label } from "@app/components/ui/label"; import { useEffect, useState } from "react"; import { Input } from "./ui/input"; import { Button } from "./ui/button"; - +import { + Credenza, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle, + CredenzaTrigger +} from "./Credenza"; +import { useTranslations } from "next-intl"; export function PathMatchModal({ - value, - onChange, - trigger, + value, + onChange, + trigger }: { - value: { path: string | null; pathMatchType: string | null }; - onChange: (config: { path: string | null; pathMatchType: string | null }) => void; - trigger: React.ReactNode; + value: { path: string | null; pathMatchType: string | null }; + onChange: (config: { + path: string | null; + pathMatchType: string | null; + }) => void; + trigger: React.ReactNode; }) { - const [open, setOpen] = useState(false); - const [matchType, setMatchType] = useState(value?.pathMatchType || "prefix"); - const [path, setPath] = useState(value?.path || ""); + const t = useTranslations(); - useEffect(() => { - if (open) { - setMatchType(value?.pathMatchType || "prefix"); - setPath(value?.path || ""); - } - }, [open, value]); + const [open, setOpen] = useState(false); + const [matchType, setMatchType] = useState( + value?.pathMatchType || "prefix" + ); + const [path, setPath] = useState(value?.path || ""); - const handleSave = () => { - onChange({ pathMatchType: matchType as any, path: path.trim() }); - setOpen(false); - }; + useEffect(() => { + if (open) { + setMatchType(value?.pathMatchType || "prefix"); + setPath(value?.path || ""); + } + }, [open, value]); - const handleClear = () => { - onChange({ pathMatchType: null, path: null }); - setOpen(false); - }; + const handleSave = () => { + onChange({ pathMatchType: matchType as any, path: path.trim() }); + setOpen(false); + }; - const getPlaceholder = () => (matchType === "regex" ? "^/api/.*" : "/path"); + const handleClear = () => { + onChange({ pathMatchType: null, path: null }); + setOpen(false); + }; - const getHelpText = () => { - switch (matchType) { - case "prefix": - return "Example: /api matches /api, /api/users, etc."; - case "exact": - return "Example: /api matches only /api"; - case "regex": - return "Example: ^/api/.* matches /api/anything"; - default: - return ""; - } - }; + const getPlaceholder = () => (matchType === "regex" ? t("pathMatchRegexPlaceholder") : t("pathMatchDefaultPlaceholder")); - return ( - - {trigger} - - - Configure Path Matching - - Set up how incoming requests should be matched based on their path. - - -
-
- - -
-
- - setPath(e.target.value)} - /> -

{getHelpText()}

-
-
- - {value?.path && ( - - )} - - -
-
- ); + const getHelpText = () => { + switch (matchType) { + case "prefix": + return t("pathMatchPrefixHelp"); + case "exact": + return t("pathMatchExactHelp"); + case "regex": + return t("pathMatchRegexHelp"); + default: + return ""; + } + }; + + return ( + + {trigger} + + + {t("pathMatchModalTitle")} + + {t("pathMatchModalDescription")} + + +
+
+ + +
+
+ + setPath(e.target.value)} + /> +

+ {getHelpText()} +

+
+
+ + {/* {value?.path && ( + )} */} + + + +
+
+ ); } - export function PathRewriteModal({ - value, - onChange, - trigger, - disabled, + value, + onChange, + trigger, + disabled }: { - value: { rewritePath: string | null; rewritePathType: string | null }; - onChange: (config: { rewritePath: string | null; rewritePathType: string | null }) => void; - trigger: React.ReactNode; - disabled?: boolean; + value: { rewritePath: string | null; rewritePathType: string | null }; + onChange: (config: { + rewritePath: string | null; + rewritePathType: string | null; + }) => void; + trigger: React.ReactNode; + disabled?: boolean; }) { - const [open, setOpen] = useState(false); - const [rewriteType, setRewriteType] = useState(value?.rewritePathType || "prefix"); - const [rewritePath, setRewritePath] = useState(value?.rewritePath || ""); + const t = useTranslations(); + const [open, setOpen] = useState(false); + const [rewriteType, setRewriteType] = useState( + value?.rewritePathType || "prefix" + ); + const [rewritePath, setRewritePath] = useState(value?.rewritePath || ""); - useEffect(() => { - if (open) { - setRewriteType(value?.rewritePathType || "prefix"); - setRewritePath(value?.rewritePath || ""); - } - }, [open, value]); + useEffect(() => { + if (open) { + setRewriteType(value?.rewritePathType || "prefix"); + setRewritePath(value?.rewritePath || ""); + } + }, [open, value]); - const handleSave = () => { - onChange({ rewritePathType: rewriteType as any, rewritePath: rewritePath.trim() }); - setOpen(false); - }; + const handleSave = () => { + onChange({ + rewritePathType: rewriteType as any, + rewritePath: rewritePath.trim() + }); + setOpen(false); + }; - const handleClear = () => { - onChange({ rewritePathType: null, rewritePath: null }); - setOpen(false); - }; + const handleClear = () => { + onChange({ rewritePathType: null, rewritePath: null }); + setOpen(false); + }; - const getPlaceholder = () => { - switch (rewriteType) { - case "regex": - return "/new/$1"; - case "stripPrefix": - return ""; - default: - return "/new-path"; - } - }; + const getPlaceholder = () => { + switch (rewriteType) { + case "regex": + return t("pathRewriteRegexPlaceholder"); + case "stripPrefix": + return ""; + default: + return t("pathRewriteDefaultPlaceholder"); + } + }; - const getHelpText = () => { - switch (rewriteType) { - case "prefix": - return "Replace the matched prefix with this value"; - case "exact": - return "Replace the entire path with this value"; - case "regex": - return "Use capture groups like $1, $2 for replacement"; - case "stripPrefix": - return "Leave empty to strip prefix or provide new prefix"; - default: - return ""; - } - }; + const getHelpText = () => { + switch (rewriteType) { + case "prefix": + return t("pathRewritePrefixHelp"); + case "exact": + return t("pathRewriteExactHelp"); + case "regex": + return t("pathRewriteRegexHelp"); + case "stripPrefix": + return t("pathRewriteStripPrefixHelp"); + default: + return ""; + } + }; - return ( - - - {trigger} - - - - Configure Path Rewriting - - Transform the matched path before forwarding to the target. - - -
-
- - -
-
- - setRewritePath(e.target.value)} - /> -

{getHelpText()}

-
-
- - {value?.rewritePath && ( - - )} - - -
-
- ); + return ( + !disabled && setOpen(v)}> + {trigger} + + + {t("pathRewriteModalTitle")} + + {t("pathRewriteModalDescription")} + + +
+
+ + +
+
+ + setRewritePath(e.target.value)} + /> +

+ {getHelpText()} +

+
+
+ + {value?.rewritePath && ( + + )} + + +
+
+ ); } export function PathMatchDisplay({ - value, + value }: { - value: { path: string | null; pathMatchType: string | null }; + value: { path: string | null; pathMatchType: string | null }; }) { - if (!value?.path) return null; + const t = useTranslations(); + + if (!value?.path) return null; - const getTypeLabel = (type: string | null) => { - const labels: Record = { - prefix: "Prefix", - exact: "Exact", - regex: "Regex", + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: t("pathMatchPrefix"), + exact: t("pathMatchExact"), + regex: t("pathMatchRegex") + }; + return labels[type || ""] || type; }; - return labels[type || ""] || type; - }; - return ( -
- - {getTypeLabel(value.pathMatchType)} - - - {value.path} - - -
- ); + return ( +
+ + {getTypeLabel(value.pathMatchType)} + + + {value.path} + + +
+ ); } - export function PathRewriteDisplay({ - value, + value }: { - value: { rewritePath: string | null; rewritePathType: string | null }; + value: { rewritePath: string | null; rewritePathType: string | null }; }) { - if (!value?.rewritePath && value?.rewritePathType !== "stripPrefix") return null; + const t = useTranslations(); + + if (!value?.rewritePath && value?.rewritePathType !== "stripPrefix") + return null; - const getTypeLabel = (type: string | null) => { - const labels: Record = { - prefix: "Prefix", - exact: "Exact", - regex: "Regex", - stripPrefix: "Strip", + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: t("pathRewritePrefix"), + exact: t("pathRewriteExact"), + regex: t("pathRewriteRegex"), + stripPrefix: t("pathRewriteStrip") + }; + return labels[type || ""] || type; }; - return labels[type || ""] || type; - }; - return ( -
- - {getTypeLabel(value.rewritePathType)} - - - {value.rewritePath || (strip)} - - -
- ); + return ( +
+ + {getTypeLabel(value.rewritePathType)} + + + {value.rewritePath || ( + + ({t("pathRewriteStripLabel")}) + + )} + + +
+ ); } diff --git a/src/components/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx index 579b2c31..0a9939ff 100644 --- a/src/components/ResourceInfoBox.tsx +++ b/src/components/ResourceInfoBox.tsx @@ -13,12 +13,14 @@ import { import { useTranslations } from "next-intl"; import { build } from "@server/build"; import CertificateStatus from "@app/components/private/CertificateStatus"; -import { toUnicode } from 'punycode'; +import { toUnicode } from "punycode"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type ResourceInfoBoxType = {}; -export default function ResourceInfoBox({ }: ResourceInfoBoxType) { +export default function ResourceInfoBox({}: ResourceInfoBoxType) { const { resource, authInfo } = useResourceContext(); + const { env } = useEnvContext(); const t = useTranslations(); @@ -28,7 +30,9 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) { {/* 4 cols because of the certs */} - + {resource.http ? ( <> @@ -37,9 +41,10 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) { {authInfo.password || - authInfo.pincode || - authInfo.sso || - authInfo.whitelist ? ( + authInfo.pincode || + authInfo.sso || + authInfo.whitelist || + authInfo.headerAuth ? (
{t("protected")} @@ -126,25 +131,28 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) { {/* */} {/* */} {/* Certificate Status Column */} - {resource.http && resource.domainId && resource.fullDomain && build != "oss" && ( - - - {t("certificateStatus", { - defaultValue: "Certificate" - })} - - - - - - )} + {resource.http && + resource.domainId && + resource.fullDomain && + env.flags.usePangolinDns && ( + + + {t("certificateStatus", { + defaultValue: "Certificate" + })} + + + + + + )} {t("visibility")} diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index 7a645bc7..ad8b4fab 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -24,7 +24,8 @@ import { MoreHorizontal, ArrowUpRight, ShieldOff, - ShieldCheck + ShieldCheck, + RefreshCw } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -179,9 +180,27 @@ export default function ResourcesTable({ const [internalColumnFilters, setInternalColumnFilters] = useState([]); const [internalGlobalFilter, setInternalGlobalFilter] = useState([]); + const [isRefreshing, setIsRefreshing] = useState(false); const currentView = searchParams.get("view") || defaultView; + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + useEffect(() => { const fetchSites = async () => { try { @@ -753,7 +772,21 @@ export default function ResourcesTable({ )}
- {getActionButton()} +
+ +
+
+ {getActionButton()} +
diff --git a/src/components/RolesDataTable.tsx b/src/components/RolesDataTable.tsx index e88f9a2f..8043fc23 100644 --- a/src/components/RolesDataTable.tsx +++ b/src/components/RolesDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; createRole?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function RolesDataTable({ columns, data, - createRole + createRole, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function RolesDataTable({ searchPlaceholder={t('accessRolesSearch')} searchColumn="name" onAdd={createRole} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('accessRolesAdd')} /> ); diff --git a/src/components/RolesTable.tsx b/src/components/RolesTable.tsx index e92e71b6..292384a8 100644 --- a/src/components/RolesTable.tsx +++ b/src/components/RolesTable.tsx @@ -20,6 +20,7 @@ import DeleteRoleForm from "@app/components/DeleteRoleForm"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; export type RoleRow = Role; @@ -30,6 +31,7 @@ type RolesTableProps = { export default function UsersTable({ roles: r }: RolesTableProps) { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const router = useRouter(); const [roles, setRoles] = useState(r); @@ -40,6 +42,24 @@ export default function UsersTable({ roles: r }: RolesTableProps) { const { org } = useOrgContext(); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -116,6 +136,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) { createRole={() => { setIsCreateModalOpen(true); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/ShareLinksDataTable.tsx b/src/components/ShareLinksDataTable.tsx index dd266bcf..f2753bcf 100644 --- a/src/components/ShareLinksDataTable.tsx +++ b/src/components/ShareLinksDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; createShareLink?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function ShareLinksDataTable({ columns, data, - createShareLink + createShareLink, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function ShareLinksDataTable({ searchPlaceholder={t('shareSearch')} searchColumn="name" onAdd={createShareLink} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('shareCreate')} /> ); diff --git a/src/components/ShareLinksTable.tsx b/src/components/ShareLinksTable.tsx index 2943311f..ba9169c1 100644 --- a/src/components/ShareLinksTable.tsx +++ b/src/components/ShareLinksTable.tsx @@ -61,6 +61,25 @@ export default function ShareLinksTable({ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [rows, setRows] = useState(shareLinks); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + function formatLink(link: string) { return link.substring(0, 20) + "..." + link.substring(link.length - 20); } @@ -292,6 +311,8 @@ export default function ShareLinksTable({ createShareLink={() => { setIsCreateModalOpen(true); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/SidebarLicenseButton.tsx b/src/components/SidebarLicenseButton.tsx new file mode 100644 index 00000000..597b761a --- /dev/null +++ b/src/components/SidebarLicenseButton.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "@app/components/ui/tooltip"; +import { Button } from "./ui/button"; +import { TicketCheck } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import Link from "next/link"; + +interface SidebarLicenseButtonProps { + isCollapsed?: boolean; +} + +export default function SidebarLicenseButton({ + isCollapsed = false +}: SidebarLicenseButtonProps) { + const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext(); + + const t = useTranslations(); + + return ( + <> + {!licenseStatus?.isHostLicensed ? ( + isCollapsed ? ( + + + + + + + + + Enable Enterprise License + + + + ) : ( + + + + ) + ) : null} + + ); +} diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 7e8ad336..7aaebfff 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -14,12 +14,14 @@ import { TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { build } from "@server/build"; export type SidebarNavItem = { href: string; title: string; icon?: React.ReactNode; - showProfessional?: boolean; + showEE?: boolean; + isBeta?: boolean; }; export type SidebarNavSection = { @@ -71,7 +73,7 @@ export function SidebarNav({ isDisabled: boolean ) => { const tooltipText = - item.showProfessional && !isUnlocked() + item.showEE && !isUnlocked() ? `${t(item.title)} (${t("licenseBadge")})` : t(item.title); @@ -106,11 +108,24 @@ export function SidebarNav({ {!isCollapsed && ( <> {t(item.title)} - {item.showProfessional && !isUnlocked() && ( - - {t("licenseBadge")} + {item.isBeta && ( + + {t("beta")} )} + {build === "enterprise" && + item.showEE && + !isUnlocked() && ( + + {t("licenseBadge")} + + )} )} @@ -154,9 +169,11 @@ export function SidebarNav({ {section.items.map((item) => { const hydratedHref = hydrateHref(item.href); const isActive = pathname.startsWith(hydratedHref); - const isProfessional = - item.showProfessional && !isUnlocked(); - const isDisabled = disabled || isProfessional; + const isEE = + build === "enterprise" && + item.showEE && + !isUnlocked(); + const isDisabled = disabled || isEE; return renderNavItem( item, hydratedHref, diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 0c7c2b48..476fd336 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -303,7 +303,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { return (
{originalRow.exitNodeName} - {build == "saas" && originalRow.exitNodeName && + {build == "saas" && originalRow.exitNodeName && ['mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'].includes(originalRow.exitNodeName.toLowerCase()) && ( Cloud )} diff --git a/src/components/StrategySelect.tsx b/src/components/StrategySelect.tsx index 374d1bae..1b922cd1 100644 --- a/src/components/StrategySelect.tsx +++ b/src/components/StrategySelect.tsx @@ -5,9 +5,9 @@ import { RadioGroup, RadioGroupItem } from "./ui/radio-group"; import { useState, ReactNode } from "react"; export interface StrategyOption { -id: TValue; + id: TValue; title: string; - description: string; + description: string | ReactNode; disabled?: boolean; icon?: ReactNode; } @@ -59,16 +59,16 @@ export function StrategySelect({ disabled={option.disabled} className="absolute left-4 top-5 h-4 w-4 border-primary text-primary" /> -
+
{option.icon && ( -
+
{option.icon}
)}
{option.title}
- {option.description} + {typeof option.description === 'string' ? option.description : option.description}
diff --git a/src/components/UsersDataTable.tsx b/src/components/UsersDataTable.tsx index 1999b620..db12b697 100644 --- a/src/components/UsersDataTable.tsx +++ b/src/components/UsersDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; inviteUser?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function UsersDataTable({ columns, data, - inviteUser + inviteUser, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function UsersDataTable({ searchPlaceholder={t('accessUsersSearch')} searchColumn="email" onAdd={inviteUser} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('accessUserCreate')} /> ); diff --git a/src/components/UsersTable.tsx b/src/components/UsersTable.tsx index 2d4c122f..be8aea49 100644 --- a/src/components/UsersTable.tsx +++ b/src/components/UsersTable.tsx @@ -51,6 +51,24 @@ export default function UsersTable({ users: u }: UsersTableProps) { const { user, updateUser } = useUserContext(); const { org } = useOrgContext(); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -290,6 +308,8 @@ export default function UsersTable({ users: u }: UsersTableProps) { `/${org?.org.orgId}/settings/access/users/create` ); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx index 1917c609..95097a33 100644 --- a/src/components/private/AuthPageSettings.tsx +++ b/src/components/private/AuthPageSettings.tsx @@ -1,17 +1,5 @@ -/* - * 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. - */ - "use client"; + import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; @@ -43,7 +31,7 @@ import { SettingsSectionForm } from "@app/components/Settings"; import { useTranslations } from "next-intl"; -import { GetLoginPageResponse } from "@server/routers/private/loginPage"; +import { GetLoginPageResponse } from "@server/routers/loginPage/types"; import { ListDomainsResponse } from "@server/routers/domain"; import { DomainRow } from "@app/components/DomainsTable"; import { toUnicode } from "punycode"; @@ -63,8 +51,8 @@ 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 { usePrivateSubscriptionStatusContext } from "@app/hooks/privateUseSubscriptionStatusContext"; -import { TierId } from "@server/lib/private/billing/tiers"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; // Auth page form schema @@ -85,454 +73,477 @@ 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 AuthPageSettings = forwardRef( + ({ onSaveSuccess, onSaveError }, ref) => { + const { org } = useOrgContext(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + const t = useTranslations(); + const { env } = useEnvContext(); - const subscription = usePrivateSubscriptionStatusContext(); - const subscribed = subscription?.getTier() === TierId.STANDARD; + const subscription = useSubscriptionStatusContext(); - // 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( + 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); - const form = useForm({ - resolver: zodResolver(AuthPageFormSchema), - defaultValues: { - authPageDomainId: loginPage?.domainId || "", - authPageSubdomain: loginPage?.subdomain || "" - }, - mode: "onChange" - }); + 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]); + // 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(() => { - if (build !== "saas") { - return; - } - - const fetchLoginPage = async () => { - try { - const res = await api.get>( - `/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>( - `/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); - - try { - // Handle auth page domain - if (data.authPageDomainId) { - if (build !== "saas" || (build === "saas" && subscribed)) { - 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 { - // 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; - } - - // 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; - - 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< + // Fetch login page and domains data + useEffect(() => { + const fetchLoginPage = async () => { + try { + const res = await api.get< AxiosResponse >(`/org/${org?.org.orgId}/login-page`); - loginPageId = fetchRes.data.data.loginPageId; + 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); } + }; - await api.delete( - `/org/${org?.org.orgId}/login-page/${loginPageId}` - ); - setLoginPage(null); - setLoginPageExists(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(false); - router.refresh(); - onSaveSuccess?.(); - } catch (e) { - toast({ - variant: "destructive", - title: t("authPageErrorUpdate"), - description: formatAxiosError(e, t("authPageErrorUpdateMessage")) - }); - onSaveError?.(e); - } finally { - setLoadingSave(false); + setHasUnsavedChanges(true); } - } - return ( - <> - - - - {t("authPage")} - - - {t("authPageDescription")} - - - - {build === "saas" && !subscribed ? ( - - - {t("orgAuthPageDisabled")}{" "} - {t("subscriptionRequiredToUse")} - - - ) : null} + // Clear auth page domain + function clearAuthPageDomain() { + form.setValue("authPageDomainId", ""); + form.setValue("authPageSubdomain", ""); + setLoginPage(null); + setHasUnsavedChanges(true); + } - - {loadingLoginPage ? ( -
-
- {t("loading")} + async function onSubmit(data: AuthPageFormValues) { + setLoadingSave(true); + + try { + // Handle auth page domain + if (data.authPageDomainId) { + if ( + build === "enterprise" || + (build === "saas" && subscription?.subscribed) + ) { + 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 { + // 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; + } + + // 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; + + 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); + } + + setHasUnsavedChanges(false); + router.refresh(); + onSaveSuccess?.(); + } catch (e) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + e, + t("authPageErrorUpdateMessage") + ) + }); + onSaveError?.(e); + } finally { + setLoadingSave(false); + } + } + + 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" + ) : ( + + +
+ +
+ + + {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" ); - 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") && ( + })() + ) : ( + t("noDomainSet") + )} + +
- )} + {form.watch( + "authPageDomainId" + ) && ( + + )} +
-
- {/* Certificate Status */} - {(build !== "saas" || - (build === "saas" && subscribed)) && - loginPage?.domainId && - loginPage?.fullDomain && - !hasUnsavedChanges && ( - + {!form.watch( + "authPageDomainId" + ) && ( +
+ {t( + "addDomainToEnableCustomAuthPages" + )} +
)} - {!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); - }} - /> - - - - - - - - - - - ); -}); + {/* 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); + }} + /> + + + + + + + + + + + ); + } +); -AuthPageSettings.displayName = 'AuthPageSettings'; +AuthPageSettings.displayName = "AuthPageSettings"; -export default AuthPageSettings; \ No newline at end of file +export default AuthPageSettings; diff --git a/src/components/private/AutoProvisionConfigWidget.tsx b/src/components/private/AutoProvisionConfigWidget.tsx index 35800ccc..159ba01e 100644 --- a/src/components/private/AutoProvisionConfigWidget.tsx +++ b/src/components/private/AutoProvisionConfigWidget.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { diff --git a/src/components/private/CertificateStatus.tsx b/src/components/private/CertificateStatus.tsx index 1b872371..f1446806 100644 --- a/src/components/private/CertificateStatus.tsx +++ b/src/components/private/CertificateStatus.tsx @@ -1,21 +1,8 @@ -/* - * 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. - */ - "use client"; import { Button } from "@/components/ui/button"; import { RotateCw } from "lucide-react"; -import { useCertificate } from "@app/hooks/privateUseCertificate"; +import { useCertificate } from "@app/hooks/useCertificate"; import { useTranslations } from "next-intl"; type CertificateStatusProps = { diff --git a/src/components/private/IdpLoginButtons.tsx b/src/components/private/IdpLoginButtons.tsx index 6b3a2e0b..c2ec1f5b 100644 --- a/src/components/private/IdpLoginButtons.tsx +++ b/src/components/private/IdpLoginButtons.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { useEffect, useState } from "react"; diff --git a/src/components/private/OrgIdpDataTable.tsx b/src/components/private/OrgIdpDataTable.tsx index c98a6234..a7dc1850 100644 --- a/src/components/private/OrgIdpDataTable.tsx +++ b/src/components/private/OrgIdpDataTable.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/components/private/OrgIdpTable.tsx b/src/components/private/OrgIdpTable.tsx index f0e4d6c9..436904a0 100644 --- a/src/components/private/OrgIdpTable.tsx +++ b/src/components/private/OrgIdpTable.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/components/private/RegionSelector.tsx b/src/components/private/RegionSelector.tsx index 56dde743..f3928345 100644 --- a/src/components/private/RegionSelector.tsx +++ b/src/components/private/RegionSelector.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { useState } from "react"; diff --git a/src/components/private/SplashImage.tsx b/src/components/private/SplashImage.tsx index a2063692..c6ddc466 100644 --- a/src/components/private/SplashImage.tsx +++ b/src/components/private/SplashImage.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { useEnvContext } from "@app/hooks/useEnvContext"; diff --git a/src/components/private/ValidateSessionTransferToken.tsx b/src/components/private/ValidateSessionTransferToken.tsx index 116785d8..fcb6a026 100644 --- a/src/components/private/ValidateSessionTransferToken.tsx +++ b/src/components/private/ValidateSessionTransferToken.tsx @@ -1,16 +1,3 @@ -/* - * 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. - */ - "use client"; import { useEnvContext } from "@app/hooks/useEnvContext"; @@ -21,7 +8,7 @@ import { useEffect, useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertCircle } from "lucide-react"; import { useTranslations } from "next-intl"; -import { TransferSessionResponse } from "@server/routers/auth/privateTransferSession"; +import { TransferSessionResponse } from "@server/routers/auth/types"; type ValidateSessionTransferTokenParams = { token: string; diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index dcfc646d..83e84073 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -10,7 +10,7 @@ const Textarea = React.forwardRef( return (