Compare commits

..

22 Commits

Author SHA1 Message Date
Owen
9b0e7b381c Fix error to gerbil 2026-03-11 15:49:03 -07:00
Owen
90afe5a7ac Log errors 2026-03-11 15:42:40 -07:00
Owen
b24de85157 Handle gerbil rejecting 0
Closes #2605
2026-03-11 15:06:26 -07:00
Owen
eda43dffe1 Fix not pulling wildcard cert updates 2026-03-11 15:06:26 -07:00
Owen
82c9a1eb70 Add demo link 2026-03-11 15:06:26 -07:00
Owen
1cc5f59f66 Implement email and ip banning 2026-03-11 11:42:31 -07:00
Owen
4e2d88efdd Add some logging to debug 2026-03-11 11:42:28 -07:00
Owen
4975cabb2c Use native drizzle count 2026-03-11 11:42:28 -07:00
Owen
225591094f Clean up 2026-03-11 11:42:28 -07:00
Owen
82f88f2cd3 Reorder delete 2026-03-11 11:42:28 -07:00
Owen
99e6bd31b6 Bump dompurify 2026-03-10 16:47:03 -07:00
Owen
5c50590d7b Bump esbuild 2026-03-10 16:47:03 -07:00
dependabot[bot]
42b9d5158d Bump the prod-minor-updates group across 1 directory with 10 updates
Bumps the prod-minor-updates group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.989.0` | `3.1003.0` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.2.1` | `8.3.0` |
| [ioredis](https://github.com/luin/ioredis) | `5.9.3` | `5.10.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.563.0` | `0.577.0` |
| [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) | `8.19.0` | `8.20.0` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.26.0` | `5.28.0` |
| [react-day-picker](https://github.com/gpbl/react-day-picker) | `9.13.2` | `9.14.0` |
| [react-icons](https://github.com/react-icons/react-icons) | `5.5.0` | `5.6.0` |
| reodotdev | `1.0.0` | `1.1.0` |
| [stripe](https://github.com/stripe/stripe-node) | `20.3.1` | `20.4.0` |



Updates `@aws-sdk/client-s3` from 3.989.0 to 3.1003.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1003.0/clients/client-s3)

Updates `express-rate-limit` from 8.2.1 to 8.3.0
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.2.1...v8.3.0)

Updates `ioredis` from 5.9.3 to 5.10.0
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/redis/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/luin/ioredis/compare/v5.9.3...v5.10.0)

Updates `lucide-react` from 0.563.0 to 0.577.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.577.0/packages/lucide-react)

Updates `pg` from 8.19.0 to 8.20.0
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.20.0/packages/pg)

Updates `posthog-node` from 5.26.0 to 5.28.0
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.28.0/packages/node)

Updates `react-day-picker` from 9.13.2 to 9.14.0
- [Release notes](https://github.com/gpbl/react-day-picker/releases)
- [Changelog](https://github.com/gpbl/react-day-picker/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gpbl/react-day-picker/compare/v9.13.2...v9.14.0)

Updates `react-icons` from 5.5.0 to 5.6.0
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v5.5.0...v5.6.0)

Updates `reodotdev` from 1.0.0 to 1.1.0

Updates `stripe` from 20.3.1 to 20.4.0
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v20.3.1...v20.4.0)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1003.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: express-rate-limit
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: ioredis
  dependency-version: 5.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.577.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: pg
  dependency-version: 8.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: posthog-node
  dependency-version: 5.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-day-picker
  dependency-version: 9.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-icons
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: reodotdev
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: stripe
  dependency-version: 20.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 16:30:13 -07:00
dependabot[bot]
2ba225299e Bump the dev-minor-updates group across 1 directory with 5 updates
Bumps the dev-minor-updates group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) | `1.52.0` | `1.53.0` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.1.18` | `4.2.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.3` | `25.3.5` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.18` | `4.2.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.55.0` | `8.56.1` |



Updates `@dotenvx/dotenvx` from 1.52.0 to 1.53.0
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v1.52.0...v1.53.0)

Updates `@tailwindcss/postcss` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/@tailwindcss-postcss)

Updates `@types/node` from 25.2.3 to 25.3.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `tailwindcss` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/tailwindcss)

Updates `typescript-eslint` from 8.55.0 to 8.56.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.53.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@types/node"
  dependency-version: 25.3.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: tailwindcss
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: typescript-eslint
  dependency-version: 8.56.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 16:30:00 -07:00
Shreyas Papinwar
fa0818d3fa fix: ensure Credenza dialog max-height 2026-03-10 10:07:36 -07:00
Owen Schwartz
91b7ceb2cf Merge pull request #2603 from Fizza-Mukhtar/fix/prevent-dashboard-domain-conflict-2595
fix: prevent resource from being created with dashboard's domain to avoid redirect loop
2026-03-07 21:15:53 -08:00
Owen Schwartz
d4b830b9bb Merge pull request #2613 from fosrl/dependabot/npm_and_yarn/multi-43b302174d
Bump fast-xml-parser and @aws-sdk/xml-builder
2026-03-06 14:16:27 -08:00
dependabot[bot]
14d6ff25a7 Bump fast-xml-parser and @aws-sdk/xml-builder
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) and [@aws-sdk/xml-builder](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/xml-builder). These dependencies needed to be updated together.

Updates `fast-xml-parser` from 5.3.6 to 5.4.1
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.6...v5.4.1)

Updates `@aws-sdk/xml-builder` from 3.972.5 to 3.972.10
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/xml-builder/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/xml-builder)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-version: 5.4.1
  dependency-type: indirect
- dependency-name: "@aws-sdk/xml-builder"
  dependency-version: 3.972.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 22:14:56 +00:00
Owen Schwartz
1f62f305ce Merge pull request #2611 from fosrl/dependabot/npm_and_yarn/express-rate-limit-8.2.2
Bump express-rate-limit from 8.2.1 to 8.2.2
2026-03-06 14:13:32 -08:00
dependabot[bot]
cebcf3e337 Bump express-rate-limit from 8.2.1 to 8.2.2
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.2.2.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.2.1...v8.2.2)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-version: 8.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:40:41 +00:00
Fizza-Mukhtar
4cfcc64481 fix: use config instead of process.env for dashboard URL check 2026-03-05 01:07:30 -08:00
Fizza-Mukhtar
1a2069a6d9 fix: prevent resource creation with dashboard domain to avoid redirect loop 2026-03-05 00:39:03 -08:00
35 changed files with 1578 additions and 3265 deletions

View File

@@ -2343,8 +2343,8 @@
"logRetentionEndOfFollowingYear": "End of following year", "logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization", "actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization", "accessLogsDescription": "View access auth requests for resources in this organization",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
"certResolver": "Certificate Resolver", "certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.", "certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver", "selectCertResolver": "Select Certificate Resolver",

3946
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "8.4.1", "@asteasolutions/zod-to-openapi": "8.4.1",
"@aws-sdk/client-s3": "3.989.0", "@aws-sdk/client-s3": "3.1004.0",
"@faker-js/faker": "10.3.0", "@faker-js/faker": "10.3.0",
"@headlessui/react": "2.2.9", "@headlessui/react": "2.2.9",
"@hookform/resolvers": "5.2.2", "@hookform/resolvers": "5.2.2",
@@ -80,16 +80,16 @@
"d3": "7.9.0", "d3": "7.9.0",
"drizzle-orm": "0.45.1", "drizzle-orm": "0.45.1",
"express": "5.2.1", "express": "5.2.1",
"express-rate-limit": "8.2.1", "express-rate-limit": "8.3.0",
"glob": "13.0.6", "glob": "13.0.6",
"helmet": "8.1.0", "helmet": "8.1.0",
"http-errors": "2.0.1", "http-errors": "2.0.1",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "5.9.3", "ioredis": "5.10.0",
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
"lucide-react": "0.563.0", "lucide-react": "0.577.0",
"maxmind": "5.0.5", "maxmind": "5.0.5",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.5.12", "next": "15.5.12",
@@ -99,20 +99,21 @@
"node-cache": "5.1.2", "node-cache": "5.1.2",
"nodemailer": "8.0.1", "nodemailer": "8.0.1",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.19.0", "pg": "8.20.0",
"posthog-node": "5.26.0", "posthog-node": "5.28.0",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.4", "react": "19.2.4",
"react-day-picker": "9.13.2", "react-day-picker": "9.14.0",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"react-easy-sort": "1.8.0", "react-easy-sort": "1.8.0",
"react-hook-form": "7.71.2", "react-hook-form": "7.71.2",
"react-icons": "5.5.0", "react-icons": "5.6.0",
"recharts": "2.15.4", "recharts": "2.15.4",
"reodotdev": "1.0.0", "reodotdev": "1.1.0",
"resend": "6.9.2",
"semver": "7.7.4", "semver": "7.7.4",
"sshpk": "^1.18.0", "sshpk": "^1.18.0",
"stripe": "20.3.1", "stripe": "20.4.1",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.5.0", "tailwind-merge": "3.5.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
@@ -130,10 +131,10 @@
"zod-validation-error": "5.0.0" "zod-validation-error": "5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.52.0", "@dotenvx/dotenvx": "1.54.1",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/preview-server": "5.2.8", "@react-email/preview-server": "5.2.8",
"@tailwindcss/postcss": "4.1.18", "@tailwindcss/postcss": "4.2.1",
"@tanstack/react-query-devtools": "5.91.3", "@tanstack/react-query-devtools": "5.91.3",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
@@ -145,10 +146,10 @@
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "25.2.3", "@types/node": "25.3.5",
"@types/nodemailer": "7.0.11", "@types/nodemailer": "7.0.11",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/pg": "8.16.0", "@types/pg": "8.18.0",
"@types/react": "19.2.14", "@types/react": "19.2.14",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
@@ -166,10 +167,14 @@
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.8.1", "prettier": "3.8.1",
"react-email": "5.2.8", "react-email": "5.2.8",
"tailwindcss": "4.1.18", "tailwindcss": "4.2.1",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.55.0" "typescript-eslint": "8.56.1"
},
"overrides": {
"esbuild": "0.27.3",
"dompurify": "3.3.2"
} }
} }

View File

@@ -328,6 +328,14 @@ export const approvals = pgTable("approvals", {
.notNull() .notNull()
}); });
export const bannedEmails = pgTable("bannedEmails", {
email: varchar("email", { length: 255 }).primaryKey(),
});
export const bannedIps = pgTable("bannedIps", {
ip: varchar("ip", { length: 255 }).primaryKey(),
});
export type Approval = InferSelectModel<typeof approvals>; export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>; export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>; export type Account = InferSelectModel<typeof account>;

View File

@@ -720,7 +720,6 @@ export const clientSitesAssociationsCache = pgTable(
.notNull(), .notNull(),
siteId: integer("siteId").notNull(), siteId: integer("siteId").notNull(),
isRelayed: boolean("isRelayed").notNull().default(false), isRelayed: boolean("isRelayed").notNull().default(false),
isJitMode: boolean("isJitMode").notNull().default(false),
endpoint: varchar("endpoint"), endpoint: varchar("endpoint"),
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
} }

View File

@@ -318,6 +318,15 @@ export const approvals = sqliteTable("approvals", {
.notNull() .notNull()
}); });
export const bannedEmails = sqliteTable("bannedEmails", {
email: text("email").primaryKey()
});
export const bannedIps = sqliteTable("bannedIps", {
ip: text("ip").primaryKey()
});
export type Approval = InferSelectModel<typeof approvals>; export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>; export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>; export type Account = InferSelectModel<typeof account>;

View File

@@ -409,9 +409,6 @@ export const clientSitesAssociationsCache = sqliteTable(
isRelayed: integer("isRelayed", { mode: "boolean" }) isRelayed: integer("isRelayed", { mode: "boolean" })
.notNull() .notNull()
.default(false), .default(false),
isJitMode: integer("isJitMode", { mode: "boolean" })
.notNull()
.default(false),
endpoint: text("endpoint"), endpoint: text("endpoint"),
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
} }

View File

@@ -85,9 +85,7 @@ export async function deleteOrgById(
deletedNewtIds.push(deletedNewt.newtId); deletedNewtIds.push(deletedNewt.newtId);
await trx await trx
.delete(newtSessions) .delete(newtSessions)
.where( .where(eq(newtSessions.newtId, deletedNewt.newtId));
eq(newtSessions.newtId, deletedNewt.newtId)
);
} }
} }
} }
@@ -121,33 +119,38 @@ export async function deleteOrgById(
eq(clientSitesAssociationsCache.clientId, client.clientId) eq(clientSitesAssociationsCache.clientId, client.clientId)
); );
} }
await trx.delete(resources).where(eq(resources.orgId, orgId));
const allOrgDomains = await trx const allOrgDomains = await trx
.select() .select()
.from(orgDomains) .from(orgDomains)
.innerJoin(domains, eq(domains.domainId, orgDomains.domainId)) .innerJoin(domains, eq(orgDomains.domainId, domains.domainId))
.where( .where(
and( and(
eq(orgDomains.orgId, orgId), eq(orgDomains.orgId, orgId),
eq(domains.configManaged, false) eq(domains.configManaged, false)
) )
); );
logger.info(`Found ${allOrgDomains.length} domains to delete`);
const domainIdsToDelete: string[] = []; const domainIdsToDelete: string[] = [];
for (const orgDomain of allOrgDomains) { for (const orgDomain of allOrgDomains) {
const domainId = orgDomain.domains.domainId; const domainId = orgDomain.domains.domainId;
const orgCount = await trx const [orgCount] = await trx
.select({ count: sql<number>`count(*)` }) .select({ count: count() })
.from(orgDomains) .from(orgDomains)
.where(eq(orgDomains.domainId, domainId)); .where(eq(orgDomains.domainId, domainId));
if (orgCount[0].count === 1) { logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`);
if (orgCount.count === 1) {
domainIdsToDelete.push(domainId); domainIdsToDelete.push(domainId);
} }
} }
logger.info(`Found ${domainIdsToDelete.length} domains to delete`);
if (domainIdsToDelete.length > 0) { if (domainIdsToDelete.length > 0) {
await trx await trx
.delete(domains) .delete(domains)
.where(inArray(domains.domainId, domainIdsToDelete)); .where(inArray(domains.domainId, domainIdsToDelete));
} }
await trx.delete(resources).where(eq(resources.orgId, orgId));
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
@@ -231,15 +234,13 @@ export function sendTerminationMessages(result: DeleteOrgByIdResult): void {
); );
} }
for (const olmId of result.olmsToTerminate) { for (const olmId of result.olmsToTerminate) {
sendTerminateClient( sendTerminateClient(0, OlmErrorCodes.TERMINATED_REKEYED, olmId).catch(
0, (error) => {
OlmErrorCodes.TERMINATED_REKEYED, logger.error(
olmId "Failed to send termination message to olm:",
).catch((error) => { error
logger.error( );
"Failed to send termination message to olm:", }
error );
);
});
} }
} }

View File

@@ -571,7 +571,7 @@ export async function updateClientSiteDestinations(
destinations: [ destinations: [
{ {
destinationIP: site.sites.subnet.split("/")[0], destinationIP: site.sites.subnet.split("/")[0],
destinationPort: site.sites.listenPort || 0 destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
} }
] ]
}; };
@@ -579,7 +579,7 @@ export async function updateClientSiteDestinations(
// add to the existing destinations // add to the existing destinations
destinations.destinations.push({ destinations.destinations.push({
destinationIP: site.sites.subnet.split("/")[0], destinationIP: site.sites.subnet.split("/")[0],
destinationPort: site.sites.listenPort || 0 destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
}); });
} }

16
server/lib/resend.ts Normal file
View File

@@ -0,0 +1,16 @@
export enum AudienceIds {
SignUps = "",
Subscribed = "",
Churned = "",
Newsletter = ""
}
let resend;
export default resend;
export async function moveEmailToAudience(
email: string,
audienceId: AudienceIds
) {
return;
}

View File

@@ -218,10 +218,11 @@ export class TraefikConfigManager {
return true; return true;
} }
// Fetch if it's been more than 24 hours (for renewals)
const dayInMs = 24 * 60 * 60 * 1000; const dayInMs = 24 * 60 * 60 * 1000;
const timeSinceLastFetch = const timeSinceLastFetch =
Date.now() - this.lastCertificateFetch.getTime(); Date.now() - this.lastCertificateFetch.getTime();
// Fetch if it's been more than 24 hours (daily routine check)
if (timeSinceLastFetch > dayInMs) { if (timeSinceLastFetch > dayInMs) {
logger.info("Fetching certificates due to 24-hour renewal check"); logger.info("Fetching certificates due to 24-hour renewal check");
return true; return true;
@@ -265,7 +266,7 @@ export class TraefikConfigManager {
return true; return true;
} }
// Check if any local certificates are missing or appear to be outdated // Check if any local certificates are missing (needs immediate fetch)
for (const domain of domainsNeedingCerts) { for (const domain of domainsNeedingCerts) {
const localState = this.lastLocalCertificateState.get(domain); const localState = this.lastLocalCertificateState.get(domain);
if (!localState || !localState.exists) { if (!localState || !localState.exists) {
@@ -274,17 +275,55 @@ export class TraefikConfigManager {
); );
return true; return true;
} }
}
// Check if certificate is expiring soon (within 30 days) // For expiry checks, throttle to every 6 hours to avoid querying the
if (localState.expiresAt) { // API/DB on every monitor loop. The certificate-service renews certs
const nowInSeconds = Math.floor(Date.now() / 1000); // 45 days before expiry, so checking every 6 hours is plenty frequent
const secondsUntilExpiry = localState.expiresAt - nowInSeconds; // to pick up renewed certs promptly.
const daysUntilExpiry = secondsUntilExpiry / (60 * 60 * 24); const renewalCheckIntervalMs = 6 * 60 * 60 * 1000; // 6 hours
if (daysUntilExpiry < 30) { if (timeSinceLastFetch > renewalCheckIntervalMs) {
logger.info( // Check non-wildcard certs for expiry (within 45 days to match
`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)` // the server-side renewal window in certificate-service)
); for (const domain of domainsNeedingCerts) {
return true; const localState =
this.lastLocalCertificateState.get(domain);
if (localState?.expiresAt) {
const nowInSeconds = Math.floor(Date.now() / 1000);
const secondsUntilExpiry =
localState.expiresAt - nowInSeconds;
const daysUntilExpiry =
secondsUntilExpiry / (60 * 60 * 24);
if (daysUntilExpiry < 45) {
logger.info(
`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)`
);
return true;
}
}
}
// Also check wildcard certificates for expiry. These are not
// included in domainsNeedingCerts since their subdomains are
// filtered out, so we must check them separately.
for (const [certDomain, state] of this
.lastLocalCertificateState) {
if (
state.exists &&
state.wildcard &&
state.expiresAt
) {
const nowInSeconds = Math.floor(Date.now() / 1000);
const secondsUntilExpiry =
state.expiresAt - nowInSeconds;
const daysUntilExpiry =
secondsUntilExpiry / (60 * 60 * 24);
if (daysUntilExpiry < 45) {
logger.info(
`Fetching certificates due to upcoming expiry for wildcard cert ${certDomain} (${Math.round(daysUntilExpiry)} days remaining)`
);
return true;
}
} }
} }
} }
@@ -361,6 +400,32 @@ export class TraefikConfigManager {
} }
} }
// Also include wildcard cert base domains that are
// expiring or expired so they get re-fetched even though
// their subdomains were filtered out above.
for (const [certDomain, state] of this
.lastLocalCertificateState) {
if (
state.exists &&
state.wildcard &&
state.expiresAt
) {
const nowInSeconds = Math.floor(
Date.now() / 1000
);
const secondsUntilExpiry =
state.expiresAt - nowInSeconds;
const daysUntilExpiry =
secondsUntilExpiry / (60 * 60 * 24);
if (daysUntilExpiry < 45) {
domainsToFetch.add(certDomain);
logger.info(
`Including expiring wildcard cert domain ${certDomain} in fetch (${Math.round(daysUntilExpiry)} days remaining)`
);
}
}
}
if (domainsToFetch.size > 0) { if (domainsToFetch.size > 0) {
// Get valid certificates for domains not covered by wildcards // Get valid certificates for domains not covered by wildcards
validCertificates = validCertificates =

View File

@@ -38,6 +38,10 @@ export const privateConfigSchema = z.object({
.string() .string()
.optional() .optional()
.transform(getEnvOrYaml("SERVER_ENCRYPTION_KEY")), .transform(getEnvOrYaml("SERVER_ENCRYPTION_KEY")),
resend_api_key: z
.string()
.optional()
.transform(getEnvOrYaml("RESEND_API_KEY")),
reo_client_id: z reo_client_id: z
.string() .string()
.optional() .optional()

View File

@@ -0,0 +1,127 @@
/*
* 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 { Resend } from "resend";
import privateConfig from "#private/lib/config";
import logger from "@server/logger";
export enum AudienceIds {
SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a",
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"
}
const resend = new Resend(
privateConfig.getRawPrivateConfig().server.resend_api_key || "missing"
);
export default resend;
export async function moveEmailToAudience(
email: string,
audienceId: AudienceIds
) {
if (process.env.ENVIRONMENT !== "prod") {
logger.debug(
`Skipping moving email ${email} to audience ${audienceId} in non-prod environment`
);
return;
}
const { error, data } = await retryWithBackoff(async () => {
const { data, error } = await resend.contacts.create({
email,
unsubscribed: false,
audienceId
});
if (error) {
throw new Error(
`Error adding email ${email} to audience ${audienceId}: ${error}`
);
}
return { error, data };
});
if (error) {
logger.error(
`Error adding email ${email} to audience ${audienceId}: ${error}`
);
return;
}
if (data) {
logger.debug(
`Added email ${email} to audience ${audienceId} with contact ID ${data.id}`
);
}
const otherAudiences = Object.values(AudienceIds).filter(
(id) => id !== audienceId
);
for (const otherAudienceId of otherAudiences) {
const { error, data } = await retryWithBackoff(async () => {
const { data, error } = await resend.contacts.remove({
email,
audienceId: otherAudienceId
});
if (error) {
throw new Error(
`Error removing email ${email} from audience ${otherAudienceId}: ${error}`
);
}
return { error, data };
});
if (error) {
logger.error(
`Error removing email ${email} from audience ${otherAudienceId}: ${error}`
);
}
if (data) {
logger.info(
`Removed email ${email} from audience ${otherAudienceId}`
);
}
}
}
type RetryOptions = {
retries?: number;
initialDelayMs?: number;
factor?: number;
};
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const { retries = 5, initialDelayMs = 500, factor = 2 } = options;
let attempt = 0;
let delay = initialDelayMs;
while (true) {
try {
return await fn();
} catch (err) {
attempt++;
if (attempt > retries) throw err;
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= factor;
}
}
}

View File

@@ -24,6 +24,7 @@ import { eq, and } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import stripe from "#private/lib/stripe"; import stripe from "#private/lib/stripe";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
import { getSubType } from "./getSubType"; import { getSubType } from "./getSubType";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
@@ -171,7 +172,7 @@ export async function handleSubscriptionCreated(
const email = orgUserRes.user.email; const email = orgUserRes.user.email;
if (email) { if (email) {
// TODO: update user in Sendy moveEmailToAudience(email, AudienceIds.Subscribed);
} }
} }
} else if (type === "license") { } else if (type === "license") {

View File

@@ -23,6 +23,7 @@ import {
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
import { getSubType } from "./getSubType"; import { getSubType } from "./getSubType";
import stripe from "#private/lib/stripe"; import stripe from "#private/lib/stripe";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
@@ -108,7 +109,7 @@ export async function handleSubscriptionDeleted(
const email = orgUserRes.user.email; const email = orgUserRes.user.email;
if (email) { if (email) {
// TODO: update user in Sendy moveEmailToAudience(email, AudienceIds.Churned);
} }
} }
} else if (type === "license") { } else if (type === "license") {

View File

@@ -29,6 +29,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { eq, or, and } from "drizzle-orm"; import { eq, or, and } from "drizzle-orm";
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource"; import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA"; import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
@@ -63,7 +64,6 @@ export type SignSshKeyResponse = {
sshUsername: string; sshUsername: string;
sshHost: string; sshHost: string;
resourceId: number; resourceId: number;
siteId: number;
keyId: string; keyId: string;
validPrincipals: string[]; validPrincipals: string[];
validAfter: string; validAfter: string;
@@ -453,7 +453,6 @@ export async function signSshKey(
sshUsername: usernameToUse, sshUsername: usernameToUse,
sshHost: sshHost, sshHost: sshHost,
resourceId: resource.siteResourceId, resourceId: resource.siteResourceId,
siteId: resource.siteId,
keyId: cert.keyId, keyId: cert.keyId,
validPrincipals: cert.validPrincipals, validPrincipals: cert.validPrincipals,
validAfter: cert.validAfter.toISOString(), validAfter: cert.validAfter.toISOString(),

View File

@@ -76,7 +76,7 @@ const processMessage = async (
clientId, clientId,
message.type, // Pass message type for granular limiting message.type, // Pass message type for granular limiting
100, // max requests per window 100, // max requests per window
100, // max requests per message type per window 20, // max requests per message type per window
60 * 1000 // window in milliseconds 60 * 1000 // window in milliseconds
); );
if (rateLimitResult.isLimited) { if (rateLimitResult.isLimited) {

View File

@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { db, users } from "@server/db"; import { bannedEmails, bannedIps, db, users } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { email, z } from "zod"; import { email, z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
@@ -22,6 +22,7 @@ import { checkValidInvite } from "@server/auth/checkValidInvite";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { build } from "@server/build"; import { build } from "@server/build";
import resend, { AudienceIds, moveEmailToAudience } from "#dynamic/lib/resend";
export const signupBodySchema = z.object({ export const signupBodySchema = z.object({
email: z.email().toLowerCase(), email: z.email().toLowerCase(),
@@ -65,6 +66,30 @@ export async function signup(
skipVerificationEmail skipVerificationEmail
} = parsedBody.data; } = parsedBody.data;
const [bannedEmail] = await db
.select()
.from(bannedEmails)
.where(eq(bannedEmails.email, email))
.limit(1);
if (bannedEmail) {
return next(
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
);
}
if (req.ip) {
const [bannedIp] = await db
.select()
.from(bannedIps)
.where(eq(bannedIps.ip, req.ip))
.limit(1);
if (bannedIp) {
return next(
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
);
}
}
const passwordHash = await hashPassword(password); const passwordHash = await hashPassword(password);
const userId = generateId(15); const userId = generateId(15);
@@ -212,7 +237,7 @@ export async function signup(
logger.debug( logger.debug(
`User ${email} opted in to marketing emails during signup.` `User ${email} opted in to marketing emails during signup.`
); );
// TODO: update user in Sendy moveEmailToAudience(email, AudienceIds.SignUps);
} }
if (config.getRawConfig().flags?.require_email_verification) { if (config.getRawConfig().flags?.require_email_verification) {

View File

@@ -125,7 +125,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
// Add site as a destination for this client // Add site as a destination for this client
const destination: PeerDestination = { const destination: PeerDestination = {
destinationIP: site.subnet.split("/")[0], destinationIP: site.subnet.split("/")[0],
destinationPort: site.listenPort destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
}; };
// Check if this destination is already in the array to avoid duplicates // Check if this destination is already in the array to avoid duplicates
@@ -165,7 +165,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
const destination: PeerDestination = { const destination: PeerDestination = {
destinationIP: peer.subnet.split("/")[0], destinationIP: peer.subnet.split("/")[0],
destinationPort: peer.listenPort destinationPort: peer.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
}; };
// Check for duplicates // Check for duplicates

View File

@@ -112,7 +112,7 @@ export async function updateHolePunch(
destinations: destinations destinations: destinations
}); });
} catch (error) { } catch (error) {
// logger.error(error); // FIX THIS logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
@@ -262,7 +262,7 @@ export async function updateAndGenerateEndpointDestinations(
if (site.subnet && site.listenPort) { if (site.subnet && site.listenPort) {
destinations.push({ destinations.push({
destinationIP: site.subnet.split("/")[0], destinationIP: site.subnet.split("/")[0],
destinationPort: site.listenPort destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
}); });
} }
} }
@@ -339,10 +339,10 @@ export async function updateAndGenerateEndpointDestinations(
handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!); handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!);
} }
if (!updatedSite || !updatedSite.subnet) { // if (!updatedSite || !updatedSite.subnet) {
logger.warn(`Site not found: ${newt.siteId}`); // logger.warn(`Site not found: ${newt.siteId}`);
throw new Error("Site not found"); // throw new Error("Site not found");
} // }
// Find all clients that connect to this site // Find all clients that connect to this site
// const sitesClientPairs = await db // const sitesClientPairs = await db

View File

@@ -1,15 +1,4 @@
import { import { clients, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, ExitNode, resources, Site, siteResources, targetHealthCheck, targets } from "@server/db";
clients,
clientSiteResourcesAssociationsCache,
clientSitesAssociationsCache,
db,
ExitNode,
resources,
Site,
siteResources,
targetHealthCheck,
targets
} from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import { initPeerAddHandshake, updatePeer } from "../olm/peers"; import { initPeerAddHandshake, updatePeer } from "../olm/peers";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
@@ -80,42 +69,40 @@ export async function buildClientConfigurationForNewtClient(
// ) // )
// ); // );
if (!client.clientSitesAssociationsCache.isJitMode) { // if we are adding sites through jit then dont add the site to the olm // update the peer info on the olm
// update the peer info on the olm // if the peer has not been added yet this will be a no-op
// if the peer has not been added yet this will be a no-op await updatePeer(client.clients.clientId, {
await updatePeer(client.clients.clientId, { siteId: site.siteId,
siteId: site.siteId, endpoint: site.endpoint!,
endpoint: site.endpoint!, relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`,
relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`, publicKey: site.publicKey!,
publicKey: site.publicKey!, serverIP: site.address,
serverIP: site.address, serverPort: site.listenPort
serverPort: site.listenPort // remoteSubnets: generateRemoteSubnets(
// remoteSubnets: generateRemoteSubnets( // allSiteResources.map(
// allSiteResources.map( // ({ siteResources }) => siteResources
// ({ siteResources }) => siteResources // )
// ) // ),
// ), // aliases: generateAliasConfig(
// aliases: generateAliasConfig( // allSiteResources.map(
// allSiteResources.map( // ({ siteResources }) => siteResources
// ({ siteResources }) => siteResources // )
// ) // )
// ) });
});
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch // also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
// if it has already been added this will be a no-op // if it has already been added this will be a no-op
await initPeerAddHandshake( await initPeerAddHandshake(
// this will kick off the add peer process for the client // this will kick off the add peer process for the client
client.clients.clientId, client.clients.clientId,
{ {
siteId, siteId,
exitNode: { exitNode: {
publicKey: exitNode.publicKey, publicKey: exitNode.publicKey,
endpoint: exitNode.endpoint endpoint: exitNode.endpoint
}
} }
); }
} );
return { return {
publicKey: client.clients.pubKey!, publicKey: client.clients.pubKey!,

View File

@@ -104,11 +104,11 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
const payload = { const payload = {
oldDestination: { oldDestination: {
destinationIP: existingSite.subnet?.split("/")[0], destinationIP: existingSite.subnet?.split("/")[0],
destinationPort: existingSite.listenPort destinationPort: existingSite.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
}, },
newDestination: { newDestination: {
destinationIP: site.subnet?.split("/")[0], destinationIP: site.subnet?.split("/")[0],
destinationPort: site.listenPort destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
} }
}; };

View File

@@ -17,8 +17,6 @@ import { getUserDeviceName } from "@server/db/names";
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration"; import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
import { OlmErrorCodes, sendOlmError } from "./error"; import { OlmErrorCodes, sendOlmError } from "./error";
import { handleFingerprintInsertion } from "./fingerprintingUtils"; import { handleFingerprintInsertion } from "./fingerprintingUtils";
import { Alias } from "@server/lib/ip";
import { build } from "@server/build";
export const handleOlmRegisterMessage: MessageHandler = async (context) => { export const handleOlmRegisterMessage: MessageHandler = async (context) => {
logger.info("Handling register olm message!"); logger.info("Handling register olm message!");
@@ -209,32 +207,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
} }
} }
// Get all sites data
const sitesCountResult = await db
.select({ count: count() })
.from(sites)
.innerJoin(
clientSitesAssociationsCache,
eq(sites.siteId, clientSitesAssociationsCache.siteId)
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Extract the count value from the result array
const sitesCount =
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
// Prepare an array to store site configurations
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
let jitMode = false;
if (sitesCount > 250 && build == "saas") {
// THIS IS THE MAX ON THE BUSINESS TIER
// we have too many sites
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
logger.info("Too many sites (%d), dropping into JIT mode", sitesCount)
jitMode = true;
}
logger.debug( logger.debug(
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}` `Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
); );
@@ -261,12 +233,28 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
await db await db
.update(clientSitesAssociationsCache) .update(clientSitesAssociationsCache)
.set({ .set({
isRelayed: relay == true, isRelayed: relay == true
isJitMode: jitMode
}) })
.where(eq(clientSitesAssociationsCache.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
} }
// Get all sites data
const sitesCountResult = await db
.select({ count: count() })
.from(sites)
.innerJoin(
clientSitesAssociationsCache,
eq(sites.siteId, clientSitesAssociationsCache.siteId)
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Extract the count value from the result array
const sitesCount =
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
// Prepare an array to store site configurations
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
// this prevents us from accepting a register from an olm that has not hole punched yet. // this prevents us from accepting a register from an olm that has not hole punched yet.
// the olm will pump the register so we can keep checking // the olm will pump the register so we can keep checking
// TODO: I still think there is a better way to do this rather than locking it out here but ??? // TODO: I still think there is a better way to do this rather than locking it out here but ???
@@ -277,25 +265,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} }
let siteConfigurations: { // NOTE: its important that the client here is the old client and the public key is the new key
siteId: number; const siteConfigurations = await buildSiteConfigurationForOlmClient(
name: string; client,
endpoint: string; publicKey,
publicKey: string | null; relay
serverIP: string | null; );
serverPort: number | null;
remoteSubnets: string[];
aliases: Alias[];
}[] = [];
if (!jitMode) { // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
// NOTE: its important that the client here is the old client and the public key is the new key // if (siteConfigurations.length === 0) {
siteConfigurations = await buildSiteConfigurationForOlmClient( // logger.warn("No valid site configurations found");
client, // return;
publicKey, // }
relay
);
}
// Return connect message with all site configurations // Return connect message with all site configurations
return { return {

View File

@@ -18,7 +18,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
} }
if (!olm.clientId) { if (!olm.clientId) {
logger.warn("Olm has no client!"); logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
return; return;
} }
@@ -41,7 +41,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
return; return;
} }
const { siteId, chainId } = message.data; const { siteId } = message.data;
// Get the site // Get the site
const [site] = await db const [site] = await db
@@ -90,8 +90,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
data: { data: {
siteId: siteId, siteId: siteId,
relayEndpoint: exitNode.endpoint, relayEndpoint: exitNode.endpoint,
relayPort: config.getRawConfig().gerbil.clients_start_port, relayPort: config.getRawConfig().gerbil.clients_start_port
chainId
} }
}, },
broadcast: false, broadcast: false,

View File

@@ -1,241 +0,0 @@
import {
clientSiteResourcesAssociationsCache,
clientSitesAssociationsCache,
db,
exitNodes,
Site,
siteResources
} from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { clients, Olm, sites } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import logger from "@server/logger";
import { initPeerAddHandshake } from "./peers";
export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
context
) => {
logger.info("Handling register olm message!");
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
if (!olm) {
logger.warn("Olm not found");
return;
}
if (!olm.clientId) {
logger.warn("Olm has no client!"); // TODO: Maybe we create the site here?
return;
}
const clientId = olm.clientId;
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
logger.warn("Client not found");
return;
}
const { siteId, resourceId, chainId } = message.data;
let site: Site | null = null;
if (siteId) {
// get the site
const [siteRes] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (siteRes) {
site = siteRes;
}
}
if (resourceId && !site) {
const resources = await db
.select()
.from(siteResources)
.where(
and(
or(
eq(siteResources.niceId, resourceId),
eq(siteResources.alias, resourceId)
),
eq(siteResources.orgId, client.orgId)
)
);
if (!resources || resources.length === 0) {
logger.error(`handleOlmServerPeerAddMessage: Resource not found`);
// cancel the request from the olm side to not keep doing this
await sendToClient(
olm.olmId,
{
type: "olm/wg/peer/chain/cancel",
data: {
chainId
}
},
{ incrementConfigVersion: false }
).catch((error) => {
logger.warn(`Error sending message:`, error);
});
return;
}
if (resources.length > 1) {
// error but this should not happen because the nice id cant contain a dot and the alias has to have a dot and both have to be unique within the org so there should never be multiple matches
logger.error(
`handleOlmServerPeerAddMessage: Multiple resources found matching the criteria`
);
return;
}
const resource = resources[0];
const currentResourceAssociationCaches = await db
.select()
.from(clientSiteResourcesAssociationsCache)
.where(
and(
eq(
clientSiteResourcesAssociationsCache.siteResourceId,
resource.siteResourceId
),
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
)
)
);
if (currentResourceAssociationCaches.length === 0) {
logger.error(
`handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to resource ${resource.siteResourceId}`
);
// cancel the request from the olm side to not keep doing this
await sendToClient(
olm.olmId,
{
type: "olm/wg/peer/chain/cancel",
data: {
chainId
}
},
{ incrementConfigVersion: false }
).catch((error) => {
logger.warn(`Error sending message:`, error);
});
return;
}
const siteIdFromResource = resource.siteId;
// get the site
const [siteRes] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteIdFromResource));
if (!siteRes) {
logger.error(
`handleOlmServerPeerAddMessage: Site with ID ${site} not found`
);
return;
}
site = siteRes;
}
if (!site) {
logger.error(`handleOlmServerPeerAddMessage: Site not found`);
return;
}
// check if the client can access this site using the cache
const currentSiteAssociationCaches = await db
.select()
.from(clientSitesAssociationsCache)
.where(
and(
eq(clientSitesAssociationsCache.clientId, client.clientId),
eq(clientSitesAssociationsCache.siteId, site.siteId)
)
);
if (currentSiteAssociationCaches.length === 0) {
logger.error(
`handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to site ${site.siteId}`
);
// cancel the request from the olm side to not keep doing this
await sendToClient(
olm.olmId,
{
type: "olm/wg/peer/chain/cancel",
data: {
chainId
}
},
{ incrementConfigVersion: false }
).catch((error) => {
logger.warn(`Error sending message:`, error);
});
return;
}
if (!site.exitNodeId) {
logger.error(
`handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node`
);
// cancel the request from the olm side to not keep doing this
await sendToClient(
olm.olmId,
{
type: "olm/wg/peer/chain/cancel",
data: {
chainId
}
},
{ incrementConfigVersion: false }
).catch((error) => {
logger.warn(`Error sending message:`, error);
});
return;
}
// get the exit node from the side
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, site.exitNodeId));
if (!exitNode) {
logger.error(
`handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node`
);
return;
}
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
// if it has already been added this will be a no-op
await initPeerAddHandshake(
// this will kick off the add peer process for the client
client.clientId,
{
siteId: site.siteId,
exitNode: {
publicKey: exitNode.publicKey,
endpoint: exitNode.endpoint
}
},
olm.olmId,
chainId
);
return;
};

View File

@@ -54,7 +54,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
return; return;
} }
const { siteId, chainId } = message.data; const { siteId } = message.data;
// get the site // get the site
const [site] = await db const [site] = await db
@@ -179,8 +179,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
), ),
aliases: generateAliasConfig( aliases: generateAliasConfig(
allSiteResources.map(({ siteResources }) => siteResources) allSiteResources.map(({ siteResources }) => siteResources)
), )
chainId: chainId,
} }
}, },
broadcast: false, broadcast: false,

View File

@@ -17,7 +17,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
} }
if (!olm.clientId) { if (!olm.clientId) {
logger.warn("Olm has no client!"); logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
return; return;
} }
@@ -40,7 +40,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
return; return;
} }
const { siteId, chainId } = message.data; const { siteId } = message.data;
// Get the site // Get the site
const [site] = await db const [site] = await db
@@ -87,8 +87,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
type: "olm/wg/peer/unrelay", type: "olm/wg/peer/unrelay",
data: { data: {
siteId: siteId, siteId: siteId,
endpoint: site.endpoint, endpoint: site.endpoint
chainId
} }
}, },
broadcast: false, broadcast: false,

View File

@@ -11,4 +11,3 @@ export * from "./handleOlmServerPeerAddMessage";
export * from "./handleOlmUnRelayMessage"; export * from "./handleOlmUnRelayMessage";
export * from "./recoverOlmWithFingerprint"; export * from "./recoverOlmWithFingerprint";
export * from "./handleOlmDisconnectingMessage"; export * from "./handleOlmDisconnectingMessage";
export * from "./handleOlmServerInitAddPeerHandshake";

View File

@@ -1,8 +1,8 @@
import { sendToClient } from "#dynamic/routers/ws"; import { sendToClient } from "#dynamic/routers/ws";
import { clientSitesAssociationsCache, db, olms } from "@server/db"; import { db, olms } from "@server/db";
import config from "@server/lib/config"; import config from "@server/lib/config";
import logger from "@server/logger"; import logger from "@server/logger";
import { and, eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { Alias } from "yaml"; import { Alias } from "yaml";
export async function addPeer( export async function addPeer(
@@ -149,8 +149,7 @@ export async function initPeerAddHandshake(
endpoint: string; endpoint: string;
}; };
}, },
olmId?: string, olmId?: string
chainId?: string
) { ) {
if (!olmId) { if (!olmId) {
const [olm] = await db const [olm] = await db
@@ -174,8 +173,7 @@ export async function initPeerAddHandshake(
publicKey: peer.exitNode.publicKey, publicKey: peer.exitNode.publicKey,
relayPort: config.getRawConfig().gerbil.clients_start_port, relayPort: config.getRawConfig().gerbil.clients_start_port,
endpoint: peer.exitNode.endpoint endpoint: peer.exitNode.endpoint
}, }
chainId
} }
}, },
{ incrementConfigVersion: true } { incrementConfigVersion: true }
@@ -183,17 +181,6 @@ export async function initPeerAddHandshake(
logger.warn(`Error sending message:`, error); logger.warn(`Error sending message:`, error);
}); });
// update the clientSiteAssociationsCache to make the isJitMode flag false so that JIT mode is disabled for this site if it restarts or something after the connection
await db
.update(clientSitesAssociationsCache)
.set({ isJitMode: false })
.where(
and(
eq(clientSitesAssociationsCache.clientId, clientId),
eq(clientSitesAssociationsCache.siteId, peer.siteId)
)
);
logger.info( logger.info(
`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}` `Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`
); );

View File

@@ -223,6 +223,20 @@ async function createHttpResource(
); );
} }
// Prevent creating resource with same domain as dashboard
const dashboardUrl = config.getRawConfig().app.dashboard_url;
if (dashboardUrl) {
const dashboardHost = new URL(dashboardUrl).hostname;
if (fullDomain === dashboardHost) {
return next(
createHttpError(
HttpCode.CONFLICT,
"Resource domain cannot be the same as the dashboard domain"
)
);
}
}
if (build != "oss") { if (build != "oss") {
const existingLoginPages = await db const existingLoginPages = await db
.select() .select()

View File

@@ -353,6 +353,20 @@ async function updateHttpResource(
); );
} }
// Prevent updating resource with same domain as dashboard
const dashboardUrl = config.getRawConfig().app.dashboard_url;
if (dashboardUrl) {
const dashboardHost = new URL(dashboardUrl).hostname;
if (fullDomain === dashboardHost) {
return next(
createHttpError(
HttpCode.CONFLICT,
"Resource domain cannot be the same as the dashboard domain"
)
);
}
}
if (build != "oss") { if (build != "oss") {
const existingLoginPages = await db const existingLoginPages = await db
.select() .select()

View File

@@ -15,8 +15,7 @@ import {
startOlmOfflineChecker, startOlmOfflineChecker,
handleOlmServerPeerAddMessage, handleOlmServerPeerAddMessage,
handleOlmUnRelayMessage, handleOlmUnRelayMessage,
handleOlmDisconnecingMessage, handleOlmDisconnecingMessage
handleOlmServerInitAddPeerHandshake
} from "../olm"; } from "../olm";
import { handleHealthcheckStatusMessage } from "../target"; import { handleHealthcheckStatusMessage } from "../target";
import { handleRoundTripMessage } from "./handleRoundTripMessage"; import { handleRoundTripMessage } from "./handleRoundTripMessage";
@@ -24,7 +23,6 @@ import { MessageHandler } from "./types";
export const messageHandlers: Record<string, MessageHandler> = { export const messageHandlers: Record<string, MessageHandler> = {
"olm/wg/server/peer/add": handleOlmServerPeerAddMessage, "olm/wg/server/peer/add": handleOlmServerPeerAddMessage,
"olm/wg/server/peer/init": handleOlmServerInitAddPeerHandshake,
"olm/wg/register": handleOlmRegisterMessage, "olm/wg/register": handleOlmRegisterMessage,
"olm/wg/relay": handleOlmRelayMessage, "olm/wg/relay": handleOlmRelayMessage,
"olm/wg/unrelay": handleOlmUnRelayMessage, "olm/wg/unrelay": handleOlmUnRelayMessage,

View File

@@ -559,7 +559,7 @@ export default function Page() {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("resourceErrorCreate"), title: t("resourceErrorCreate"),
description: t("resourceErrorCreateMessageDescription") description: formatAxiosError(e, t("resourceErrorCreateMessageDescription"))
}); });
} }

View File

@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
return ( return (
<CredenzaContent <CredenzaContent
className={cn( className={cn(
"overflow-y-auto max-h-[100dvh] md:max-h-screen md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0", "overflow-y-auto max-h-[100dvh] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
className className
)} )}
{...props} {...props}

View File

@@ -51,6 +51,7 @@ const docsLinkClassName =
const PANGOLIN_CLOUD_SIGNUP_URL = "https://app.pangolin.net/auth/signup/"; const PANGOLIN_CLOUD_SIGNUP_URL = "https://app.pangolin.net/auth/signup/";
const ENTERPRISE_DOCS_URL = const ENTERPRISE_DOCS_URL =
"https://docs.pangolin.net/self-host/enterprise-edition"; "https://docs.pangolin.net/self-host/enterprise-edition";
const BOOK_A_DEMO_URL = "https://click.fossorial.io/ep922";
function getTierLinkRenderer(billingHref: string) { function getTierLinkRenderer(billingHref: string) {
return function tierLinkRenderer(chunks: React.ReactNode) { return function tierLinkRenderer(chunks: React.ReactNode) {
@@ -78,6 +79,22 @@ function getPangolinCloudLinkRenderer() {
}; };
} }
function getBookADemoLinkRenderer() {
return function bookADemoLinkRenderer(chunks: React.ReactNode) {
return (
<Link
href={BOOK_A_DEMO_URL}
target="_blank"
rel="noopener noreferrer"
className={docsLinkClassName}
>
{chunks}
<ExternalLink className="size-3.5 shrink-0" />
</Link>
);
};
}
function getDocsLinkRenderer(href: string) { function getDocsLinkRenderer(href: string) {
return function docsLinkRenderer(chunks: React.ReactNode) { return function docsLinkRenderer(chunks: React.ReactNode) {
return ( return (
@@ -116,6 +133,7 @@ export function PaidFeaturesAlert({ tiers }: Props) {
const tierLinkRenderer = getTierLinkRenderer(billingHref); const tierLinkRenderer = getTierLinkRenderer(billingHref);
const pangolinCloudLinkRenderer = getPangolinCloudLinkRenderer(); const pangolinCloudLinkRenderer = getPangolinCloudLinkRenderer();
const enterpriseDocsLinkRenderer = getDocsLinkRenderer(ENTERPRISE_DOCS_URL); const enterpriseDocsLinkRenderer = getDocsLinkRenderer(ENTERPRISE_DOCS_URL);
const bookADemoLinkRenderer = getBookADemoLinkRenderer();
if (env.flags.disableEnterpriseFeatures) { if (env.flags.disableEnterpriseFeatures) {
return null; return null;
@@ -157,7 +175,8 @@ export function PaidFeaturesAlert({ tiers }: Props) {
{t.rich("licenseRequiredToUse", { {t.rich("licenseRequiredToUse", {
enterpriseLicenseLink: enterpriseLicenseLink:
enterpriseDocsLinkRenderer, enterpriseDocsLinkRenderer,
pangolinCloudLink: pangolinCloudLinkRenderer pangolinCloudLink: pangolinCloudLinkRenderer,
bookADemoLink: bookADemoLinkRenderer
})} })}
</span> </span>
</div> </div>
@@ -174,7 +193,8 @@ export function PaidFeaturesAlert({ tiers }: Props) {
{t.rich("ossEnterpriseEditionRequired", { {t.rich("ossEnterpriseEditionRequired", {
enterpriseEditionLink: enterpriseEditionLink:
enterpriseDocsLinkRenderer, enterpriseDocsLinkRenderer,
pangolinCloudLink: pangolinCloudLinkRenderer pangolinCloudLink: pangolinCloudLinkRenderer,
bookADemoLink: bookADemoLinkRenderer
})} })}
</span> </span>
</div> </div>