Compare commits

...

553 Commits

Author SHA1 Message Date
Owen
972febf0ea Fix regex 2025-12-21 20:36:19 -05:00
Owen
6060b1d60d Merge branch 'main' into dev 2025-12-21 20:31:38 -05:00
miloschwartz
c91b4beac5 fix resource queries 2025-12-21 19:42:03 -05:00
Owen Schwartz
3577b5efb9 New translations en-us.json (Norwegian Bokmal) 2025-12-21 18:34:26 -05:00
Owen Schwartz
6069b84e58 New translations en-us.json (Chinese Simplified) 2025-12-21 18:34:26 -05:00
Owen Schwartz
cbccea0bbc New translations en-us.json (Turkish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
e17212c584 New translations en-us.json (Russian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
e38102b022 New translations en-us.json (Portuguese) 2025-12-21 18:34:26 -05:00
Owen Schwartz
5749704cf1 New translations en-us.json (Polish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
f584cba6be New translations en-us.json (Dutch) 2025-12-21 18:34:26 -05:00
Owen Schwartz
0661a950c7 New translations en-us.json (Korean) 2025-12-21 18:34:26 -05:00
Owen Schwartz
8f5ac1282a New translations en-us.json (Italian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
232a178e0f New translations en-us.json (German) 2025-12-21 18:34:26 -05:00
Owen Schwartz
4f64db1d82 New translations en-us.json (Czech) 2025-12-21 18:34:26 -05:00
Owen Schwartz
d91356574b New translations en-us.json (Bulgarian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
bd666d46b2 New translations en-us.json (Spanish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
881346c31a New translations en-us.json (French) 2025-12-21 18:34:26 -05:00
Owen Schwartz
ec1b41ebbb New translations en-us.json (Norwegian Bokmal) 2025-12-21 18:34:26 -05:00
Owen Schwartz
abe7bbf068 New translations en-us.json (Chinese Simplified) 2025-12-21 18:34:26 -05:00
Owen Schwartz
a1d33f8103 New translations en-us.json (Turkish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
b55386e301 New translations en-us.json (Russian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
59fc5713ca New translations en-us.json (Portuguese) 2025-12-21 18:34:26 -05:00
Owen Schwartz
b9bd6433a7 New translations en-us.json (Polish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
6b2e77262e New translations en-us.json (Dutch) 2025-12-21 18:34:26 -05:00
Owen Schwartz
8904db8dd1 New translations en-us.json (Korean) 2025-12-21 18:34:26 -05:00
Owen Schwartz
0da15ae1e6 New translations en-us.json (Italian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
0086818928 New translations en-us.json (German) 2025-12-21 18:34:26 -05:00
Owen Schwartz
d7abf9369e New translations en-us.json (Czech) 2025-12-21 18:34:26 -05:00
Owen Schwartz
5fcc7bbff4 New translations en-us.json (Bulgarian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
e90d87f26d New translations en-us.json (Spanish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
46a6d2be9e New translations en-us.json (French) 2025-12-21 18:34:26 -05:00
Owen Schwartz
f6709c1bdf New translations en-us.json (German) 2025-12-21 18:34:26 -05:00
Owen Schwartz
c2a721791f New translations en-us.json (German) 2025-12-21 18:34:26 -05:00
Owen Schwartz
ec1f5eff19 New translations en-us.json (Norwegian Bokmal) 2025-12-21 18:34:26 -05:00
Owen Schwartz
10463e5b55 New translations en-us.json (Chinese Simplified) 2025-12-21 18:34:26 -05:00
Owen Schwartz
30fa048637 New translations en-us.json (Turkish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
6bae89023b New translations en-us.json (Russian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
874aac010e New translations en-us.json (Portuguese) 2025-12-21 18:34:26 -05:00
Owen Schwartz
465a007f2e New translations en-us.json (Polish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
a678a18bcf New translations en-us.json (Dutch) 2025-12-21 18:34:26 -05:00
Owen Schwartz
dbb0979e86 New translations en-us.json (Korean) 2025-12-21 18:34:26 -05:00
Owen Schwartz
2571ade633 New translations en-us.json (Italian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
38bae0dc6a New translations en-us.json (German) 2025-12-21 18:34:26 -05:00
Owen Schwartz
7409d44923 New translations en-us.json (Czech) 2025-12-21 18:34:26 -05:00
Owen Schwartz
84b4f15ed4 New translations en-us.json (Bulgarian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
412c4717ad New translations en-us.json (Spanish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
88d3e76c44 New translations en-us.json (French) 2025-12-21 18:34:26 -05:00
Owen Schwartz
8626454811 New translations en-us.json (Norwegian Bokmal) 2025-12-21 18:34:26 -05:00
Owen Schwartz
f7c0a6875c New translations en-us.json (Chinese Simplified) 2025-12-21 18:34:26 -05:00
Owen Schwartz
f89f3398fa New translations en-us.json (Turkish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
32898eb5d3 New translations en-us.json (Russian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
9fe05b7af4 New translations en-us.json (Portuguese) 2025-12-21 18:34:26 -05:00
Owen Schwartz
f8be34370d New translations en-us.json (Polish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
91bd38227e New translations en-us.json (Dutch) 2025-12-21 18:34:26 -05:00
Owen Schwartz
101ee581e8 New translations en-us.json (Korean) 2025-12-21 18:34:26 -05:00
Owen Schwartz
8d099c51e1 New translations en-us.json (Italian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
939264014b New translations en-us.json (German) 2025-12-21 18:34:26 -05:00
Owen Schwartz
bbeb4c029c New translations en-us.json (Czech) 2025-12-21 18:34:26 -05:00
Owen Schwartz
59a37cc606 New translations en-us.json (Bulgarian) 2025-12-21 18:34:26 -05:00
Owen Schwartz
ce3962f97a New translations en-us.json (Spanish) 2025-12-21 18:34:26 -05:00
Owen Schwartz
3c20fd0a55 New translations en-us.json (French) 2025-12-21 18:34:26 -05:00
Owen
e320f9f16e Handle the rc 2025-12-21 17:23:35 -05:00
Owen
11200b99d3 Use image tools to combine the tags 2025-12-21 17:17:02 -05:00
Dhananjay Mahtha
7507806aaa Fix: Support public-resources and private-resources in Docker blueprint labels
- Add support for pangolin.public-resources.* labels as alias for proxy-resources
- Add support for pangolin.private-resources.* labels as alias for client-resources
- Update processContainerLabels to parse all four resource type prefixes
- Update early-exit check in applyNewtDockerBlueprint to consider all four resource keys
- ConfigSchema transformation will merge public/private into proxy/client as designed

Fixes #2125
2025-12-21 17:09:01 -05:00
Dhananjay Mahtha
90c48f20e0 Fix: Prevent cache memory leak by adding maxKeys limit and conditional caching
- Add maxKeys limit (10,000) to NodeCache to prevent unbounded memory growth
- Skip caching undefined values when GeoIP/ASN lookups fail (e.g., when MaxMind DB not configured)
- Add periodic cache statistics logging every 5 minutes for monitoring
- Fixes memory leak where cache would grow indefinitely with high request volumes

The maxKeys limit uses LRU eviction, so oldest entries are automatically removed
when the limit is reached. With ~10k requests/day and 5min TTL, 10k keys provides
ample headroom while preventing OOM issues.

Fixes #2120
2025-12-21 17:08:27 -05:00
Owen
9e68c6c004 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-21 16:59:55 -05:00
Owen
130c890678 Merge branch 'main' into dev 2025-12-21 16:58:13 -05:00
Owen Schwartz
5e183911e1 Revert "Bump recharts from 2.15.4 to 3.5.1"
This reverts commit 9d9401d2ee.
2025-12-21 16:50:08 -05:00
miloschwartz
74479c984c update traefik config with new badger version 2025-12-21 16:48:50 -05:00
Owen
1d5d856799 Make extendedCompatibility on by default 2025-12-21 16:38:28 -05:00
Owen
8ea6b0cd9e Adjust algo for showing maintence page 2025-12-21 16:38:27 -05:00
miloschwartz
90d07f9794 use cammel case in cli args 2025-12-21 16:26:06 -05:00
miloschwartz
e9a29e7db2 improvements to create private resource modal 2025-12-21 16:07:32 -05:00
miloschwartz
b2df8eb72e add advanced toggle to sites and clients create 2025-12-21 16:02:09 -05:00
miloschwartz
3f81b88073 add advanced section to org create form 2025-12-21 15:56:57 -05:00
miloschwartz
dedc13ab98 bump version 2025-12-21 15:45:12 -05:00
miloschwartz
2f8ecf17ed add postgres migration 2025-12-21 15:43:38 -05:00
miloschwartz
81149085fa add sqlite migration 2025-12-21 15:32:43 -05:00
miloschwartz
0aa56d441e fix scroll bar 2025-12-20 22:31:50 -05:00
miloschwartz
757b735d98 separate maintenance section 2025-12-20 22:29:22 -05:00
miloschwartz
4af7900dae auto focus 2fa 2025-12-20 22:06:53 -05:00
miloschwartz
a3610b7dde add badger middleware to api and dashboard routes 2025-12-20 21:49:18 -05:00
Owen
af4f85a081 Fix api call 2025-12-20 21:14:48 -05:00
Owen
6a5939599c Remove maintence mode internal vars and use server 2025-12-20 17:28:39 -05:00
Owen
51ef859349 Dont print env var; confusing user 2025-12-20 17:28:30 -05:00
Owen
e477a5a1b5 Add ee install option 2025-12-20 17:28:07 -05:00
miloschwartz
4a98061a62 visual enhancements 2025-12-20 16:50:05 -05:00
Owen
be20289140 Resolve some warnings 2025-12-20 16:38:03 -05:00
Owen
3ce0cc1992 Add missing semicolon 2025-12-20 16:35:16 -05:00
Owen
a9a0fbe244 Merge UI into new screen 2025-12-20 16:35:16 -05:00
Owen
03d1f4bbb9 Update lock 2025-12-20 16:35:16 -05:00
Owen
9b3d066a91 Fix merge issues 2025-12-20 16:35:16 -05:00
miloschwartz
ccd4f9b65c restyle maintenance mode screen 2025-12-20 16:35:16 -05:00
Owen
d8344988c0 Restrict license 2025-12-20 16:35:16 -05:00
Owen
bb5594ab2f Update form 2025-12-20 16:35:16 -05:00
Owen
19f8cda3d9 Make private 2025-12-20 16:35:16 -05:00
Pallavi Kumari
d3c4688c0f redirect everything to maintenance page 2025-12-20 16:35:16 -05:00
Pallavi Kumari
2d92111f1d add to priv route 2025-12-20 16:35:16 -05:00
Owen
b8ffc601d4 add backend API maintenance screen 2025-12-20 16:35:16 -05:00
Pallavi Kumari
8af95ea1ca Lazy-Load DB for maintenance-screen 2025-12-20 16:35:16 -05:00
Pallavi Kumari
beddb0d187 Skip config fetch during build 2025-12-20 16:35:16 -05:00
Pallavi Kumari
bd20bb0dd1 add logger 2025-12-20 16:35:16 -05:00
Owen
662e63317b add tooltip 2025-12-20 16:35:16 -05:00
Pallavi Kumari
d82535d3e1 point the resource to the nextjs server for maintenance screen 2025-12-20 16:35:16 -05:00
Pallavi Kumari
1d862131dd remove maintenance mode from oss traefik config generator 2025-12-20 16:35:16 -05:00
Pallavi Kumari
150c51c9eb move settings into a new SettingsSection card 2025-12-20 16:35:16 -05:00
Owen
8618a5c2fd add en-Us strings 2025-12-20 16:35:16 -05:00
Pallavi Kumari
795302a351 refactor files and add func to private traefik config generator file 2025-12-20 16:35:16 -05:00
Pallavi Kumari
096a2bfa10 fix maintenance router name 2025-12-20 16:35:16 -05:00
Pallavi Kumari
188994ce84 add pg schema 2025-12-20 16:35:16 -05:00
Owen
800bdcb277 generate traefik config for maintenance ui 2025-12-20 16:35:16 -05:00
Pallavi Kumari
c033fd4e8b backend for updating maintenance screen 2025-12-20 16:35:16 -05:00
Owen
d2fa55dd11 ui to enable down for maintenance screen 2025-12-20 16:35:16 -05:00
Owen
7e047d9e34 db schema for maintenance 2025-12-20 16:35:16 -05:00
dependabot[bot]
9d9401d2ee Bump recharts from 2.15.4 to 3.5.1
Bumps [recharts](https://github.com/recharts/recharts) from 2.15.4 to 3.5.1.
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v2.15.4...v3.5.1)

---
updated-dependencies:
- dependency-name: recharts
  dependency-version: 3.5.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 15:35:30 -05:00
dependabot[bot]
9a621044d8 Bump aws-actions/configure-aws-credentials from 2 to 5
Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 2 to 5.
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/v2...v5)

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 15:35:24 -05:00
dependabot[bot]
3a6fbb67a5 Bump actions/upload-artifact from 5.0.0 to 6.0.0
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](330a01c490...b7c566a772)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 15:35:15 -05:00
Owen
c7c70fa736 Merge branch 'jln-brtn-dev' into dev 2025-12-20 15:34:53 -05:00
Owen
eafcefbe45 Merge branch 'dev' of github.com:jln-brtn/pangolin into jln-brtn-dev 2025-12-20 15:34:32 -05:00
miloschwartz
8ed13b41d9 Merge branch 'dev' into cicd 2025-12-20 12:32:15 -05:00
Owen
b80757a129 Add blueprint validation 2025-12-20 12:16:31 -05:00
Owen
13ddf30781 Add hybrid route 2025-12-20 12:16:31 -05:00
Owen
4ecca88856 Add asn option to blueprint type 2025-12-20 12:16:31 -05:00
Thomas Wilde
4f154d212e Add ASN-based resource rule matching
- Add MaxMind ASN database integration
- Implement ASN lookup and matching in resource rule verification
- Add curated list of 100+ major ASNs (cloud, ISP, CDN, mobile carriers)
- Add ASN dropdown selector in resource rules UI with search functionality
- Support custom ASN input for unlisted ASNs
- Add 'ALL ASNs' special case handling (AS0)
- Cache ASN lookups with 5-minute TTL for performance
- Update validation schemas to support ASN match type

This allows administrators to create resource access rules based on
Autonomous System Numbers, similar to existing country-based rules.
Useful for restricting access by ISP, cloud provider, or mobile carrier.
2025-12-20 12:16:31 -05:00
dependabot[bot]
981d777a65 Bump the prod-patch-updates group across 1 directory with 6 updates
Bumps the prod-patch-updates group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `1.0.1` | `1.0.2` |
| [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `2.0.1` | `2.0.2` |
| [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) | `0.5.10` | `0.5.11` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.45.0` | `0.45.1` |
| [eslint](https://github.com/eslint/eslint) | `9.39.1` | `9.39.2` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.17.2` | `5.17.4` |



Updates `@react-email/components` from 1.0.1 to 1.0.2
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/components/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/components@1.0.2/packages/components)

Updates `@react-email/tailwind` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@2.0.2/packages/tailwind)

Updates `@tailwindcss/forms` from 0.5.10 to 0.5.11
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.10...v0.5.11)

Updates `drizzle-orm` from 0.45.0 to 0.45.1
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.45.0...0.45.1)

Updates `eslint` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

Updates `posthog-node` from 5.17.2 to 5.17.4
- [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.17.4/packages/node)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-version: 1.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/tailwind"
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@tailwindcss/forms"
  dependency-version: 0.5.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: drizzle-orm
  dependency-version: 0.45.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: posthog-node
  dependency-version: 5.17.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 12:16:31 -05:00
dependabot[bot]
dd13758085 Bump the dev-patch-updates group across 1 directory with 4 updates
Bumps the dev-patch-updates group with 4 updates in the / directory: [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx), [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss), [esbuild](https://github.com/evanw/esbuild) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss).


Updates `@dotenvx/dotenvx` from 1.51.1 to 1.51.2
- [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.51.1...v1.51.2)

Updates `@tailwindcss/postcss` from 4.1.17 to 4.1.18
- [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.1.18/packages/@tailwindcss-postcss)

Updates `esbuild` from 0.27.1 to 0.27.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.2)

Updates `tailwindcss` from 4.1.17 to 4.1.18
- [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.1.18/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.51.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: esbuild
  dependency-version: 0.27.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 12:16:30 -05:00
dependabot[bot]
3d8153aeb1 Bump the prod-minor-updates group across 1 directory with 7 updates
Bumps the prod-minor-updates group with 7 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.948.0` | `3.955.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.0.8` | `16.1.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.559.0` | `0.562.0` |
| [next-intl](https://github.com/amannn/next-intl) | `4.5.8` | `4.6.1` |
| [react-day-picker](https://github.com/gpbl/react-day-picker) | `9.12.0` | `9.13.0` |
| [stripe](https://github.com/stripe/stripe-node) | `20.0.0` | `20.1.0` |
| [zod](https://github.com/colinhacks/zod) | `4.1.13` | `4.2.1` |



Updates `@aws-sdk/client-s3` from 3.948.0 to 3.955.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.955.0/clients/client-s3)

Updates `eslint-config-next` from 16.0.8 to 16.1.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.1.0/packages/eslint-config-next)

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

Updates `next-intl` from 4.5.8 to 4.6.1
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.5.8...v4.6.1)

Updates `react-day-picker` from 9.12.0 to 9.13.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.12.0...v9.13.0)

Updates `stripe` from 20.0.0 to 20.1.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.0.0...v20.1.0)

Updates `zod` from 4.1.13 to 4.2.1
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.1.13...v4.2.1)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.955.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint-config-next
  dependency-version: 16.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.562.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: next-intl
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-day-picker
  dependency-version: 9.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: stripe
  dependency-version: 20.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: zod
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 12:16:30 -05:00
Owen
ce3cb98422 Add blueprint validation 2025-12-20 12:16:00 -05:00
Owen
ae5bdcd88b Add hybrid route 2025-12-20 12:16:00 -05:00
Owen
428a76d742 Add asn option to blueprint type 2025-12-20 12:16:00 -05:00
Thomas Wilde
8d2955475b Add ASN-based resource rule matching
- Add MaxMind ASN database integration
- Implement ASN lookup and matching in resource rule verification
- Add curated list of 100+ major ASNs (cloud, ISP, CDN, mobile carriers)
- Add ASN dropdown selector in resource rules UI with search functionality
- Support custom ASN input for unlisted ASNs
- Add 'ALL ASNs' special case handling (AS0)
- Cache ASN lookups with 5-minute TTL for performance
- Update validation schemas to support ASN match type

This allows administrators to create resource access rules based on
Autonomous System Numbers, similar to existing country-based rules.
Useful for restricting access by ISP, cloud provider, or mobile carrier.
2025-12-20 12:16:00 -05:00
miloschwartz
9ffa391416 improve clean redirects 2025-12-20 12:00:58 -05:00
dependabot[bot]
1f4ebf1907 Bump the prod-patch-updates group across 1 directory with 6 updates
Bumps the prod-patch-updates group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `1.0.1` | `1.0.2` |
| [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `2.0.1` | `2.0.2` |
| [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) | `0.5.10` | `0.5.11` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.45.0` | `0.45.1` |
| [eslint](https://github.com/eslint/eslint) | `9.39.1` | `9.39.2` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.17.2` | `5.17.4` |



Updates `@react-email/components` from 1.0.1 to 1.0.2
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/components/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/components@1.0.2/packages/components)

Updates `@react-email/tailwind` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@2.0.2/packages/tailwind)

Updates `@tailwindcss/forms` from 0.5.10 to 0.5.11
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.10...v0.5.11)

Updates `drizzle-orm` from 0.45.0 to 0.45.1
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.45.0...0.45.1)

Updates `eslint` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

Updates `posthog-node` from 5.17.2 to 5.17.4
- [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.17.4/packages/node)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-version: 1.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/tailwind"
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@tailwindcss/forms"
  dependency-version: 0.5.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: drizzle-orm
  dependency-version: 0.45.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: posthog-node
  dependency-version: 5.17.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 11:56:46 -05:00
dependabot[bot]
4cb5c22268 Bump the dev-patch-updates group across 1 directory with 4 updates
Bumps the dev-patch-updates group with 4 updates in the / directory: [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx), [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss), [esbuild](https://github.com/evanw/esbuild) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss).


Updates `@dotenvx/dotenvx` from 1.51.1 to 1.51.2
- [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.51.1...v1.51.2)

Updates `@tailwindcss/postcss` from 4.1.17 to 4.1.18
- [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.1.18/packages/@tailwindcss-postcss)

Updates `esbuild` from 0.27.1 to 0.27.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.2)

Updates `tailwindcss` from 4.1.17 to 4.1.18
- [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.1.18/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.51.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: esbuild
  dependency-version: 0.27.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 11:43:27 -05:00
dependabot[bot]
b7b65bb295 Bump the prod-minor-updates group across 1 directory with 7 updates
Bumps the prod-minor-updates group with 7 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.948.0` | `3.955.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.0.8` | `16.1.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.559.0` | `0.562.0` |
| [next-intl](https://github.com/amannn/next-intl) | `4.5.8` | `4.6.1` |
| [react-day-picker](https://github.com/gpbl/react-day-picker) | `9.12.0` | `9.13.0` |
| [stripe](https://github.com/stripe/stripe-node) | `20.0.0` | `20.1.0` |
| [zod](https://github.com/colinhacks/zod) | `4.1.13` | `4.2.1` |



Updates `@aws-sdk/client-s3` from 3.948.0 to 3.955.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.955.0/clients/client-s3)

Updates `eslint-config-next` from 16.0.8 to 16.1.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.1.0/packages/eslint-config-next)

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

Updates `next-intl` from 4.5.8 to 4.6.1
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.5.8...v4.6.1)

Updates `react-day-picker` from 9.12.0 to 9.13.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.12.0...v9.13.0)

Updates `stripe` from 20.0.0 to 20.1.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.0.0...v20.1.0)

Updates `zod` from 4.1.13 to 4.2.1
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.1.13...v4.2.1)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.955.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint-config-next
  dependency-version: 16.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.562.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: next-intl
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-day-picker
  dependency-version: 9.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: stripe
  dependency-version: 20.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: zod
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 11:43:06 -05:00
miloschwartz
afc19f192b visual enhancements to sidebar 2025-12-19 21:57:44 -05:00
miloschwartz
5587bd9d59 Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-12-19 17:06:57 -05:00
miloschwartz
b5f8e8feb2 add org auth slug with device auth support 2025-12-19 17:04:37 -05:00
Jacky Fong
9bd66fa306 add back the blueprints api - draft 2025-12-19 15:01:33 -05:00
Owen
fea4d43920 Make utility subnet configurable 2025-12-19 14:45:00 -05:00
miloschwartz
d414617f9d add color to product updates 2025-12-19 10:45:45 -05:00
miloschwartz
1d7e55bf98 add gradient to saas 2025-12-18 18:16:22 -05:00
miloschwartz
bc45e16109 improve local table state 2025-12-18 18:08:07 -05:00
miloschwartz
4f1dc19569 sidebar enhancements 2025-12-18 17:54:29 -05:00
miloschwartz
1af938d7ea Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-12-18 17:47:59 -05:00
miloschwartz
fc924f707c add banners 2025-12-18 17:47:54 -05:00
Owen
6e7ba1dc52 Prevent overlapping resources with org subnets 2025-12-18 17:08:50 -05:00
Owen
3e01bfef7d Move primaryDb into driver 2025-12-18 17:08:50 -05:00
miloschwartz
d8b662496b Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-12-18 16:49:48 -05:00
miloschwartz
e0de003c2c Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-12-18 16:49:44 -05:00
miloschwartz
6e35c182b0 Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-12-18 16:13:59 -05:00
miloschwartz
2479a3c53c improved private resource modal 2025-12-18 16:13:15 -05:00
Owen
6b609bb078 Force big queries onto primary db to prevent 40001 2025-12-18 16:03:15 -05:00
Owen
9c21e3da16 Merge branch 'main' into dev 2025-12-18 15:00:13 -05:00
Owen
7ccde11e3e Fix crowdsec healthcheck
Fixes #2118
2025-12-18 13:49:55 -05:00
miloschwartz
56b0185c8f visual adjustments 2025-12-18 10:58:16 -05:00
Milo Schwartz
8b47b2aabe Merge pull request #1989 from Fredkiss3/refactor/save-button-positions
refactor: save button positionning
2025-12-18 07:28:47 -08:00
miloschwartz
416fd914cb visual enhacements 2025-12-18 10:26:28 -05:00
depado
16653dd524 fix(database): filter dates evaluated at module load time 2025-12-18 10:06:31 -05:00
Fred KISSIE
e2d3d172af Merge branch 'dev' into refactor/save-button-positions 2025-12-18 04:39:17 +01:00
Fred KISSIE
137d6c2523 🏷️ fix typescript error 2025-12-18 04:36:09 +01:00
Fred KISSIE
1a976c78ef ♻️ separate org settings page into multiple forms 2025-12-18 04:27:24 +01:00
miloschwartz
e309a125f5 move blueprints on sidebar 2025-12-17 22:23:35 -05:00
Owen
2bdb1ddb6f Update lock 2025-12-17 22:14:57 -05:00
Owen
8ff588407c Fix various small issues; blueprints working 2025-12-17 22:12:16 -05:00
Owen
c2e06725a8 Keep the same site resource id 2025-12-17 22:12:16 -05:00
Owen
bb43e0c325 Handle changing site by recreating site resource 2025-12-17 22:12:16 -05:00
Owen
35ea01610a Update API routes and ui 2025-12-17 22:12:16 -05:00
miloschwartz
79eefc0ac7 blueprints enhancements 2025-12-17 22:05:36 -05:00
miloschwartz
3a781f9ac4 add contrast to sidebar 2025-12-17 21:28:46 -05:00
Owen
cc1e551f43 Fix deleting site not terminating newt 2025-12-17 21:23:25 -05:00
miloschwartz
68191d5921 fix refresh button doesnt work for users table 2025-12-17 21:19:50 -05:00
Fred KISSIE
2b3d065650 Merge branch 'dev' into refactor/save-button-positions 2025-12-18 01:46:13 +01:00
Fred KISSIE
7ae80d2cad ♻️ apply domain picker from dev 2025-12-18 00:20:19 +01:00
miloschwartz
acf08e3ef6 simplify idp create selector 2025-12-17 16:56:31 -05:00
miloschwartz
6f50fb8a4f remote node changes 2025-12-17 16:50:39 -05:00
miloschwartz
a5b203af27 add rotate server secret command 2025-12-17 16:23:11 -05:00
miloschwartz
443b53ee37 add clear exit nodes pangctl command 2025-12-17 16:01:55 -05:00
miloschwartz
e033c10021 simplify animation 2025-12-17 15:52:12 -05:00
miloschwartz
ad4c44c325 visual adjustments 2025-12-17 15:34:36 -05:00
miloschwartz
4aef7ca8d5 small branding fixes and adjustments 2025-12-17 15:02:39 -05:00
Owen
f892acbc4c Add tcp, udp ports, and icmp to blueprints 2025-12-17 11:58:45 -05:00
Milo Schwartz
9010ed6237 Merge pull request #1846 from Fredkiss3/feat/login-page-customization
feat: login page customization
2025-12-17 08:42:55 -08:00
miloschwartz
9f29657570 Merge branch 'dev' into feat/login-page-customization 2025-12-17 11:41:17 -05:00
Milo Schwartz
1b13132845 Merge pull request #2033 from buggystick/feature/oidc-email-error
Add OIDC authentication error response support
2025-12-17 08:21:06 -08:00
Milo Schwartz
553fda265c Merge branch 'dev' into feature/oidc-email-error 2025-12-17 08:20:40 -08:00
miloschwartz
0f79826535 fix useEffect re-render too many times 2025-12-17 11:18:30 -05:00
miloschwartz
14438bd2b4 remove desc text 2025-12-17 10:47:51 -05:00
Milo Schwartz
c4445c329f Merge pull request #2034 from Fredkiss3/refactor/domain-picker-default-value
refactor:  Update `<DomainPicker />` to accept default values
2025-12-17 07:40:46 -08:00
miloschwartz
5c032ee0c3 add larger header text 2025-12-17 10:34:49 -05:00
Fred KISSIE
d3d5a1c204 🚸 trigger null domain change if the user switches from another domain type to free domain option to prevent the modal from registering it as a valid value 2025-12-17 05:22:04 +01:00
miloschwartz
809bb4a7b4 adjustments to dialog spacing 2025-12-16 23:19:18 -05:00
miloschwartz
e8f763a77f fix broken link 2025-12-16 23:06:12 -05:00
Fred KISSIE
3ad4a76f03 ♻️ pass default subdomain value to org auth page settings 2025-12-17 05:05:30 +01:00
Fred KISSIE
b133593ea2 🚸 now the domain picker is deterministic 2025-12-17 04:57:16 +01:00
Owen
43fb06084f Alias should not get double regex 2025-12-16 21:54:14 -05:00
Owen
9de39dbe42 Support wildcard resources 2025-12-16 21:54:14 -05:00
Fred KISSIE
c98d61a8fb ♻️ pass default value to domain picker 2025-12-17 02:36:29 +01:00
Fred KISSIE
fccff9c23a Merge branch 'dev' into refactor/domain-picker-default-value 2025-12-17 00:52:32 +01:00
Fred KISSIE
e02fa7c148 ♻️ pass the default domainId instead of the base domain 2025-12-17 00:52:12 +01:00
Owen
a21029582e Always send the relay port config 2025-12-16 18:38:20 -05:00
Fred KISSIE
9ef7faace7 🚧 wip 2025-12-16 23:45:53 +01:00
Owen
3d5ae9dd5c Disable icmp packets over private resources 2025-12-16 17:20:18 -05:00
miloschwartz
6072ee93fa add remove invitation to integration api 2025-12-16 17:17:03 -05:00
Owen
7f7f6eeaea Check the postgres string first
Fixes #2092
2025-12-16 10:42:32 -05:00
Owen
1b4884afd8 Make sure to push changes 2025-12-15 22:13:56 -05:00
Owen
0c0ad7029f Batch and delay for large amounts of targets 2025-12-15 22:13:56 -05:00
Owen
10f1437496 Small visual adjustments 2025-12-15 22:13:56 -05:00
Owen
c44c1a5518 Add UI, update API, send to newt 2025-12-15 22:13:56 -05:00
Owen Schwartz
48110ccda3 Merge pull request #2080 from water-sucks/server-setup-token-env-var
feat(setup): allow declaring a server setup token through env variable
2025-12-15 21:04:05 -05:00
Varun Narravula
e94f21bc05 ci: parallelize test workflow 2025-12-15 21:03:47 -05:00
Owen Schwartz
65f8a414be Merge pull request #2084 from water-sucks/parallelize-test-ci-workflow
ci: parallelize test workflow
2025-12-15 20:57:48 -05:00
Fred KISSIE
8dad38775c 🐛 use /resource instead of /site-resource 2025-12-16 01:53:20 +01:00
Fred KISSIE
0d14cb853e ♻️ invalidate everything & fix use effect condition 2025-12-16 01:53:06 +01:00
Fred KISSIE
778e6bf623 💄 lower margin y 2025-12-16 00:27:24 +01:00
miloschwartz
5a960649db fix generate password reset code only shows for non internal users 2025-12-15 18:06:29 -05:00
Fred KISSIE
23a7688789 💄 more margin top 2025-12-15 23:51:06 +01:00
Owen
0e3b6b90b7 Send reply to email in support requests 2025-12-15 17:43:45 -05:00
Fred KISSIE
872bb557c2 💄 put save org settings button into the form 2025-12-15 23:36:13 +01:00
Fred KISSIE
9125a7bccb 🚧 org settings form 2025-12-15 23:18:28 +01:00
Fred KISSIE
5a0a8893e8 Merge branch 'dev' into refactor/save-button-positions 2025-12-15 17:04:58 +01:00
Varun Narravula
abe76e5002 ci: parallelize test workflow 2025-12-15 05:30:43 -08:00
Varun Narravula
474b9a685d feat(setup): allow declaring a server setup token through env variable 2025-12-14 16:24:17 -08:00
Owen
97631c068c Clean key
Ref #1806
2025-12-14 15:58:29 -05:00
Owen Schwartz
98c77ad7e2 Update README.md 2025-12-14 03:09:45 -05:00
Owen Schwartz
3915df3200 Merge pull request #2068 from mgruszkiewicz/fix-missing-gpg-in-installer
Fix: Add missing gnupg utility during Docker installation
2025-12-13 14:28:04 -05:00
Mateusz Gruszkiewicz
9b98acb553 fix missing gpg dependency which is preventing docker from installing correctly 2025-12-13 19:27:15 +01:00
Owen
a767a31c21 Quiet log message 2025-12-13 12:28:44 -05:00
Owen
f2d4c2f83c Remove duplicate target 2025-12-13 12:16:11 -05:00
Owen
25fed23758 Speed up build 2025-12-13 12:13:33 -05:00
Owen Schwartz
5cb3fa1127 Merge pull request #2066 from fosrl/dev
Dev
2025-12-13 12:09:22 -05:00
Owen
deac26bad2 Bump version 2025-12-13 12:07:35 -05:00
miloschwartz
c7747fd4b4 add license watermark 2025-12-13 11:45:15 -05:00
Owen
1aaad43871 Format 2025-12-13 11:36:53 -05:00
Owen
143175bde7 Update react-dom 2025-12-13 11:34:58 -05:00
Owen
9f55d6b20a Try to fix issue not sending newt commands 2025-12-13 11:19:42 -05:00
miloschwartz
4366ca5836 add spacing to delete modal 2025-12-13 10:57:24 -05:00
miloschwartz
9cb95576d0 Merge branch 'dev' into cicd 2025-12-12 23:08:06 -05:00
miloschwartz
d5307adef0 fix bug preventing save resource priority closes #2063 2025-12-12 22:52:00 -05:00
miloschwartz
3d857c3b52 fix client side pagination issue 2025-12-12 22:41:10 -05:00
Owen
a012369f83 Make sure to always check retention first
Fixes #2061
2025-12-12 18:39:13 -05:00
Fred KISSIE
9cee3d9c79 ♻️ refactor 2025-12-12 23:35:24 +01:00
Fred KISSIE
8257dca340 ♻️ refactor 2025-12-12 23:34:35 +01:00
Fred KISSIE
5e0a1cf9c5 💡remove comment 2025-12-12 22:09:37 +01:00
miloschwartz
b3ec9dfda2 split builds based on arch 2025-12-12 15:56:42 -05:00
Fred KISSIE
93d4f60314 ♻️correctly init the form 2025-12-12 21:55:23 +01:00
Fred KISSIE
769d20cea1 Merge branch 'dev' into refactor/save-button-positions 2025-12-12 21:42:06 +01:00
Fred KISSIE
124ba208de ♻️ use react querty 2025-12-12 21:40:49 +01:00
Owen
ba99614d58 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-12 14:54:59 -05:00
Owen
27db77bca4 Format 2025-12-12 14:53:26 -05:00
miloschwartz
29b924230f add runner restart action 2025-12-12 14:48:49 -05:00
Owen
8eb3f6aacc Bump next and react again
CVE-2025-55184 and CVE-2025-67779
2025-12-12 09:55:52 -05:00
Fred KISSIE
7f07ccea44 Merge branch 'dev' into refactor/save-button-positions 2025-12-12 00:32:02 +01:00
Fred KISSIE
c13bfc709f Merge branch 'dev' into refactor/save-button-positions 2025-12-11 23:35:00 +01:00
Fred KISSIE
6fc54bcc9e ♻️ set default value on domain picker modal in proxy resource page 2025-12-11 22:51:02 +01:00
Owen
5d6ee45125 Merge branch 'dev' 2025-12-11 16:49:40 -05:00
Owen Schwartz
fceaedfcd8 Merge pull request #2045 from Fredkiss3/fix/update-full-domain-on-resource-page
fix: full domain should be updated when the form is saved on resource proxy page
2025-12-11 16:49:22 -05:00
Fred KISSIE
181612ce25 🐛 full domain should be updated when the form is saved 2025-12-11 22:26:38 +01:00
Owen
224b78fc64 Update consts 2025-12-11 16:13:33 -05:00
Owen
757e540be6 Merge branch 'main' into dev 2025-12-11 16:12:08 -05:00
Milo Schwartz
bf1675686c Update README.md 2025-12-11 15:44:47 -05:00
miloschwartz
f81909489a add client telmetry and fix missing openapi on prefault 2025-12-11 10:38:48 -05:00
miloschwartz
963468d7fa remove top border from dialog 2025-12-11 10:17:17 -05:00
miloschwartz
f67f4f8834 update screenshots and readme 2025-12-10 21:13:09 -05:00
Owen
4c819d264b Only permit ipv4 for now 2025-12-10 20:40:22 -05:00
Owen Schwartz
cbcb23ccea Merge pull request #2036 from fosrl/dependabot/npm_and_yarn/dev-minor-updates-316ddb12fb
Bump @types/pg from 8.15.6 to 8.16.0 in the dev-minor-updates group
2025-12-10 20:35:49 -05:00
Owen Schwartz
d8b27de5ac Merge pull request #2038 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-0a59212de9
Bump the prod-minor-updates group across 1 directory with 4 updates
2025-12-10 20:35:43 -05:00
Owen
01f7842fd5 Fix function rename issue 2025-12-10 20:34:40 -05:00
Owen Schwartz
d409e58186 Merge pull request #2020 from Fredkiss3/fix/log-analytics-adjustments
refactor: adjustments for logs pages
2025-12-10 20:33:12 -05:00
Owen Schwartz
c9e1c4da1c Merge pull request #2026 from fosrl/crowdin_dev
New Crowdin updates
2025-12-10 20:25:31 -05:00
dependabot[bot]
9c38f65ad4 Bump the prod-minor-updates group across 1 directory with 4 updates
Bumps the prod-minor-updates group with 4 updates in the / directory: [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3), [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react), [npm](https://github.com/npm/cli) and [resend](https://github.com/resend/resend-node).


Updates `@aws-sdk/client-s3` from 3.947.0 to 3.948.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.948.0/clients/client-s3)

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

Updates `npm` from 11.6.4 to 11.7.0
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v11.6.4...v11.7.0)

Updates `resend` from 6.5.2 to 6.6.0
- [Release notes](https://github.com/resend/resend-node/releases)
- [Commits](https://github.com/resend/resend-node/compare/v6.5.2...v6.6.0)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.948.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.559.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: npm
  dependency-version: 11.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: resend
  dependency-version: 6.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 01:21:19 +00:00
dependabot[bot]
2316462721 Bump @types/pg from 8.15.6 to 8.16.0 in the dev-minor-updates group
Bumps the dev-minor-updates group with 1 update: [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg).


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

---
updated-dependencies:
- dependency-name: "@types/pg"
  dependency-version: 8.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 01:19:20 +00:00
Owen Schwartz
7cc990107a Merge pull request #2035 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-3ea7ca757b
Bump react-email from 5.0.6 to 5.0.7 in the dev-patch-updates group
2025-12-10 20:19:00 -05:00
dependabot[bot]
9917a569ac Bump react-email from 5.0.6 to 5.0.7 in the dev-patch-updates group
Bumps the dev-patch-updates group with 1 update: [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email).


Updates `react-email` from 5.0.6 to 5.0.7
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/react-email/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/react-email@5.0.7/packages/react-email)

---
updated-dependencies:
- dependency-name: react-email
  dependency-version: 5.0.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 01:18:06 +00:00
Fred KISSIE
aab0471b6b 🏷️ fix typescript errors 2025-12-10 21:26:55 +01:00
Fred KISSIE
de684b212f 🔇 remove console.log 2025-12-10 21:26:46 +01:00
Fred KISSIE
fbd3802e46 ♻️ Update domain picker component to accept default values 2025-12-10 21:17:00 +01:00
Fred KISSIE
4e842a660a 🚧 wip: refactor proxy resource page 2025-12-10 21:15:42 +01:00
Fred KISSIE
ce6b609ca2 ♻️ Update domain picker component to accept default values 2025-12-10 21:15:26 +01:00
David Reed
78369b6f6a Add OIDC authentication error response support 2025-12-10 11:13:04 -08:00
Fred KISSIE
ea43bf97c7 Merge branch 'dev' into refactor/save-button-positions 2025-12-10 20:04:59 +01:00
Owen
c56574e431 Send site add in case the client does not have the site 2025-12-10 11:57:45 -05:00
Fred KISSIE
f9c0e0ec3d 💬 updated text 2025-12-10 03:56:08 +01:00
Fred KISSIE
85986dcccb Merge branch 'dev' into fix/log-analytics-adjustments 2025-12-10 03:49:40 +01:00
Fred KISSIE
c9779254c3 💄add time range tooltip to explain it better 2025-12-10 03:44:46 +01:00
Fred KISSIE
5b620469c7 ♻️ set export logs limits to 50 000 everywhere 2025-12-10 03:42:53 +01:00
Fred KISSIE
df4b9de334 🚧 wip: export limits 2025-12-10 03:24:32 +01:00
Fred KISSIE
d490cab48c Merge dev into fix/log-analytics-adjustments 2025-12-10 03:19:14 +01:00
miloschwartz
b68c0962c6 visual enhancements 2025-12-09 20:58:45 -05:00
Owen
ee2a438602 Merge branch 'main' into dev 2025-12-09 16:26:21 -05:00
Owen
74dd3fdc9f Update packages 2025-12-09 16:18:20 -05:00
Owen
314da3ee3e Update formatting to work with ipv6 2025-12-09 16:11:12 -05:00
Owen Schwartz
68cfc84249 New translations en-us.json (German) 2025-12-09 14:09:22 -05:00
Owen
0bcf5c2b42 Update packages 2025-12-09 12:09:23 -05:00
Owen
9210e005e9 Merge branch 'main' into dev 2025-12-09 12:08:32 -05:00
Owen
f245632371 Fix expires at not updating 2025-12-09 11:50:48 -05:00
miloschwartz
6453b070bb add more resiliency to the license check 2025-12-09 11:26:11 -05:00
Owen Schwartz
8c4db93a93 Merge pull request #2024 from fosrl/dependabot/npm_and_yarn/multi-1eaea4558a
Bump next and @react-email/preview-server
2025-12-09 10:57:54 -05:00
Owen
f9b03943c3 Format all files 2025-12-09 10:56:14 -05:00
Owen
fa839a811f Merge branch 'Fredkiss3-chore/some-dx-changes' into dev 2025-12-09 10:54:35 -05:00
Owen
88d2c2eac8 Merge branch 'chore/some-dx-changes' of github.com:Fredkiss3/pangolin into Fredkiss3-chore/some-dx-changes 2025-12-09 10:54:28 -05:00
dependabot[bot]
c84cc1815b Bump next and @react-email/preview-server
Bumps [next](https://github.com/vercel/next.js) to 15.5.7 and updates ancestor dependency [@react-email/preview-server](https://github.com/resend/react-email/tree/HEAD/packages/preview-server). These dependencies need to be updated together.


Updates `next` from 15.5.2 to 15.5.7
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.2...v15.5.7)

Updates `@react-email/preview-server` from 4.3.2 to 5.0.6
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/preview-server/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/preview-server@5.0.6/packages/preview-server)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.7
  dependency-type: indirect
- dependency-name: "@react-email/preview-server"
  dependency-version: 5.0.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 15:50:48 +00:00
Owen
2c23ffd178 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-09 10:50:33 -05:00
Owen Schwartz
da3f7ae404 Merge pull request #2018 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-a5bcaae1b2
Bump the prod-minor-updates group across 1 directory with 4 updates
2025-12-09 10:49:02 -05:00
Owen Schwartz
f460559a4b Merge pull request #2021 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-cd2d8c1767
Bump @types/node from 24.10.1 to 24.10.2 in the dev-patch-updates group
2025-12-09 10:48:10 -05:00
Owen Schwartz
0c9deeb2d7 Merge pull request #2022 from fosrl/dependabot/go_modules/install/prod-minor-updates-f9eb33633d
Bump golang.org/x/term from 0.37.0 to 0.38.0 in /install in the prod-minor-updates group
2025-12-09 10:48:02 -05:00
Owen Schwartz
1289b99f14 Merge pull request #2009 from fosrl/dependabot/npm_and_yarn/stripe-20.0.0
Bump stripe from 18.2.1 to 20.0.0
2025-12-09 10:47:54 -05:00
Owen Schwartz
1a7a6e5b6f Merge pull request #2007 from fosrl/dependabot/npm_and_yarn/react-email/render-2.0.0
Bump @react-email/render from 1.4.0 to 2.0.0
2025-12-09 10:47:00 -05:00
Owen Schwartz
f56135eed3 Merge pull request #2011 from Lokowitz/fix-dev
Update packages
2025-12-09 10:46:32 -05:00
Owen
23e9a61f3e Fixing various bugs 2025-12-09 10:31:43 -05:00
Lokowitz
5428ad1009 merge upstream 2025-12-09 11:40:57 +00:00
Lokowitz
bba28bc5f2 Merge remote-tracking branch 'upstream/dev' into fix-dev 2025-12-09 11:40:04 +00:00
Owen
18498a32ce Quite log messages 2025-12-08 22:07:17 -05:00
Owen
887af85db1 Fix removing remote subnet on remove site resource 2025-12-08 22:06:37 -05:00
Owen
a306aa971b Pick client endpoint as part of the transation 2025-12-08 21:37:17 -05:00
Owen
0a9b19ecfc Try to fix deadlocks again
Fixes FOU-284
2025-12-08 21:26:23 -05:00
Owen
e011580b96 Update and add server version 2025-12-08 21:26:23 -05:00
miloschwartz
048ce850a8 get coutry using maxmind and clear stale device codes 2025-12-08 21:12:19 -05:00
dependabot[bot]
2ca1f15add Bump the prod-minor-updates group across 1 directory with 4 updates
Bumps the prod-minor-updates group with 4 updates in the / directory: [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi), [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3), [react-day-picker](https://github.com/gpbl/react-day-picker) and [winston](https://github.com/winstonjs/winston).


Updates `@asteasolutions/zod-to-openapi` from 8.1.0 to 8.2.0
- [Release notes](https://github.com/asteasolutions/zod-to-openapi/releases)
- [Commits](https://github.com/asteasolutions/zod-to-openapi/compare/v8.1.0...v8.2.0)

Updates `@aws-sdk/client-s3` from 3.943.0 to 3.946.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.946.0/clients/client-s3)

Updates `react-day-picker` from 9.11.3 to 9.12.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.11.3...v9.12.0)

Updates `winston` from 3.18.3 to 3.19.0
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/compare/v3.18.3...v3.19.0)

---
updated-dependencies:
- dependency-name: "@asteasolutions/zod-to-openapi"
  dependency-version: 8.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.946.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-day-picker
  dependency-version: 9.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: winston
  dependency-version: 3.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 01:23:08 +00:00
dependabot[bot]
05ebd547b5 Bump golang.org/x/term in /install in the prod-minor-updates group
Bumps the prod-minor-updates group in /install with 1 update: [golang.org/x/term](https://github.com/golang/term).


Updates `golang.org/x/term` from 0.37.0 to 0.38.0
- [Commits](https://github.com/golang/term/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 01:21:11 +00:00
dependabot[bot]
5a8b1383a4 Bump @types/node from 24.10.1 to 24.10.2 in the dev-patch-updates group
Bumps the dev-patch-updates group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.10.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 01:20:03 +00:00
miloschwartz
ede51bebb5 use semver to compare versions in product updates 2025-12-08 19:51:32 -05:00
Owen Schwartz
fd29071d57 Merge pull request #2004 from fosrl/dependabot/github_actions/actions/checkout-6.0.1
Bump actions/checkout from 6.0.0 to 6.0.1
2025-12-08 19:48:01 -05:00
Owen Schwartz
8e1af79dc4 Merge pull request #2003 from fosrl/dependabot/github_actions/actions/setup-node-6.1.0
Bump actions/setup-node from 6.0.0 to 6.1.0
2025-12-08 19:47:48 -05:00
Owen Schwartz
dc8c28626d Merge pull request #2002 from fosrl/dependabot/github_actions/actions/stale-10.1.1
Bump actions/stale from 10.1.0 to 10.1.1
2025-12-08 19:47:40 -05:00
Fred KISSIE
9db2feff77 ♻️ set default time to 7 days ago in API too 2025-12-09 00:17:34 +01:00
Fred KISSIE
adf76bfb53 ♻️ set default start time to 7 days ago 2025-12-08 23:56:28 +01:00
Fred KISSIE
e0a79b7d4d ♻️ set default log analytics time range to. 7days ago 2025-12-08 22:57:05 +01:00
Owen
b63a8fd3ed Merge branch 'dev' 2025-12-08 15:16:12 -05:00
Owen
ada3c6f2ef Update and add server version 2025-12-08 15:16:05 -05:00
Owen
aafca7694d Update and add server version 2025-12-08 15:15:42 -05:00
dependabot[bot]
9ea3914a93 Bump @react-email/render from 1.4.0 to 2.0.0
Bumps [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) from 1.4.0 to 2.0.0.
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/render/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/render@2.0.0/packages/render)

---
updated-dependencies:
- dependency-name: "@react-email/render"
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 19:13:04 +00:00
miloschwartz
4345669793 remove file 2025-12-08 14:12:46 -05:00
miloschwartz
1aeb31be04 remove file 2025-12-08 14:12:10 -05:00
Milo Schwartz
66cae9802d Merge pull request #2017 from fosrl/dev
1.13.0-rc.0
2025-12-08 11:10:36 -08:00
Fred KISSIE
64120ea878 🔨Add format script and install prettier 2025-12-08 19:57:08 +01:00
Fred KISSIE
0003ec021b 🔨add default vscode options for new contributors 2025-12-08 19:56:53 +01:00
Owen Schwartz
2325e30f26 Merge pull request #2015 from fosrl/crowdin_dev
New Crowdin updates
2025-12-08 11:57:17 -05:00
Owen Schwartz
d1c98cf650 New translations en-us.json (Norwegian Bokmal) 2025-12-08 11:56:43 -05:00
Owen Schwartz
d06cd9b5be New translations en-us.json (Chinese Simplified) 2025-12-08 11:56:42 -05:00
Owen Schwartz
2eb440d019 New translations en-us.json (Turkish) 2025-12-08 11:56:40 -05:00
Owen Schwartz
4084c85c00 New translations en-us.json (Russian) 2025-12-08 11:56:39 -05:00
Owen Schwartz
4fee65e5a4 New translations en-us.json (Portuguese) 2025-12-08 11:56:37 -05:00
Owen Schwartz
17ee51249c New translations en-us.json (Polish) 2025-12-08 11:56:36 -05:00
Owen Schwartz
f239c4370e New translations en-us.json (Dutch) 2025-12-08 11:56:34 -05:00
Owen Schwartz
c2a32a50cd New translations en-us.json (Korean) 2025-12-08 11:56:32 -05:00
Owen Schwartz
7229bfa51b New translations en-us.json (Italian) 2025-12-08 11:56:31 -05:00
Owen Schwartz
080e2f0a3a New translations en-us.json (German) 2025-12-08 11:56:28 -05:00
Owen Schwartz
64e5cc172d New translations en-us.json (Czech) 2025-12-08 11:56:27 -05:00
Owen Schwartz
c51a1c9c4d New translations en-us.json (Bulgarian) 2025-12-08 11:56:25 -05:00
Owen Schwartz
79958be380 New translations en-us.json (Spanish) 2025-12-08 11:56:22 -05:00
Owen Schwartz
05daedc6ad New translations en-us.json (French) 2025-12-08 11:56:21 -05:00
miloschwartz
0234234108 fix settings footer buttons break point on mobile 2025-12-08 10:38:29 -05:00
miloschwartz
f9b15b9156 add color to health check 2025-12-08 10:31:53 -05:00
miloschwartz
37830d211d use static.pangolin.net 2025-12-08 10:23:11 -05:00
Lokowitz
c9a1da210f revert my fix 2025-12-08 08:27:05 +00:00
Lokowitz
ace402af2d update packages 2025-12-08 08:23:32 +00:00
Lokowitz
e60dce25c9 Merge remote-tracking branch 'upstream/dev' into fix-dev
merge dev
2025-12-08 08:21:19 +00:00
Owen
24cdac95cd Fix not rebuilding site resources from blueprint 2025-12-07 22:13:26 -05:00
Owen
e10f7efcbe Fix blueprints zod update 2025-12-07 22:00:55 -05:00
Owen
1d7f4322e3 Migrations working 2025-12-07 21:14:36 -05:00
dependabot[bot]
ccfff030e5 Bump stripe from 18.2.1 to 20.0.0
Bumps [stripe](https://github.com/stripe/stripe-node) from 18.2.1 to 20.0.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/v18.2.1...v20.0.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-version: 20.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 01:25:10 +00:00
dependabot[bot]
00765c1faf Bump actions/checkout from 6.0.0 to 6.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](1af3b93b68...8e8c483db8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 01:16:59 +00:00
dependabot[bot]
f6bbdeadb9 Bump actions/setup-node from 6.0.0 to 6.1.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](2028fbc5c2...395ad32622)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 01:16:53 +00:00
dependabot[bot]
9cf520574a Bump actions/stale from 10.1.0 to 10.1.1
Bumps [actions/stale](https://github.com/actions/stale) from 10.1.0 to 10.1.1.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](5f858e3efb...997185467f)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 01:16:48 +00:00
Owen
e8f10b049e Generate resources for remote subnets 2025-12-07 20:04:30 -05:00
Owen
a3ba4fff54 Bump version to 1.13.0-rc.0 2025-12-07 17:57:22 -05:00
miloschwartz
eecfcd640c add pg and modify sqlite 2025-12-07 15:30:10 -05:00
Owen
40c38fa070 Clear the associations first 2025-12-07 15:19:40 -05:00
Owen
042c88ccb8 Calc session id correctly 2025-12-07 15:01:18 -05:00
Owen
5a60f66ae0 Update sqlite migration to update caches 2025-12-07 14:47:20 -05:00
Owen
4d665e8596 Try to fix the expires at problem 2025-12-07 14:30:06 -05:00
miloschwartz
9221bcf889 add disconnect button to clients 2025-12-07 13:50:05 -05:00
miloschwartz
2418813902 add sqlite migration 2025-12-07 12:58:01 -05:00
miloschwartz
f66a9bdd33 only show updates number if more than one 2025-12-07 11:47:19 -05:00
miloschwartz
bc7a1f4673 change translation 2025-12-07 11:45:58 -05:00
miloschwartz
9010803046 fix verifySiteAccess middleware 2025-12-07 11:40:06 -05:00
miloschwartz
311233b9f7 update remote node version col 2025-12-07 11:13:43 -05:00
miloschwartz
38203a0e7c adjustments to update notification 2025-12-07 11:10:44 -05:00
Owen
5e9d660e26 We need to generate a niceId every time we make a client 2025-12-07 11:07:08 -05:00
Owen
110e950476 Send site name 2025-12-07 10:51:38 -05:00
Lokowitz
f8ab5b7af7 update packages 2025-12-07 14:03:34 +00:00
Owen
4e7843c1f3 Fix null subdomain causing overwriting
Fixes #1645
2025-12-06 21:59:03 -05:00
miloschwartz
502d15b9dc fix newt version styling 2025-12-06 21:39:58 -05:00
Owen
71db29c09c Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-06 21:37:24 -05:00
Owen
8cced5011b Fix empty strip preventing create 2025-12-06 21:37:10 -05:00
Owen
a812dde026 Update lock 2025-12-06 21:37:05 -05:00
Owen Schwartz
58374f77c9 Merge pull request #1996 from fosrl/crowdin_dev
New Crowdin updates
2025-12-06 21:35:43 -05:00
miloschwartz
8df3fa0ac0 make product update text a more readable 2025-12-06 21:31:12 -05:00
Owen Schwartz
840e9914cb New translations en-us.json (Norwegian Bokmal) 2025-12-06 21:30:08 -05:00
Owen Schwartz
f30a4f3cfd New translations en-us.json (Chinese Simplified) 2025-12-06 21:30:07 -05:00
Owen Schwartz
27004f9d0c New translations en-us.json (Turkish) 2025-12-06 21:30:05 -05:00
Owen Schwartz
427638ed3d New translations en-us.json (Russian) 2025-12-06 21:30:04 -05:00
Owen Schwartz
350379b0c7 New translations en-us.json (Portuguese) 2025-12-06 21:30:03 -05:00
Owen Schwartz
cf80c9d45c New translations en-us.json (Polish) 2025-12-06 21:30:02 -05:00
Owen Schwartz
2d801b8ea5 New translations en-us.json (Dutch) 2025-12-06 21:30:00 -05:00
Owen Schwartz
f82d01d39b New translations en-us.json (Korean) 2025-12-06 21:29:59 -05:00
Owen Schwartz
e959ce1698 New translations en-us.json (Italian) 2025-12-06 21:29:58 -05:00
Owen Schwartz
25e176e8d5 New translations en-us.json (German) 2025-12-06 21:29:57 -05:00
Owen Schwartz
8df01eb13a New translations en-us.json (Czech) 2025-12-06 21:29:55 -05:00
Owen Schwartz
8d87f31bec New translations en-us.json (Bulgarian) 2025-12-06 21:29:54 -05:00
Owen Schwartz
2b3594a5ea New translations en-us.json (Spanish) 2025-12-06 21:29:52 -05:00
Owen Schwartz
72b7c8de0c New translations en-us.json (French) 2025-12-06 21:29:51 -05:00
miloschwartz
b329dbb585 add agent to table 2025-12-06 21:21:39 -05:00
miloschwartz
56d30ad6bd clean up targets input a little 2025-12-06 21:00:57 -05:00
Owen
e24a13fb11 Implement previous year delete 2025-12-06 20:49:53 -05:00
miloschwartz
d7e06161a8 use niceId for client routes 2025-12-06 20:31:20 -05:00
Owen
8a8c0edad3 Speed up writing to not block io operations 2025-12-06 17:54:39 -05:00
Owen
66fc8529c2 Update blueprints to support new clients 2025-12-06 17:32:49 -05:00
Owen
0beaadf512 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-06 17:24:16 -05:00
Owen
58177f4a02 Add niceid 2025-12-06 17:24:00 -05:00
miloschwartz
28725dd164 update logging 2025-12-06 16:18:44 -05:00
miloschwartz
1714140ee7 fix branding favicon and subtitle texts 2025-12-06 16:16:42 -05:00
Owen
6329c3d140 Merge branch 'main' into dev 2025-12-06 16:11:28 -05:00
Owen Schwartz
44113ad93a Merge pull request #1995 from fosrl/copilot/fix-crowdsec-setup-token
Fix: Display setup token after CrowdSec installation
2025-12-06 16:10:20 -05:00
Owen
ee1af459cc Add default to path for hc
Fixes #1905
2025-12-06 16:07:47 -05:00
copilot-swe-agent[bot]
69561caa74 Fix setup token display condition to include CrowdSec installation
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-12-06 20:57:57 +00:00
Owen Schwartz
6f03d099b8 New translations en-us.json (Norwegian Bokmal) 2025-12-06 15:43:29 -05:00
Owen Schwartz
1581b5cb74 New translations en-us.json (Chinese Simplified) 2025-12-06 15:43:28 -05:00
Owen Schwartz
e09ec56fad New translations en-us.json (Turkish) 2025-12-06 15:43:26 -05:00
Owen Schwartz
8bcad76eb5 New translations en-us.json (Russian) 2025-12-06 15:43:25 -05:00
Owen Schwartz
ff4a6b1d3f New translations en-us.json (Portuguese) 2025-12-06 15:43:24 -05:00
Owen Schwartz
07b04b2603 New translations en-us.json (Polish) 2025-12-06 15:43:23 -05:00
Owen Schwartz
54471c703c New translations en-us.json (Dutch) 2025-12-06 15:43:21 -05:00
Owen Schwartz
8a160ec0fe New translations en-us.json (Korean) 2025-12-06 15:43:20 -05:00
Owen Schwartz
15da2f130b New translations en-us.json (Italian) 2025-12-06 15:43:19 -05:00
Owen Schwartz
d64d2d6916 New translations en-us.json (German) 2025-12-06 15:43:18 -05:00
Owen Schwartz
68928843a5 New translations en-us.json (Czech) 2025-12-06 15:43:16 -05:00
Owen Schwartz
1228fddb01 New translations en-us.json (Bulgarian) 2025-12-06 15:43:15 -05:00
Owen Schwartz
3fa0b01c41 New translations en-us.json (Spanish) 2025-12-06 15:43:14 -05:00
Owen Schwartz
a4884f90a9 New translations en-us.json (French) 2025-12-06 15:43:12 -05:00
copilot-swe-agent[bot]
d7311ad947 Add setup token printing after CrowdSec installation
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-12-06 20:38:40 +00:00
copilot-swe-agent[bot]
1aa155a0af Initial plan 2025-12-06 20:35:40 +00:00
Owen Schwartz
4f1c207083 Merge pull request #1993 from fosrl/copilot/fix-install-script-no-reply-email
Make no-reply email address required in install script
2025-12-06 15:21:18 -05:00
Owen
dc6ee70eba Update lock, downgrade to node 24 2025-12-06 15:12:58 -05:00
miloschwartz
0f9f4dfaeb fix orgs query 2025-12-06 13:21:58 -05:00
miloschwartz
22941c0653 show credentials tab in oss 2025-12-06 13:11:53 -05:00
miloschwartz
d714f7d52c remove old component 2025-12-06 12:51:57 -05:00
Milo Schwartz
4f2dd92e81 Merge pull request #1988 from Fredkiss3/refactor/show-product-updates-conditionnally
refactor: Only show the product updates to an org admin or owner
2025-12-06 09:39:48 -08:00
Milo Schwartz
090706c816 Merge branch 'dev' into refactor/show-product-updates-conditionnally 2025-12-06 09:38:39 -08:00
miloschwartz
f449fdc7ec add optial disconnect on regenerate credentials 2025-12-06 12:36:31 -05:00
copilot-swe-agent[bot]
394d1503dd Make no-reply email address required when email is enabled
- Added helpful hint in prompt suggesting to use SMTP username
- Added validation to ensure no-reply email is not empty when email is enabled
- Applied gofmt formatting improvements

Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-12-06 17:35:11 +00:00
copilot-swe-agent[bot]
60380b70ed Initial plan 2025-12-06 17:27:20 +00:00
Owen Schwartz
cece7a59bf Merge pull request #1992 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-affb329a3f
Bump the prod-patch-updates group across 1 directory with 5 updates
2025-12-06 12:17:04 -05:00
miloschwartz
00174be8c0 show id in credential regen 2025-12-06 12:07:43 -05:00
miloschwartz
1d303feca2 restyle client regen credentials 2025-12-06 11:47:58 -05:00
Owen
3f4fae8f09 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-06 11:46:18 -05:00
Owen
dab795e94a Merge branch 'main' into dev 2025-12-06 11:45:46 -05:00
miloschwartz
bd2165c553 prevent modal flash 2025-12-06 11:44:47 -05:00
miloschwartz
646497cda0 improve site regenerate cred ui 2025-12-06 11:40:28 -05:00
Owen Schwartz
dbc046397b Merge pull request #1949 from marcschaeferger-org/fix-security/other
Fix multiple reported Security Issues
2025-12-06 11:24:01 -05:00
dependabot[bot]
fbafb48562 Bump the prod-patch-updates group across 1 directory with 5 updates
Bumps the prod-patch-updates group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.0.3` | `16.0.7` |
| [http-errors](https://github.com/jshttp/http-errors) | `2.0.0` | `2.0.1` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.0` | `19.2.1` |
| [react-day-picker](https://github.com/gpbl/react-day-picker) | `9.11.1` | `9.11.3` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.0` | `19.2.1` |



Updates `eslint-config-next` from 16.0.3 to 16.0.7
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.0.7/packages/eslint-config-next)

Updates `http-errors` from 2.0.0 to 2.0.1
- [Release notes](https://github.com/jshttp/http-errors/releases)
- [Changelog](https://github.com/jshttp/http-errors/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/http-errors/compare/v2.0.0...v2.0.1)

Updates `react` from 19.2.0 to 19.2.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.1/packages/react)

Updates `react-day-picker` from 9.11.1 to 9.11.3
- [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.11.1...v9.11.3)

Updates `react-dom` from 19.2.0 to 19.2.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.1/packages/react-dom)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: http-errors
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react
  dependency-version: 19.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-day-picker
  dependency-version: 9.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-dom
  dependency-version: 19.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 16:21:43 +00:00
Owen Schwartz
ccb17cdbbf Merge pull request #1991 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-0afe584c4c
Bump the dev-patch-updates group across 1 directory with 4 updates
2025-12-06 11:21:33 -05:00
Owen Schwartz
c56512dc7d Merge pull request #1881 from fosrl/dependabot/npm_and_yarn/react-email/tailwind-2.0.1
Bump @react-email/tailwind from 1.2.2 to 2.0.1
2025-12-06 11:20:50 -05:00
Owen Schwartz
a92edf519e Merge pull request #1900 from fosrl/copilot/fix-hc-health-status-unknown
Reset hcHealth to "unknown" when health check is disabled
2025-12-06 11:20:38 -05:00
dependabot[bot]
6cd3f2df1b Bump the dev-patch-updates group across 1 directory with 4 updates
Bumps the dev-patch-updates group with 4 updates in the / directory: [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer), [@types/yargs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/yargs), [drizzle-kit](https://github.com/drizzle-team/drizzle-orm) and [esbuild](https://github.com/evanw/esbuild).


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

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

Updates `drizzle-kit` from 0.31.6 to 0.31.8
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/drizzle-kit@0.31.6...drizzle-kit@0.31.8)

Updates `esbuild` from 0.27.0 to 0.27.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.0...v0.27.1)

---
updated-dependencies:
- dependency-name: "@types/nodemailer"
  dependency-version: 7.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@types/yargs"
  dependency-version: 17.0.35
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: drizzle-kit
  dependency-version: 0.31.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: esbuild
  dependency-version: 0.27.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 16:19:59 +00:00
Owen Schwartz
b9c0089fac Merge pull request #1913 from fosrl/dependabot/github_actions/actions/setup-go-6.1.0
Bump actions/setup-go from 6.0.0 to 6.1.0
2025-12-06 11:17:30 -05:00
Owen Schwartz
b2f78c9149 Merge pull request #1914 from fosrl/dependabot/github_actions/actions/checkout-6.0.0
Bump actions/checkout from 5.0.0 to 6.0.0
2025-12-06 11:17:23 -05:00
Owen Schwartz
2a361b010f Merge pull request #1962 from fosrl/dependabot/npm_and_yarn/nodemailer-7.0.11
Bump nodemailer from 7.0.10 to 7.0.11
2025-12-06 11:17:11 -05:00
Owen Schwartz
7bfa732a90 Merge pull request #1983 from fosrl/dependabot/npm_and_yarn/dev-minor-updates-42b6b37ac0
Bump the dev-minor-updates group across 1 directory with 2 updates
2025-12-06 11:16:53 -05:00
Owen Schwartz
c554364001 Merge pull request #1985 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-35f2f5c31e
Bump the prod-minor-updates group across 1 directory with 6 updates
2025-12-06 11:16:33 -05:00
Owen
5e52c48e77 Fix import 2025-12-05 21:48:45 -05:00
Owen
c233fc564e Fix credentials 2025-12-05 21:28:41 -05:00
Fred KISSIE
72bc26f0f8 💬 update texts to be more specific 2025-12-06 01:14:15 +01:00
Fred KISSIE
151cd3e6de Trigger Build 2025-12-06 01:01:48 +01:00
Fred KISSIE
97489b9564 ♻️ check by current Org 2025-12-06 00:58:11 +01:00
Fred KISSIE
d263d282ee Merge branch 'dev' into refactor/show-product-updates-conditionnally 2025-12-06 00:55:18 +01:00
Fred KISSIE
2ec2295cd6 ♻️ separate proxy page into multiple forms 2025-12-06 00:51:36 +01:00
Owen
d1c7832e40 Update rekey endpoints and pages 2025-12-05 18:30:43 -05:00
Owen
313d3c72da Fix query logging 2025-12-05 18:30:43 -05:00
Owen
c8ec94c307 Fix session test for olm and show proper alert for ee 2025-12-05 18:30:43 -05:00
Owen
4809b64f7d Reset package lock 2025-12-05 18:30:43 -05:00
miloschwartz
26e49ca39d simplify sidebar titles 2025-12-05 17:33:30 -05:00
miloschwartz
bb1472d25c prefill alias if enter hostname in destination 2025-12-05 17:14:39 -05:00
Fred KISSIE
a0a369dc43 ♻️ refactor reverse proxy targets page 2025-12-05 23:10:10 +01:00
Owen
8ea7b2ce02 Merge branch 'clients-user' into dev 2025-12-05 16:40:56 -05:00
Owen
1ee70e04ed Fix a couple things wiht destinations 2025-12-05 16:40:43 -05:00
Fred KISSIE
d0157ea7a5 Merge branch 'dev' into feat/login-page-customization 2025-12-05 22:38:07 +01:00
miloschwartz
d90f3bb6be remember sidebar expand 2025-12-05 16:34:44 -05:00
miloschwartz
149f4c1332 remove org id from olm command 2025-12-05 16:28:34 -05:00
miloschwartz
8e3b5688d5 allow server admins to generate password reset code 2025-12-05 16:27:08 -05:00
Fred KISSIE
bfd1293847 🚨 fix lint errors 2025-12-05 22:16:55 +01:00
Fred KISSIE
f4701f3da5 🐛 fix lockfile bugs with node version 2025-12-05 22:12:44 +01:00
Fred KISSIE
93af09ee97 📦 update lockfile 2025-12-05 21:56:01 +01:00
Fred KISSIE
897ddbec01 📦 fix lockfile 2025-12-05 21:41:52 +01:00
Fred KISSIE
889b381e96 ♻️ only show product updates if the user is an admin or the owner 2025-12-05 21:33:35 +01:00
miloschwartz
54c05c8345 show contact admin on forgot password if no smtp server 2025-12-05 15:23:26 -05:00
Owen Schwartz
8d62fb3865 Merge pull request #1986 from marcschaeferger/fix-ci
Fix Github CI Action
2025-12-05 10:25:09 -05:00
Marc Schäfer
6217086cd5 Add login step for GitHub Container Registry
Added GitHub Container Registry login step for cosign.
2025-12-05 15:54:59 +01:00
dependabot[bot]
6fbe25e91f Bump the prod-minor-updates group across 1 directory with 6 updates
Bumps the prod-minor-updates group with 6 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.922.0` | `3.943.0` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.44.7` | `0.45.0` |
| [express](https://github.com/expressjs/express) | `5.1.0` | `5.2.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.552.0` | `0.556.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.66.0` | `7.68.0` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge) | `3.3.1` | `3.4.0` |



Updates `@aws-sdk/client-s3` from 3.922.0 to 3.943.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.943.0/clients/client-s3)

Updates `drizzle-orm` from 0.44.7 to 0.45.0
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.44.7...0.45.0)

Updates `express` from 5.1.0 to 5.2.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/v5.1.0...v5.2.1)

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

Updates `react-hook-form` from 7.66.0 to 7.68.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.66.0...v7.68.0)

Updates `tailwind-merge` from 3.3.1 to 3.4.0
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.3.1...v3.4.0)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.943.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: drizzle-orm
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: express
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.556.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: tailwind-merge
  dependency-version: 3.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>
2025-12-05 14:49:26 +00:00
dependabot[bot]
57b3f49819 Bump the dev-minor-updates group across 1 directory with 2 updates
Bumps the dev-minor-updates group with 2 updates in the / directory: [esbuild-node-externals](https://github.com/pradel/esbuild-node-externals) and [tsx](https://github.com/privatenumber/tsx).


Updates `esbuild-node-externals` from 1.19.1 to 1.20.1
- [Release notes](https://github.com/pradel/esbuild-node-externals/releases)
- [Commits](https://github.com/pradel/esbuild-node-externals/compare/esbuild-node-externals-v1.19.1...esbuild-node-externals-v1.20.1)

Updates `tsx` from 4.20.6 to 4.21.0
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.20.6...v4.21.0)

---
updated-dependencies:
- dependency-name: esbuild-node-externals
  dependency-version: 1.20.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: tsx
  dependency-version: 4.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 14:45:55 +00:00
Fred KISSIE
d89f5279bf ♻️address PR feedback 2025-12-05 01:08:02 +01:00
Fred KISSIE
744305ab39 ♻️ refactor 2025-12-05 00:02:13 +01:00
Fred KISSIE
ba9048a377 Merge branch 'dev' into feat/login-page-customization 2025-12-04 23:56:16 +01:00
dependabot[bot]
2cdc23d63e Bump nodemailer from 7.0.10 to 7.0.11
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.10 to 7.0.11.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.10...v7.0.11)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 7.0.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 03:55:03 +00:00
Julien Breton
46ed27a218 Fix: Extend Basic Auth compatibility with browsers 2025-12-01 01:18:09 +01:00
Marc Schäfer
336d31ce39 fix(validators): restore 2+ char domain label requirement
- Replace (?:[a-z\\d-]{0,61}[a-z\\d])? with (?:[a-z\\d-]{1,61}[a-z\\d]) to keep labels 2–63 chars
- Avoid unintentionally allowing single-character labels (e.g. a.com) introduced by the previous regex change

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-30 02:41:03 +01:00
Marc Schäfer
8df62e8b6a Potential fix for code scanning alert no. 19: Inefficient regular expression
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-29 23:40:20 +01:00
Marc Schäfer
3eab3b0827 Potential fix for code scanning alert no. 8: DOM text reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-29 23:39:54 +01:00
Marc Schäfer
fbbab60956 Potential fix for code scanning alert no. 7: Insecure randomness
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-29 23:39:41 +01:00
dependabot[bot]
a8d11d78fc Bump actions/checkout from 5.0.0 to 6.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...1af3b93b68)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 01:27:21 +00:00
dependabot[bot]
e16aa6e90b Bump actions/setup-go from 6.0.0 to 6.1.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4469467582...4dc6199c7b)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 01:27:15 +00:00
copilot-swe-agent[bot]
be4d697dfe Set hcHealth to 'unknown' when health check is disabled in updateTarget
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-11-20 15:29:01 +00:00
copilot-swe-agent[bot]
94b34c489c Initial plan 2025-11-20 15:23:27 +00:00
Fred KISSIE
ff089ec6d7 📦update lockfile 2025-11-18 03:48:41 +01:00
Fred KISSIE
dc4f9a9bd1 ♻️ check for licence when checking for subscription 2025-11-18 03:32:05 +01:00
Fred KISSIE
e867de023a ♻️ load branding only if correctly subscribed 2025-11-18 03:14:20 +01:00
Fred KISSIE
e00c3f2193 🛂 check for subscription status 2025-11-18 02:46:22 +01:00
Fred KISSIE
8c30995228 ♻️ refactor 2025-11-18 02:38:08 +01:00
Fred KISSIE
3ba65a3311 ♻️ check for disabled features in general org settings page 2025-11-18 02:35:11 +01:00
dependabot[bot]
447b706909 Bump @react-email/tailwind from 1.2.2 to 2.0.1
Bumps [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) from 1.2.2 to 2.0.1.
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@2.0.1/packages/tailwind)

---
updated-dependencies:
- dependency-name: "@react-email/tailwind"
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 01:31:38 +00:00
Fred KISSIE
c5914dc0c0 ♻️ Also check for active subscription in paid status hook 2025-11-18 02:26:49 +01:00
Fred KISSIE
30f3ab11b2 🚚 rename SecurityFeaturesAlert to PaidFeaturesAlert 2025-11-18 02:26:25 +01:00
Fred KISSIE
66b01b764f ♻️ adapt zod schema to v4 and move form description below the inptu 2025-11-18 01:07:46 +01:00
Fred KISSIE
ee7e7778b6 ♻️commit 2025-11-17 22:23:11 +01:00
Fred KISSIE
0d0c43f72b Merge branch 'dev' into feat/login-page-customization 2025-11-17 22:18:32 +01:00
Fred KISSIE
83f36bce9d ♻️refactor 2025-11-17 22:17:55 +01:00
Fred KISSIE
2466d24c1a 🔥remove unused imports 2025-11-15 07:08:07 +01:00
Fred KISSIE
2f34def4d7 ♻️ correctly apply the CSS variable 2025-11-15 07:06:20 +01:00
Fred KISSIE
8e8f992876 💡add comment 2025-11-15 07:04:36 +01:00
Fred KISSIE
1d9ed9d219 💡remove useless comments 2025-11-15 07:01:27 +01:00
Fred KISSIE
616fb9c8e9 ♻️remove unused imports 2025-11-15 06:59:15 +01:00
Fred KISSIE
a2ab7191e5 🔇remove log 2025-11-15 06:58:05 +01:00
Fred KISSIE
7a31292ec7 revert package.json changes 2025-11-15 06:34:40 +01:00
Fred KISSIE
196fbbe334 📦update lockfile 2025-11-15 06:32:45 +01:00
Fred KISSIE
5bb5aeff36 Merge branch 'dev' into feat/login-page-customization 2025-11-15 06:32:03 +01:00
Fred KISSIE
2ada05b286 ♻️only apply org branding in saas 2025-11-15 06:26:17 +01:00
Fred KISSIE
87f23f582c apply branding to org auth page 2025-11-15 06:08:02 +01:00
Fred KISSIE
29a52f6ac4 🐛 Apply branding to auth page when not authenticated not only when authed 2025-11-15 05:43:17 +01:00
Fred KISSIE
790f7083e2 🐛 fix cols and some other refactors 2025-11-15 04:04:10 +01:00
Fred KISSIE
5c851e82ff ♻️refactor 2025-11-15 04:03:42 +01:00
Fred KISSIE
854f638da3 show toast message when updating auth page domain 2025-11-15 04:03:21 +01:00
Fred KISSIE
4842648e7b ♻️refactor 2025-11-15 02:38:51 +01:00
Fred KISSIE
8f152bdf9f add primary color branding to the page 2025-11-15 02:38:46 +01:00
Fred KISSIE
d003436179 ⚗️ generate build variable as fully typed to prevent typos (to check if it's ok) 2025-11-15 01:43:58 +01:00
Fred KISSIE
9776ef43ea ♻️ only include org settings in saas build 2025-11-15 01:42:20 +01:00
Fred KISSIE
e2c4a906c4 ♻️rename title & subtitle to orgTitle and orgSubtitle 2025-11-15 01:41:56 +01:00
Fred KISSIE
27e8250cd1 ♻️some refactor 2025-11-15 01:07:07 +01:00
Fred KISSIE
0d84b7af6e ♻️show org page branding section only in saas 2025-11-15 01:07:00 +01:00
Fred KISSIE
b961271aa6 ♻️ some refactor 2025-11-15 01:06:22 +01:00
Fred KISSIE
b505cc60b0 🗃️ Add primaryColor to login page branding 2025-11-15 01:06:09 +01:00
Fred KISSIE
955f927c59 🚧WIP 2025-11-14 01:24:15 +01:00
Fred KISSIE
4beed9d464 apply auth branding to resource auth page 2025-11-13 03:24:47 +01:00
Fred KISSIE
228481444f ♻️ do not manually track the loading state in ConfirmDeleteDialog 2025-11-13 02:19:25 +01:00
Fred KISSIE
02cd2cfb17 save and update branding 2025-11-13 02:18:52 +01:00
Fred KISSIE
d218a4bbc3 🏷️ fix types 2025-11-12 03:50:11 +01:00
Fred KISSIE
4bd1c4e0c6 ♻️ refactor 2025-11-12 03:50:04 +01:00
Fred KISSIE
cfde4e7443 🚧 WIP 2025-11-12 03:43:19 +01:00
Fred KISSIE
f58cf68f7c 🚧 WIP 2025-11-11 23:35:20 +01:00
Fred KISSIE
08e43400e4 🚧 frontend wip 2025-11-11 21:14:10 +01:00
Fred KISSIE
46d60bd090 ♻️ add type 2025-11-11 17:08:52 +01:00
Fred KISSIE
5641a2aa31 🗃️ add org auth page model 2025-11-11 17:08:27 +01:00
Fred KISSIE
0abc561bb8 ♻️ refactor 2025-11-11 02:22:26 +01:00
744 changed files with 35538 additions and 20813 deletions

View File

@@ -1,6 +1,3 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
"extends": ["next/core-web-vitals", "next/typescript"]
}

View File

@@ -17,16 +17,42 @@ on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+.rc.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build and Release
runs-on: [self-hosted, linux, x64]
pre-run:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instances
run: |
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances started"
release-arm:
name: Build and Release (ARM64)
runs-on: [self-hosted, linux, arm64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
@@ -36,13 +62,19 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -50,13 +82,189 @@ jobs:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *".rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Build and push Docker images (Docker Hub - ARM64)
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make build-rc-arm tag=$TAG
else
make build-release-arm tag=$TAG
fi
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
release-amd:
name: Build and Release (AMD64)
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *".rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Build and push Docker images (Docker Hub - AMD64)
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make build-rc-amd tag=$TAG
else
make build-release-amd tag=$TAG
fi
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
create-manifest:
name: Create Multi-Arch Manifests
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [release-arm, release-amd]
if: >-
${{
needs.release-arm.result == 'success' &&
needs.release-amd.result == 'success'
}}
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *".rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Create multi-arch manifests
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make create-manifests-rc tag=$TAG
else
make create-manifests tag=$TAG
fi
echo "Created multi-arch manifests for tag: ${TAG}"
shell: bash
sign-and-package:
name: Sign and Package
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [release-arm, release-amd, create-manifest]
if: >-
${{
needs.release-arm.result == 'success' &&
needs.release-amd.result == 'success' &&
needs.create-manifest.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version: 1.24
@@ -96,21 +304,14 @@ jobs:
- name: Build installer
working-directory: install
run: |
make go-build-release
make go-build-release
- name: Upload artifacts from /install/bin
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: install-bin
path: install/bin/
- name: Build and push Docker images (Docker Hub)
run: |
TAG=${{ env.TAG }}
make build-release tag=$TAG
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
- name: Install skopeo + jq
# skopeo: copy/inspect images between registries
# jq: JSON parsing tool used to extract digest values
@@ -127,15 +328,25 @@ jobs:
- name: Copy tag from Docker Hub to GHCR
# Mirror the already-built image (all architectures) to GHCR so we can sign it
# Wait a bit for both architectures to be available in Docker Hub manifest
run: |
set -euo pipefail
TAG=${{ env.TAG }}
echo "Waiting for multi-arch manifest to be ready..."
sleep 30
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:$TAG \
docker://$GHCR_IMAGE:$TAG
shell: bash
- name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign
# cosign is used to sign and verify container images (key and keyless)
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
@@ -178,3 +389,33 @@ jobs:
"${REF}" -o text
done
shell: bash
post-run:
needs: [pre-run, release-arm, release-amd, create-manifest, sign-and-package]
if: >-
${{
always() &&
needs.pre-run.result == 'success' &&
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
(needs.create-manifest.result == 'success' || needs.create-manifest.result == 'skipped' || needs.create-manifest.result == 'failure') &&
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
}}
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Stop EC2 instances
run: |
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances stopped"

View File

@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: '22'

39
.github/workflows/restart-runners.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Restart Runners
on:
schedule:
- cron: '0 0 */7 * *'
permissions:
id-token: write
contents: read
jobs:
ec2-maintenance-prod:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instance
run: |
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances started"
- name: Wait
run: sleep 600
- name: Stop EC2 instance
run: |
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances stopped"

View File

@@ -14,7 +14,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
days-before-stale: 14
days-before-close: 14

View File

@@ -12,11 +12,12 @@ on:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- name: Install Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: '22'
@@ -57,8 +58,26 @@ jobs:
echo "App failed to start"
exit 1
build-sqlite:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Copy config file
run: cp config/config.example.yml config/config.yml
- name: Build Docker image sqlite
run: make build-sqlite
run: make dev-build-sqlite
build-postgres:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Copy config file
run: cp config/config.example.yml config/config.yml
- name: Build Docker image pg
run: make build-pg
run: make dev-build-pg

2
.nvmrc
View File

@@ -1 +1 @@
25
24

12
.prettierignore Normal file
View File

@@ -0,0 +1,12 @@
.github/
bruno/
cli/
config/
messages/
next.config.mjs/
public/
tailwind.config.js/
test/
**/*.yml
**/*.yaml
**/*.md

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode"]
}

22
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
"editor.codeActionsOnSave": {
"source.addMissingImports.ts": "always"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true
}

96
:w Normal file
View File

@@ -0,0 +1,96 @@
import { db } from "@server/db/pg/driver";
import { sql } from "drizzle-orm";
import { __DIRNAME } from "@server/lib/consts";
const version = "1.14.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
try {
await db.execute(sql`BEGIN`);
await db.execute(sql`
CREATE TABLE "loginPageBranding" (
"loginPageBrandingId" serial PRIMARY KEY NOT NULL,
"logoUrl" text NOT NULL,
"logoWidth" integer NOT NULL,
"logoHeight" integer NOT NULL,
"primaryColor" text,
"resourceTitle" text NOT NULL,
"resourceSubtitle" text,
"orgTitle" text,
"orgSubtitle" text
);
`);
await db.execute(sql`
CREATE TABLE "loginPageBrandingOrg" (
"loginPageBrandingId" integer NOT NULL,
"orgId" varchar NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "resourceHeaderAuthExtendedCompatibility" (
"headerAuthExtendedCompatibilityId" serial PRIMARY KEY NOT NULL,
"resourceId" integer NOT NULL,
"extendedCompatibilityIsActivated" boolean DEFAULT false NOT NULL
);
`);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceModeEnabled" boolean DEFAULT false NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceModeType" text DEFAULT 'forced';`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceTitle" text;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceMessage" text;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceEstimatedTime" text;`
);
await db.execute(
sql`ALTER TABLE "siteResources" ADD COLUMN "tcpPortRangeString" varchar;`
);
await db.execute(
sql`ALTER TABLE "siteResources" ADD COLUMN "udpPortRangeString" varchar;`
);
await db.execute(
sql`ALTER TABLE "siteResources" ADD COLUMN "disableIcmp" boolean DEFAULT false NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "loginPageBrandingOrg" ADD CONSTRAINT "loginPageBrandingOrg_loginPageBrandingId_loginPageBranding_loginPageBrandingId_fk" FOREIGN KEY ("loginPageBrandingId") REFERENCES "public"."loginPageBranding"("loginPageBrandingId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "loginPageBrandingOrg" ADD CONSTRAINT "loginPageBrandingOrg_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourceHeaderAuthExtendedCompatibility" ADD CONSTRAINT "resourceHeaderAuthExtendedCompatibility_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(sql`COMMIT`);
console.log("Migrated database");
} catch (e) {
await db.execute(sql`ROLLBACK`);
console.log("Unable to migrate database");
console.log(e);
throw e;
}
console.log(`${version} migration complete`);
}

View File

@@ -1,4 +1,4 @@
FROM node:25-alpine AS builder
FROM node:24-alpine AS builder
WORKDIR /app
@@ -43,23 +43,25 @@ RUN test -f dist/server.mjs
RUN npm run build:cli
FROM node:25-alpine AS runner
# Prune dev dependencies and clean up to prepare for copy to runner
RUN npm prune --omit=dev && npm cache clean --force
FROM node:24-alpine AS runner
WORKDIR /app
# Curl used for the health checks
# Python and build tools needed for better-sqlite3 native compilation
RUN apk add --no-cache curl tzdata python3 make g++
# Only curl and tzdata needed at runtime - no build tools!
RUN apk add --no-cache curl tzdata
# COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Copy pre-built node_modules from builder (already pruned to production only)
# This includes the compiled native modules like better-sqlite3
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/init ./dist/init
COPY --from=builder /app/package.json ./package.json
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs

247
Makefile
View File

@@ -1,8 +1,13 @@
.PHONY: build build-pg build-release build-arm build-x86 test clean
.PHONY: build build-pg build-release build-release-arm build-release-amd create-manifests build-arm build-x86 test clean
major_tag := $(shell echo $(tag) | cut -d. -f1)
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
build-release:
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
build-sqlite:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \
@@ -16,6 +21,12 @@ build-release:
--tag fosrl/pangolin:$(minor_tag) \
--tag fosrl/pangolin:$(tag) \
--push .
build-postgresql:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \
fi
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
@@ -25,6 +36,12 @@ build-release:
--tag fosrl/pangolin:postgresql-$(minor_tag) \
--tag fosrl/pangolin:postgresql-$(tag) \
--push .
build-ee-sqlite:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \
fi
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
@@ -34,6 +51,12 @@ build-release:
--tag fosrl/pangolin:ee-$(minor_tag) \
--tag fosrl/pangolin:ee-$(tag) \
--push .
build-ee-postgresql:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \
fi
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
@@ -44,6 +67,135 @@ build-release:
--tag fosrl/pangolin:ee-postgresql-$(tag) \
--push .
build-release-arm:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
exit 1; \
fi
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--platform linux/arm64 \
--tag fosrl/pangolin:latest-arm64 \
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
--tag fosrl/pangolin:$$MINOR_TAG-arm64 \
--tag fosrl/pangolin:$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--platform linux/arm64 \
--tag fosrl/pangolin:postgresql-latest-arm64 \
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-arm64 \
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-latest-arm64 \
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
--tag fosrl/pangolin:ee-$$MINOR_TAG-arm64 \
--tag fosrl/pangolin:ee-$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-arm64 \
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
--push .
build-release-amd:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release-amd tag=<tag>"; \
exit 1; \
fi
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--platform linux/amd64 \
--tag fosrl/pangolin:latest-amd64 \
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
--tag fosrl/pangolin:$$MINOR_TAG-amd64 \
--tag fosrl/pangolin:$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--platform linux/amd64 \
--tag fosrl/pangolin:postgresql-latest-amd64 \
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-amd64 \
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-latest-amd64 \
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
--tag fosrl/pangolin:ee-$$MINOR_TAG-amd64 \
--tag fosrl/pangolin:ee-$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-amd64 \
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
--push .
create-manifests:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make create-manifests tag=<tag>"; \
exit 1; \
fi
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
echo "Creating multi-arch manifests for sqlite (oss)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:latest \
--tag fosrl/pangolin:$$MAJOR_TAG \
--tag fosrl/pangolin:$$MINOR_TAG \
--tag fosrl/pangolin:$(tag) \
fosrl/pangolin:latest-arm64 \
fosrl/pangolin:latest-amd64 && \
echo "Creating multi-arch manifests for postgresql (oss)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:postgresql-latest \
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
--tag fosrl/pangolin:postgresql-$(tag) \
fosrl/pangolin:postgresql-latest-arm64 \
fosrl/pangolin:postgresql-latest-amd64 && \
echo "Creating multi-arch manifests for sqlite (enterprise)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:ee-latest \
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
--tag fosrl/pangolin:ee-$$MINOR_TAG \
--tag fosrl/pangolin:ee-$(tag) \
fosrl/pangolin:ee-latest-arm64 \
fosrl/pangolin:ee-latest-amd64 && \
echo "Creating multi-arch manifests for postgresql (enterprise)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:ee-postgresql-latest \
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG \
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG \
--tag fosrl/pangolin:ee-postgresql-$(tag) \
fosrl/pangolin:ee-postgresql-latest-arm64 \
fosrl/pangolin:ee-postgresql-latest-amd64 && \
echo "All multi-arch manifests created successfully!"
build-rc:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
@@ -74,16 +226,103 @@ build-rc:
--tag fosrl/pangolin:ee-postgresql-$(tag) \
--push .
build-rc-arm:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
exit 1; \
fi
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--platform linux/arm64 \
--tag fosrl/pangolin:$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--platform linux/arm64 \
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
--push .
build-rc-amd:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
exit 1; \
fi
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--platform linux/amd64 \
--tag fosrl/pangolin:$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--platform linux/amd64 \
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
--push .
create-manifests-rc:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make create-manifests-rc tag=<tag>"; \
exit 1; \
fi
@echo "Creating multi-arch manifests for RC sqlite (oss)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:$(tag) \
fosrl/pangolin:$(tag)-arm64 \
fosrl/pangolin:$(tag)-amd64 && \
echo "Creating multi-arch manifests for RC postgresql (oss)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:postgresql-$(tag) \
fosrl/pangolin:postgresql-$(tag)-arm64 \
fosrl/pangolin:postgresql-$(tag)-amd64 && \
echo "Creating multi-arch manifests for RC sqlite (enterprise)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:ee-$(tag) \
fosrl/pangolin:ee-$(tag)-arm64 \
fosrl/pangolin:ee-$(tag)-amd64 && \
echo "Creating multi-arch manifests for RC postgresql (enterprise)..." && \
docker buildx imagetools create \
--tag fosrl/pangolin:ee-postgresql-$(tag) \
fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
fosrl/pangolin:ee-postgresql-$(tag)-amd64 && \
echo "All RC multi-arch manifests created successfully!"
build-arm:
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
build-x86:
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
build-sqlite:
dev-build-sqlite:
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
build-pg:
dev-build-pg:
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
test:

View File

@@ -31,7 +31,7 @@
[![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://pangolin.net/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)
[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@pangolin-net)
</div>
@@ -41,7 +41,7 @@
</strong>
</p>
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 an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control.
## Installation
@@ -60,14 +60,20 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and contex
## Key Features
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
| <img width=500 /> | <img width=500 /> |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
| **Reverse proxy across networks anywhere**<br /><br />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. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
| **Quickly connect Pangolin sites**<br /><br />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. | <img src="public/clip.gif" width=500 /><tr></tr> |
| **Connect remote networks with sites**<br /><br />Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | <img src="public/clip.gif" width=500 /><tr></tr> |
| **Client-based private resource access**<br /><br />Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. | <img src="public/screenshots/private-resources.png" width=500 /><tr></tr> |
| **Zero-trust granular access**<br /><br />Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications and services you explicitly define, reducing security risk and attack surface. | <img src="public/screenshots/user-devices.png" width=500 /><tr></tr> |
## Download Clients
Download the Pangolin client for your platform:
- [Mac](https://pangolin.net/downloads/mac)
- [Windows](https://pangolin.net/downloads/windows)
- [Linux](https://pangolin.net/downloads/linux)
## Get Started

View File

@@ -0,0 +1,36 @@
import { CommandModule } from "yargs";
import { db, exitNodes } from "@server/db";
import { eq } from "drizzle-orm";
type ClearExitNodesArgs = { };
export const clearExitNodes: CommandModule<
{},
ClearExitNodesArgs
> = {
command: "clear-exit-nodes",
describe:
"Clear all exit nodes from the database",
// no args
builder: (yargs) => {
return yargs;
},
handler: async (argv: {}) => {
try {
console.log(`Clearing all exit nodes from the database`);
// Delete all exit nodes
const deletedCount = await db
.delete(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all
console.log(`Deleted ${deletedCount.length} exit node(s) from the database`);
process.exit(0);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
};

View File

@@ -0,0 +1,284 @@
import { CommandModule } from "yargs";
import { db, idpOidcConfig, licenseKey } from "@server/db";
import { encrypt, decrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { eq } from "drizzle-orm";
import fs from "fs";
import yaml from "js-yaml";
type RotateServerSecretArgs = {
"old-secret": string;
"new-secret": string;
force?: boolean;
};
export const rotateServerSecret: CommandModule<
{},
RotateServerSecretArgs
> = {
command: "rotate-server-secret",
describe:
"Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
builder: (yargs) => {
return yargs
.option("old-secret", {
type: "string",
demandOption: true,
describe: "The current server secret (for verification)"
})
.option("new-secret", {
type: "string",
demandOption: true,
describe: "The new server secret to use"
})
.option("force", {
type: "boolean",
default: false,
describe:
"Force rotation even if the old secret doesn't match the config file. " +
"Use this if you know the old secret is correct but the config file is out of sync. " +
"WARNING: This will attempt to decrypt all values with the provided old secret. " +
"If the old secret is incorrect, the rotation will fail or corrupt data."
});
},
handler: async (argv: {
"old-secret": string;
"new-secret": string;
force?: boolean;
}) => {
try {
// Determine which config file exists
const configPath = fs.existsSync(configFilePath1)
? configFilePath1
: fs.existsSync(configFilePath2)
? configFilePath2
: null;
if (!configPath) {
console.error(
"Error: Config file not found. Expected config.yml or config.yaml in the config directory."
);
process.exit(1);
}
// Read current config
const configContent = fs.readFileSync(configPath, "utf8");
const config = yaml.load(configContent) as any;
if (!config?.server?.secret) {
console.error(
"Error: No server secret found in config file. Cannot rotate."
);
process.exit(1);
}
const configSecret = config.server.secret;
const oldSecret = argv["old-secret"];
const newSecret = argv["new-secret"];
const force = argv.force || false;
// Verify that the provided old secret matches the one in config
if (configSecret !== oldSecret) {
if (!force) {
console.error(
"Error: The provided old secret does not match the secret in the config file."
);
console.error(
"\nIf you are certain the old secret is correct and the config file is out of sync,"
);
console.error(
"you can use the --force flag to bypass this check."
);
console.error(
"\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail"
);
console.error(
"or corrupt encrypted data. Only use --force if you are absolutely certain."
);
process.exit(1);
} else {
console.warn(
"\nWARNING: Using --force flag. Bypassing old secret verification."
);
console.warn(
"The provided old secret does not match the config file, but proceeding anyway."
);
console.warn(
"If the old secret is incorrect, this operation will fail or corrupt data.\n"
);
}
}
// Validate new secret
if (newSecret.length < 8) {
console.error(
"Error: New secret must be at least 8 characters long"
);
process.exit(1);
}
if (oldSecret === newSecret) {
console.error("Error: New secret must be different from old secret");
process.exit(1);
}
console.log("Starting server secret rotation...");
console.log("This will decrypt and re-encrypt all encrypted values in the database.");
// Read all data first
console.log("\nReading encrypted data from database...");
const idpConfigs = await db.select().from(idpOidcConfig);
const licenseKeys = await db.select().from(licenseKey);
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
console.log(`Found ${licenseKeys.length} license key(s)`);
// Prepare all decrypted and re-encrypted values
console.log("\nDecrypting and re-encrypting values...");
type IdpUpdate = {
idpOauthConfigId: number;
encryptedClientId: string;
encryptedClientSecret: string;
};
type LicenseKeyUpdate = {
oldLicenseKeyId: string;
newLicenseKeyId: string;
encryptedToken: string;
encryptedInstanceId: string;
};
const idpUpdates: IdpUpdate[] = [];
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
// Process idpOidcConfig entries
for (const idpConfig of idpConfigs) {
try {
// Decrypt with old secret
const decryptedClientId = decrypt(idpConfig.clientId, oldSecret);
const decryptedClientSecret = decrypt(
idpConfig.clientSecret,
oldSecret
);
// Re-encrypt with new secret
const encryptedClientId = encrypt(decryptedClientId, newSecret);
const encryptedClientSecret = encrypt(
decryptedClientSecret,
newSecret
);
idpUpdates.push({
idpOauthConfigId: idpConfig.idpOauthConfigId,
encryptedClientId,
encryptedClientSecret
});
} catch (error) {
console.error(
`Error processing IdP config ${idpConfig.idpOauthConfigId}:`,
error
);
throw error;
}
}
// Process licenseKey entries
for (const key of licenseKeys) {
try {
// Decrypt with old secret
const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret);
const decryptedToken = decrypt(key.token, oldSecret);
const decryptedInstanceId = decrypt(key.instanceId, oldSecret);
// Re-encrypt with new secret
const encryptedLicenseKeyId = encrypt(
decryptedLicenseKeyId,
newSecret
);
const encryptedToken = encrypt(decryptedToken, newSecret);
const encryptedInstanceId = encrypt(
decryptedInstanceId,
newSecret
);
licenseKeyUpdates.push({
oldLicenseKeyId: key.licenseKeyId,
newLicenseKeyId: encryptedLicenseKeyId,
encryptedToken,
encryptedInstanceId
});
} catch (error) {
console.error(
`Error processing license key ${key.licenseKeyId}:`,
error
);
throw error;
}
}
// Perform all database updates in a single transaction
console.log("\nUpdating database in transaction...");
await db.transaction(async (trx) => {
// Update idpOidcConfig entries
for (const update of idpUpdates) {
await trx
.update(idpOidcConfig)
.set({
clientId: update.encryptedClientId,
clientSecret: update.encryptedClientSecret
})
.where(
eq(
idpOidcConfig.idpOauthConfigId,
update.idpOauthConfigId
)
);
}
// Update licenseKey entries (delete old, insert new)
for (const update of licenseKeyUpdates) {
// Delete old entry
await trx
.delete(licenseKey)
.where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId));
// Insert new entry with re-encrypted values
await trx.insert(licenseKey).values({
licenseKeyId: update.newLicenseKeyId,
token: update.encryptedToken,
instanceId: update.encryptedInstanceId
});
}
});
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
// Update config file with new secret
console.log("\nUpdating config file...");
config.server.secret = newSecret;
const newConfigContent = yaml.dump(config, {
indent: 2,
lineWidth: -1
});
fs.writeFileSync(configPath, newConfigContent, "utf8");
console.log(`Updated config file: ${configPath}`);
console.log("\nServer secret rotation completed successfully!");
console.log(`\nSummary:`);
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
console.log(` - License keys: ${licenseKeyUpdates.length}`);
console.log(
`\n IMPORTANT: Restart the server for the new secret to take effect.`
);
process.exit(0);
} catch (error) {
console.error("Error rotating server secret:", error);
process.exit(1);
}
}
};

View File

@@ -4,10 +4,14 @@ import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
import { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret";
yargs(hideBin(process.argv))
.scriptName("pangctl")
.command(setAdminCredentials)
.command(resetUserSecurityKeys)
.command(clearExitNodes)
.command(rotateServerSecret)
.demandCommand()
.help().argv;

View File

@@ -17,4 +17,4 @@
"lib": "@/lib",
"hooks": "@/hooks"
}
}
}

View File

@@ -11,7 +11,7 @@ experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.2.0"
version: "v1.3.0"
log:
level: "DEBUG"

View File

@@ -1,9 +1,7 @@
import { defineConfig } from "drizzle-kit";
import path from "path";
const schema = [
path.join("server", "db", "pg", "schema"),
];
const schema = [path.join("server", "db", "pg", "schema")];
export default defineConfig({
dialect: "postgresql",

View File

@@ -2,9 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
import { defineConfig } from "drizzle-kit";
import path from "path";
const schema = [
path.join("server", "db", "sqlite", "schema"),
];
const schema = [path.join("server", "db", "sqlite", "schema")];
export default defineConfig({
dialect: "sqlite",

View File

@@ -24,20 +24,20 @@ const argv = yargs(hideBin(process.argv))
alias: "e",
describe: "Entry point file",
type: "string",
demandOption: true,
demandOption: true
})
.option("out", {
alias: "o",
describe: "Output file path",
type: "string",
demandOption: true,
demandOption: true
})
.option("build", {
alias: "b",
describe: "Build type (oss, saas, enterprise)",
type: "string",
choices: ["oss", "saas", "enterprise"],
default: "oss",
default: "oss"
})
.help()
.alias("help", "h").argv;
@@ -66,7 +66,9 @@ function privateImportGuardPlugin() {
// Check if the importing file is NOT in server/private
const normalizedImporter = path.normalize(importingFile);
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
const isInServerPrivate = normalizedImporter.includes(
path.normalize("server/private")
);
if (!isInServerPrivate) {
const violation = {
@@ -79,8 +81,8 @@ function privateImportGuardPlugin() {
console.log(`PRIVATE IMPORT VIOLATION:`);
console.log(` File: ${importingFile}`);
console.log(` Import: ${args.path}`);
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
console.log('');
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
console.log("");
}
// Return null to let the default resolver handle it
@@ -89,16 +91,20 @@ function privateImportGuardPlugin() {
build.onEnd((result) => {
if (violations.length > 0) {
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
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(
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
);
});
console.log('');
console.log("");
result.errors.push({
text: `Private import violations detected: ${violations.length} violation(s) found`,
location: null,
notes: violations.map(v => ({
notes: violations.map((v) => ({
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
location: null
}))
@@ -121,7 +127,9 @@ function dynamicImportGuardPlugin() {
// Check if the importing file is NOT in server/private
const normalizedImporter = path.normalize(importingFile);
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
const isInServerPrivate = normalizedImporter.includes(
path.normalize("server/private")
);
if (isInServerPrivate) {
const violation = {
@@ -134,8 +142,8 @@ function dynamicImportGuardPlugin() {
console.log(`DYNAMIC IMPORT VIOLATION:`);
console.log(` File: ${importingFile}`);
console.log(` Import: ${args.path}`);
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
console.log('');
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
console.log("");
}
// Return null to let the default resolver handle it
@@ -144,16 +152,20 @@ function dynamicImportGuardPlugin() {
build.onEnd((result) => {
if (violations.length > 0) {
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
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(
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
);
});
console.log('');
console.log("");
result.errors.push({
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
location: null,
notes: violations.map(v => ({
notes: violations.map((v) => ({
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
location: null
}))
@@ -172,21 +184,28 @@ function dynamicImportSwitcherPlugin(buildValue) {
const switches = [];
build.onStart(() => {
console.log(`Dynamic import switcher using build type: ${buildValue}`);
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\//, '');
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") {
} 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/`);
console.warn(
`Unknown build type '${buildValue}', defaulting to #open/`
);
replacement = `#open/${dynamicPath}`;
}
@@ -201,8 +220,10 @@ function dynamicImportSwitcherPlugin(buildValue) {
console.log(`DYNAMIC IMPORT SWITCH:`);
console.log(` File: ${args.importer}`);
console.log(` Original: ${args.path}`);
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
console.log('');
console.log(
` Switched to: ${replacement} (build: ${buildValue})`
);
console.log("");
// Rewrite the import path and let the normal resolution continue
return build.resolve(replacement, {
@@ -215,12 +236,18 @@ function dynamicImportSwitcherPlugin(buildValue) {
build.onEnd((result) => {
if (switches.length > 0) {
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
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(
` ${i + 1}. ${path.relative(process.cwd(), s.file)}`
);
console.log(
` ${s.originalPath}${s.replacementPath}`
);
});
console.log('');
console.log("");
}
});
}
@@ -235,7 +262,7 @@ esbuild
format: "esm",
minify: false,
banner: {
js: banner,
js: banner
},
platform: "node",
external: ["body-parser"],
@@ -244,20 +271,22 @@ esbuild
dynamicImportGuardPlugin(),
dynamicImportSwitcherPlugin(argv.build),
nodeExternalsPlugin({
packagePath: getPackagePaths(),
}),
packagePath: getPackagePaths()
})
],
sourcemap: "inline",
target: "node22",
target: "node22"
})
.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):`);
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 => {
error.notes.forEach((note) => {
console.error(` - ${note.text}`);
});
}

View File

@@ -1,19 +1,19 @@
import tseslint from 'typescript-eslint';
import tseslint from "typescript-eslint";
export default tseslint.config({
files: ["**/*.{ts,tsx,js,jsx}"],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true
}
files: ["**/*.{ts,tsx,js,jsx}"],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true
}
}
},
rules: {
semi: "error",
"prefer-const": "warn"
}
},
rules: {
"semi": "error",
"prefer-const": "warn"
}
});
});

View File

@@ -9,10 +9,15 @@ services:
PARSERS: crowdsecurity/whitelists
ENROLL_TAGS: docker
healthcheck:
interval: 10s
retries: 15
timeout: 10s
test: ["CMD", "cscli", "capi", "status"]
test:
- CMD
- cscli
- lapi
- status
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
labels:
- "traefik.enable=false" # Disable traefik for crowdsec
volumes:

View File

@@ -1,5 +1,9 @@
http:
middlewares:
badger:
plugin:
badger:
disableForwardAuth: true
redirect-to-https:
redirectScheme:
scheme: https
@@ -44,7 +48,7 @@ http:
crowdsecAppsecUnreachableBlock: true # Block on unreachable
crowdsecAppsecBodyLimit: 10485760
crowdsecLapiKey: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later
crowdsecLapiHost: crowdsec:8080 # CrowdSec
crowdsecLapiHost: crowdsec:8080 # CrowdSec
crowdsecLapiScheme: http # CrowdSec API scheme
forwardedHeadersTrustedIPs: # Forwarded headers trusted IPs
- "0.0.0.0/0" # All IP addresses are trusted for forwarded headers (CHANGE MADE HERE)
@@ -63,6 +67,7 @@ http:
- web
middlewares:
- redirect-to-https
- badger
# Next.js router (handles everything except API and WebSocket paths)
next-router:
@@ -72,6 +77,7 @@ http:
- websecure
middlewares:
- security-headers # Add security headers middleware
- badger
tls:
certResolver: letsencrypt
@@ -83,6 +89,7 @@ http:
- websecure
middlewares:
- security-headers # Add security headers middleware
- badger
tls:
certResolver: letsencrypt
@@ -94,6 +101,7 @@ http:
- websecure
middlewares:
- security-headers # Add security headers middleware
- badger
tls:
certResolver: letsencrypt
@@ -106,4 +114,13 @@ http:
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server
- url: "http://pangolin:3000" # API/WebSocket server
tcp:
serversTransports:
pp-transport-v1:
proxyProtocol:
version: 1
pp-transport-v2:
proxyProtocol:
version: 2

View File

@@ -1,7 +1,7 @@
name: pangolin
services:
pangolin:
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
container_name: pangolin
restart: unless-stopped
volumes:

View File

@@ -1,5 +1,9 @@
http:
middlewares:
badger:
plugin:
badger:
disableForwardAuth: true
redirect-to-https:
redirectScheme:
scheme: https
@@ -13,6 +17,7 @@ http:
- web
middlewares:
- redirect-to-https
- badger
# Next.js router (handles everything except API and WebSocket paths)
next-router:
@@ -20,6 +25,8 @@ http:
service: next-service
entryPoints:
- websecure
middlewares:
- badger
tls:
certResolver: letsencrypt
@@ -29,6 +36,8 @@ http:
service: api-service
entryPoints:
- websecure
middlewares:
- badger
tls:
certResolver: letsencrypt
@@ -38,6 +47,8 @@ http:
service: api-service
entryPoints:
- websecure
middlewares:
- badger
tls:
certResolver: letsencrypt
@@ -59,4 +70,4 @@ tcp:
version: 1
pp-transport-v2:
proxyProtocol:
version: 2
version: 2

View File

@@ -73,7 +73,7 @@ func installDocker() error {
case strings.Contains(osRelease, "ID=ubuntu"):
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
apt-get update &&
apt-get install -y apt-transport-https ca-certificates curl &&
apt-get install -y apt-transport-https ca-certificates curl gpg &&
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
apt-get update &&
@@ -82,7 +82,7 @@ func installDocker() error {
case strings.Contains(osRelease, "ID=debian"):
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
apt-get update &&
apt-get install -y apt-transport-https ca-certificates curl &&
apt-get install -y apt-transport-https ca-certificates curl gpg &&
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
apt-get update &&

View File

@@ -3,8 +3,8 @@ module installer
go 1.24.0
require (
golang.org/x/term v0.37.0
golang.org/x/term v0.38.0
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/sys v0.38.0 // indirect
require golang.org/x/sys v0.39.0 // indirect

View File

@@ -1,7 +1,7 @@
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -54,13 +54,31 @@ func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
if defaultValue {
defaultStr = "yes"
}
input := readString(reader, prompt+" (yes/no)", defaultStr)
return strings.ToLower(input) == "yes"
for {
input := readString(reader, prompt+" (yes/no)", defaultStr)
lower := strings.ToLower(input)
if lower == "yes" {
return true
} else if lower == "no" {
return false
} else {
fmt.Println("Please enter 'yes' or 'no'.")
}
}
}
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
input := readStringNoDefault(reader, prompt+" (yes/no)")
return strings.ToLower(input) == "yes"
for {
input := readStringNoDefault(reader, prompt+" (yes/no)")
lower := strings.ToLower(input)
if lower == "yes" {
return true
} else if lower == "no" {
return false
} else {
fmt.Println("Please enter 'yes' or 'no'.")
}
}
}
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {

View File

@@ -49,13 +49,14 @@ type Config struct {
DoCrowdsecInstall bool
EnableGeoblocking bool
Secret string
IsEnterprise bool
}
type SupportedContainer string
const (
Docker SupportedContainer = "docker"
Podman SupportedContainer = "podman"
Docker SupportedContainer = "docker"
Podman SupportedContainer = "podman"
Undefined SupportedContainer = "undefined"
)
@@ -160,7 +161,7 @@ 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 {
@@ -179,7 +180,7 @@ func main() {
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")
fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\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\"")
@@ -209,8 +210,8 @@ func main() {
parsedURL, err := url.Parse(appConfig.DashboardURL)
if err != nil {
fmt.Printf("Error parsing URL: %v\n", err)
return
fmt.Printf("Error parsing URL: %v\n", err)
return
}
config.DashboardDomain = parsedURL.Hostname()
@@ -242,7 +243,7 @@ func main() {
}
}
if !alreadyInstalled {
if !alreadyInstalled || config.DoCrowdsecInstall {
// Setup Token Section
fmt.Println("\n=== Setup Token ===")
@@ -300,7 +301,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
// Linux only.
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
fmt.Printf("Error configuring unprivileged ports: %v\n", err)
os.Exit(1)
}
} else {
@@ -339,6 +340,8 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration
fmt.Println("\n=== Basic Configuration ===")
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for persoal use or for businesses making less than 100k USD annually.")
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
// Set default dashboard domain after base domain is collected
@@ -359,7 +362,7 @@ func collectUserInput(reader *bufio.Reader) Config {
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", "")
config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "")
}
// Validate required fields
@@ -371,6 +374,10 @@ func collectUserInput(reader *bufio.Reader) Config {
fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1)
}
if config.EnableEmail && config.EmailNoReply == "" {
fmt.Println("Error: No-reply email address is required when email is enabled")
os.Exit(1)
}
// Advanced configuration
@@ -643,28 +650,28 @@ func checkPortsAvailable(port int) error {
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",
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)
}
// Extract the database
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
}
// 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
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
{
"setupCreate": "Create the organization, site, and resources",
"headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.",
"headerAuthCompatibility": "Extended compatibility",
"setupNewOrg": "New Organization",
"setupCreateOrg": "Create Organization",
"setupCreateResources": "Create Resources",
@@ -33,7 +35,7 @@
"password": "Password",
"confirmPassword": "Confirm Password",
"createAccount": "Create Account",
"viewSettings": "View settings",
"viewSettings": "View Settings",
"delete": "Delete",
"name": "Name",
"online": "Online",
@@ -51,6 +53,9 @@
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
"siteManageSites": "Manage Sites",
"siteDescription": "Create and manage sites to enable connectivity to private networks",
"sitesBannerTitle": "Connect Any Network",
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
"sitesBannerButtonText": "Install Site",
"siteCreate": "Create Site",
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
"siteCreateDescription": "Create a new site to start connecting resources",
@@ -100,6 +105,7 @@
"siteTunnelDescription": "Determine how you want to connect to the site",
"siteNewtCredentials": "Credentials",
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
"remoteNodeCredentialsDescription": "This is how the remote node will authenticate with the server",
"siteCredentialsSave": "Save the Credentials",
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
"siteInfo": "Site Information",
@@ -144,10 +150,14 @@
"expires": "Expires",
"never": "Never",
"shareErrorSelectResource": "Please select a resource",
"proxyResourceTitle": "Manage Proxy Resources",
"proxyResourceTitle": "Manage Public Resources",
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
"clientResourceTitle": "Manage Client Resources",
"proxyResourcesBannerTitle": "Web-based Public Access",
"proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
"clientResourceTitle": "Manage Private Resources",
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
"privateResourcesBannerTitle": "Zero-Trust Private Access",
"privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.",
"resourcesSearch": "Search resources...",
"resourceAdd": "Add Resource",
"resourceErrorDelte": "Error deleting resource",
@@ -157,9 +167,9 @@
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
"resourceHTTP": "HTTPS Resource",
"resourceHTTPDescription": "Proxy requests to the app over HTTPS using a subdomain or base domain.",
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
"resourceRaw": "Raw TCP/UDP Resource",
"resourceRawDescription": "Proxy requests to the app over TCP/UDP using a port number. This only works when sites are connected to nodes.",
"resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.",
"resourceCreate": "Create Resource",
"resourceCreateDescription": "Follow the steps below to create a new resource",
"resourceSeeAll": "See All Resources",
@@ -181,7 +191,7 @@
"baseDomain": "Base Domain",
"subdomnainDescription": "The subdomain where the resource will be accessible.",
"resourceRawSettings": "TCP/UDP Settings",
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP. You map the resource to a port on the host Pangolin server, so you can access the resource from server-public-ip:mapped-port.",
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP",
"protocol": "Protocol",
"protocolSelect": "Select a protocol",
"resourcePortNumber": "Port Number",
@@ -419,7 +429,7 @@
"userErrorExistsDescription": "This user is already a member of the organization.",
"inviteError": "Failed to invite user",
"inviteErrorDescription": "An error occurred while inviting the user",
"userInvited": "User invited",
"userInvited": "User Invited",
"userInvitedDescription": "The user has been successfully invited.",
"userErrorCreate": "Failed to create user",
"userErrorCreateDescription": "An error occurred while creating the user",
@@ -499,7 +509,7 @@
"proxyUpdatedDescription": "Proxy settings have been updated successfully",
"proxyErrorUpdate": "Failed to update proxy settings",
"proxyErrorUpdateDescription": "An error occurred while updating proxy settings",
"targetAddr": "IP / Hostname",
"targetAddr": "Host",
"targetPort": "Port",
"targetProtocol": "Protocol",
"targetTlsSettings": "Secure Connection Configuration",
@@ -687,7 +697,7 @@
"resourceRoleDescription": "Admins can always access this resource.",
"resourceUsersRoles": "Access Controls",
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
"resourceUsersRolesSubmit": "Save Users & Roles",
"resourceUsersRolesSubmit": "Save Access Controls",
"resourceWhitelistSave": "Saved successfully",
"resourceWhitelistSaveDescription": "Whitelist settings have been saved",
"ssoUse": "Use Platform SSO",
@@ -924,6 +934,10 @@
"passwordResetSent": "We'll send a password reset code to this email address.",
"passwordResetCode": "Reset Code",
"passwordResetCodeDescription": "Check your email for the reset code.",
"generatePasswordResetCode": "Generate Password Reset Code",
"passwordResetCodeGenerated": "Password Reset Code Generated",
"passwordResetCodeGeneratedDescription": "Share this code with the user. They can use it to reset their password.",
"passwordResetUrl": "Reset URL",
"passwordNew": "New Password",
"passwordNewConfirm": "Confirm New Password",
"changePassword": "Change Password",
@@ -941,6 +955,9 @@
"pincodeAuth": "Authenticator Code",
"pincodeSubmit2": "Submit Code",
"passwordResetSubmit": "Request Reset",
"passwordResetAlreadyHaveCode": "Enter Code",
"passwordResetSmtpRequired": "Please contact your administrator",
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
"passwordBack": "Back to Password",
"loginBack": "Go back to log in",
"signup": "Sign up",
@@ -1028,6 +1045,7 @@
"updateOrgUser": "Update Org User",
"createOrgUser": "Create Org User",
"actionUpdateOrg": "Update Organization",
"actionRemoveInvitation": "Remove Invitation",
"actionUpdateUser": "Update User",
"actionGetUser": "Get User",
"actionGetOrgUser": "Get Organization User",
@@ -1037,6 +1055,8 @@
"actionGetSite": "Get Site",
"actionListSites": "List Sites",
"actionApplyBlueprint": "Apply Blueprint",
"actionListBlueprints": "List Blueprints",
"actionGetBlueprint": "Get Blueprint",
"setupToken": "Setup Token",
"setupTokenDescription": "Enter the setup token from the server console.",
"setupTokenRequired": "Setup token is required",
@@ -1169,8 +1189,8 @@
"sidebarHome": "Home",
"sidebarSites": "Sites",
"sidebarResources": "Resources",
"sidebarProxyResources": "Proxy Resources",
"sidebarClientResources": "Client Resources",
"sidebarProxyResources": "Public",
"sidebarClientResources": "Private",
"sidebarAccessControl": "Access Control",
"sidebarLogsAndAnalytics": "Logs & Analytics",
"sidebarUsers": "Users",
@@ -1184,14 +1204,14 @@
"sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License",
"sidebarClients": "Clients",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Machine Clients",
"sidebarUserDevices": "Users",
"sidebarMachineClients": "Machines",
"sidebarDomains": "Domains",
"sidebarGeneral": "General",
"sidebarGeneral": "Manage",
"sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blueprints",
"sidebarOrganization": "Organization",
"sidebarLogsAnalytics": "Request Analytics",
"sidebarLogsAnalytics": "Analytics",
"blueprints": "Blueprints",
"blueprintsDescription": "Apply declarative configurations and view previous runs",
"blueprintAdd": "Add Blueprint",
@@ -1301,8 +1321,11 @@
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
"documentation": "Documentation",
"saveAllSettings": "Save All Settings",
"saveResourceTargets": "Save Targets",
"saveResourceHttp": "Save Proxy Settings",
"saveProxyProtocol": "Save Proxy protocol settings",
"settingsUpdated": "Settings updated",
"settingsUpdatedDescription": "All settings have been updated successfully",
"settingsUpdatedDescription": "Settings updated successfully",
"settingsErrorUpdate": "Failed to update settings",
"settingsErrorUpdateDescription": "An error occurred while updating settings",
"sidebarCollapse": "Collapse",
@@ -1313,9 +1336,9 @@
"productUpdateTitle": "Product Updates",
"productUpdateEmpty": "No updates",
"dismissAll": "Dismiss all",
"pangolinUpdateAvailable": "New version available",
"pangolinUpdateAvailable": "Update Available",
"pangolinUpdateAvailableInfo": "Version {version} is ready to install",
"pangolinUpdateAvailableReleaseNotes": "View release notes",
"pangolinUpdateAvailableReleaseNotes": "View Release Notes",
"newtUpdateAvailable": "Update Available",
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
"domainPickerEnterDomain": "Domain",
@@ -1501,6 +1524,7 @@
"addNewTarget": "Add New Target",
"targetsList": "Targets List",
"advancedMode": "Advanced Mode",
"advancedSettings": "Advanced Settings",
"targetErrorDuplicateTargetFound": "Duplicate target found",
"healthCheckHealthy": "Healthy",
"healthCheckUnhealthy": "Unhealthy",
@@ -1570,7 +1594,7 @@
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "Unknown",
"resourcesTableNotMonitored": "Not monitored",
"editInternalResourceDialogEditClientResource": "Edit Client Resource",
"editInternalResourceDialogEditClientResource": "Edit Private Resource",
"editInternalResourceDialogUpdateResourceProperties": "Update the resource configuration and access controls for {resourceName}",
"editInternalResourceDialogResourceProperties": "Resource Properties",
"editInternalResourceDialogName": "Name",
@@ -1604,14 +1628,13 @@
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
"createInternalResourceDialogClose": "Close",
"createInternalResourceDialogCreateClientResource": "Create Client Resource",
"createInternalResourceDialogCreateClientResource": "Create Private Resource",
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
"createInternalResourceDialogResourceProperties": "Resource Properties",
"createInternalResourceDialogName": "Name",
"createInternalResourceDialogSite": "Site",
"createInternalResourceDialogSelectSite": "Select site...",
"createInternalResourceDialogSearchSites": "Search sites...",
"createInternalResourceDialogNoSitesFound": "No sites found.",
"selectSite": "Select site...",
"noSitesFound": "No sites found.",
"createInternalResourceDialogProtocol": "Protocol",
"createInternalResourceDialogTcp": "TCP",
"createInternalResourceDialogUdp": "UDP",
@@ -1651,7 +1674,7 @@
"siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.",
"siteNameDescription": "The display name of the site that can be changed later.",
"autoLoginExternalIdp": "Auto Login with External IDP",
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external identity provider for authentication.",
"selectIdp": "Select IDP",
"selectIdpPlaceholder": "Choose an IDP...",
"selectIdpRequired": "Please select an IDP when auto login is enabled.",
@@ -1663,7 +1686,7 @@
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud",
"remoteExitNodeDescription": "Self-host your own remote relay and proxy server nodes",
"remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Search nodes...",
"remoteExitNodeAdd": "Add Node",
@@ -1673,20 +1696,22 @@
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
"remoteExitNodeDelete": "Delete Node",
"sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Secret",
"remoteExitNodeCreate": {
"title": "Create Node",
"description": "Create a new node to extend network connectivity",
"title": "Create Remote Node",
"description": "Create a new self-hosted remote relay and proxy server node",
"viewAllButton": "View All Nodes",
"strategy": {
"title": "Creation Strategy",
"description": "Choose this to manually configure the node or generate new credentials.",
"description": "Select how you want to create the remote node",
"adopt": {
"title": "Adopt Node",
"description": "Choose this if you already have the credentials for the node."
},
"generate": {
"title": "Generate Keys",
"description": "Choose this if you want to generate new keys for the node"
"description": "Choose this if you want to generate new keys for the node."
}
},
"adopt": {
@@ -1799,9 +1824,30 @@
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Subnet",
"subnetDescription": "The subnet for this organization's network configuration.",
"authPage": "Auth Page",
"authPageDescription": "Configure the auth page for the organization",
"customDomain": "Custom Domain",
"authPage": "Authentication Pages",
"authPageDescription": "Set a custom domain for the organization's authentication pages",
"authPageDomain": "Auth Page Domain",
"authPageBranding": "Custom Branding",
"authPageBrandingDescription": "Configure the branding that appears on authentication pages for this organization",
"authPageBrandingUpdated": "Auth page Branding updated successfully",
"authPageBrandingRemoved": "Auth page Branding removed successfully",
"authPageBrandingRemoveTitle": "Remove Auth Page Branding",
"authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?",
"authPageBrandingDeleteConfirm": "Confirm Delete Branding",
"brandingLogoURL": "Logo URL",
"brandingPrimaryColor": "Primary Color",
"brandingLogoWidth": "Width (px)",
"brandingLogoHeight": "Height (px)",
"brandingOrgTitle": "Title for Organization Auth Page",
"brandingOrgDescription": "{orgName} will be replaced with the organization's name",
"brandingOrgSubtitle": "Subtitle for Organization Auth Page",
"brandingResourceTitle": "Title for Resource Auth Page",
"brandingResourceSubtitle": "Subtitle for Resource Auth Page",
"brandingResourceDescription": "{resourceName} will be replaced with the organization's name",
"saveAuthPageDomain": "Save Domain",
"saveAuthPageBranding": "Save Branding",
"removeAuthPageBranding": "Remove Branding",
"noDomainSet": "No domain set",
"changeDomain": "Change Domain",
"selectDomain": "Select Domain",
@@ -1810,7 +1856,7 @@
"setAuthPageDomain": "Set Auth Page Domain",
"failedToFetchCertificate": "Failed to fetch certificate",
"failedToRestartCertificate": "Failed to restart certificate",
"addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for the organization",
"addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.",
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
"domainPickerProvidedDomain": "Provided Domain",
"domainPickerFreeProvidedDomain": "Free Provided Domain",
@@ -1825,10 +1871,19 @@
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
"domainPickerSubdomainSanitized": "Subdomain sanitized",
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
"orgAuthSignInTitle": "Sign in to the organization",
"orgAuthSignInTitle": "Organization Sign In",
"orgAuthChooseIdpDescription": "Choose your identity provider to continue",
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Organization Sign In",
"orgAuthSelectOrgDescription": "Enter your organization ID to continue",
"orgAuthOrgIdPlaceholder": "your-organization",
"orgAuthOrgIdHelp": "Enter your organization's unique identifier",
"orgAuthSelectOrgHelp": "After entering your organization ID, you'll be taken to your organization's sign-in page where you can use SSO or your organization credentials.",
"orgAuthRememberOrgId": "Remember this organization ID",
"orgAuthBackToSignIn": "Back to standard sign in",
"orgAuthNoAccount": "Don't have an account?",
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
"idpDisabled": "Identity providers are disabled.",
"orgAuthPageDisabled": "Organization auth page is disabled.",
@@ -1843,6 +1898,8 @@
"enableTwoFactorAuthentication": "Enable two-factor authentication",
"completeSecuritySteps": "Complete Security Steps",
"securitySettings": "Security Settings",
"dangerSection": "Danger Zone",
"dangerSectionDescription": "Permanently delete all data associated with this organization",
"securitySettingsDescription": "Configure security policies for the organization",
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
@@ -1880,7 +1937,7 @@
"securityPolicyChangeWarningText": "This will affect all users in the organization",
"authPageErrorUpdateMessage": "An error occurred while updating the auth page settings",
"authPageErrorUpdate": "Unable to update auth page",
"authPageUpdated": "Auth page updated successfully",
"authPageDomainUpdated": "Auth page Domain updated successfully",
"healthCheckNotAvailable": "Local",
"rewritePath": "Rewrite Path",
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
@@ -1908,8 +1965,15 @@
"beta": "Beta",
"manageUserDevices": "User Devices",
"manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources",
"downloadClientBannerTitle": "Download Pangolin Client",
"downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately.",
"manageMachineClients": "Manage Machine Clients",
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
"machineClientsBannerTitle": "Servers & Automated Systems",
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
"machineClientsBannerPangolinCLI": "Pangolin CLI",
"machineClientsBannerOlmCLI": "Olm CLI",
"machineClientsBannerOlmContainer": "Olm Container",
"clientsTableUserClients": "User",
"clientsTableMachineClients": "Machine",
"licenseTableValidUntil": "Valid Until",
@@ -2046,20 +2110,22 @@
"pathRewriteStripLabel": "strip",
"sidebarEnableEnterpriseLicense": "Enable Enterprise License",
"cannotbeUndone": "This can not be undone.",
"toConfirm": "to confirm",
"toConfirm": "to confirm.",
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?",
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
"sidebarLogs": "Logs",
"request": "Request",
"requests": "Requests",
"logs": "Logs",
"logsSettingsDescription": "Monitor logs collected from this orginization",
"logsSettingsDescription": "Monitor logs collected from this organization",
"searchLogs": "Search logs...",
"action": "Action",
"actor": "Actor",
"timestamp": "Timestamp",
"accessLogs": "Access Logs",
"exportCsv": "Export CSV",
"exportError": "Unknown error when exporting CSV",
"exportCsvTooltip": "Within Time Range",
"actorId": "Actor ID",
"allowedByRule": "Allowed by Rule",
"allowedNoAuth": "Allowed No Auth",
@@ -2101,6 +2167,7 @@
"logRetention30Days": "30 days",
"logRetention90Days": "90 days",
"logRetentionForever": "Forever",
"logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization",
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
@@ -2112,7 +2179,7 @@
"unverified": "Unverified",
"domainSetting": "Domain Settings",
"domainSettingDescription": "Configure settings for the domain",
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).",
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (requires a properly configured certificate resolver).",
"recordName": "Record Name",
"auto": "Auto",
"TTL": "TTL",
@@ -2213,8 +2280,8 @@
"regenerate": "Regenerate",
"credentials": "Credentials",
"savecredentials": "Save Credentials",
"regeneratecredentials": "Re-key",
"regenerateCredentials": "Regenerate and save your credentials",
"regenerateCredentialsButton": "Regenerate Credentials",
"regenerateCredentials": "Regenerate Credentials",
"generatedcredentials": "Generated Credentials",
"copyandsavethesecredentials": "Copy and save these credentials",
"copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.",
@@ -2222,13 +2289,12 @@
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
"credentialsSaveError": "Credentials Save Error",
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.",
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.",
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones and cause a disconnection. Make sure to update any configurations that use these credentials.",
"confirm": "Confirm",
"regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Secret Key",
"featureDisabledTooltip": "This feature is only available in the enterprise plan and require a license to use it.",
"niceId": "Nice ID",
"niceIdUpdated": "Nice ID Updated",
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
@@ -2247,5 +2313,84 @@
"clientAddress": "Client Address (Advanced)",
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
"setupSubnetAdvanced": "Subnet (Advanced)",
"setupSubnetDescription": "The subnet for this organization's internal network."
"setupSubnetDescription": "The subnet for this organization's internal network.",
"setupUtilitySubnet": "Utility Subnet (Advanced)",
"setupUtilitySubnetDescription": "The subnet for this organization's alias addresses and DNS server.",
"siteRegenerateAndDisconnect": "Regenerate and Disconnect",
"siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?",
"siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.",
"siteRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this site?",
"siteRegenerateCredentialsWarning": "This will regenerate the credentials. The site will stay connected until you manually restart it and use the new credentials.",
"clientRegenerateAndDisconnect": "Regenerate and Disconnect",
"clientRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this client?",
"clientRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the client. The client will need to be restarted with the new credentials.",
"clientRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this client?",
"clientRegenerateCredentialsWarning": "This will regenerate the credentials. The client will stay connected until you manually restart it and use the new credentials.",
"remoteExitNodeRegenerateAndDisconnect": "Regenerate and Disconnect",
"remoteExitNodeRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this remote exit node?",
"remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.",
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
"remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.",
"agent": "Agent",
"personalUseOnly": "Personal Use Only",
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
"instanceIsUnlicensed": "This instance is unlicensed.",
"portRestrictions": "Port Restrictions",
"allPorts": "All",
"custom": "Custom",
"allPortsAllowed": "All Ports Allowed",
"allPortsBlocked": "All Ports Blocked",
"tcpPortsDescription": "Specify which TCP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 80,443,8000-9000).",
"udpPortsDescription": "Specify which UDP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 53,123,500-600).",
"organizationLoginPageTitle": "Organization Login Page",
"organizationLoginPageDescription": "Customize the login page for this organization",
"resourceLoginPageTitle": "Resource Login Page",
"resourceLoginPageDescription": "Customize the login page for individual resources",
"enterConfirmation": "Enter confirmation",
"blueprintViewDetails": "Details",
"defaultIdentityProvider": "Default Identity Provider",
"editInternalResourceDialogNetworkSettings": "Network Settings",
"editInternalResourceDialogAccessPolicy": "Access Policy",
"editInternalResourceDialogAddRoles": "Add Roles",
"editInternalResourceDialogAddUsers": "Add Users",
"editInternalResourceDialogAddClients": "Add Clients",
"editInternalResourceDialogDestinationLabel": "Destination",
"editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.",
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
"editInternalResourceDialogIcmp": "ICMP",
"editInternalResourceDialogAccessControl": "Access Control",
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
"orgAuthWhatsThis": "Where can I find my organization ID?",
"learnMore": "Learn more",
"backToHome": "Go back to home",
"needToSignInToOrg": "Need to use your organization's identity provider?",
"maintenanceMode": "Maintenance Mode",
"maintenanceModeDescription": "Display a maintenance page to visitors",
"maintenanceModeType": "Maintenance Mode Type",
"showMaintenancePage": "Show a maintenance page to visitors",
"enableMaintenanceMode": "Enable Maintenance Mode",
"automatic": "Automatic",
"automaticModeDescription": " Show maintenance page only when all backend targets are down or unhealthy. Your resource continues working normally as long as at least one target is healthy.",
"forced": "Forced",
"forcedModeDescription": "Always show the maintenance page regardless of backend health. Use this for planned maintenance when you want to prevent all access.",
"warning:" : "Warning:",
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
"pageTitle": "Page Title",
"pageTitleDescription": "The main heading displayed on the maintenance page",
"maintenancePageMessage": "Maintenance Message",
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
"maintenancePageMessageDescription": "Detailed message explaining the maintenance",
"maintenancePageTimeTitle": "Estimated Completion Time (Optional)",
"maintenanceTime": "e.g., 2 hours, Nov 1 at 5:00 PM",
"maintenanceEstimatedTimeDescription": "When you expect maintenance to be completed",
"editDomain": "Edit Domain",
"editDomainDescription": "Select a domain for your resource",
"maintenanceModeDisabledTooltip": "This feature requires a valid license to enable.",
"maintenanceScreenTitle": "Service Temporarily Unavailable",
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
"createInternalResourceDialogDestinationRequired": "Destination is required"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,14 @@
{
"setupCreate": "조직, 사이트 및 리소스를 생성하십시오.",
"setupCreate": "조직, 사이트 및 리소스를 생성합니다.",
"headerAuthCompatibilityInfo": "인증 토큰이 없을 때 401 Unauthorized 응답을 강제하도록 설정합니다. 서버 챌린지 없이 자격 증명을 제공하지 않는 브라우저나 특정 HTTP 라이브러리에 필요합니다.",
"headerAuthCompatibility": "확장된 호환성",
"setupNewOrg": "새 조직",
"setupCreateOrg": "조직 생성",
"setupCreateResources": "리소스 생성",
"setupOrgName": "조직 이름",
"orgDisplayName": "이것은 귀하의 조직의 표시 이름입니다.",
"orgDisplayName": "이것은 조직의 표시 이름입니다.",
"orgId": "조직 ID",
"setupIdentifierMessage": "이것은 귀하의 조직에 대한 고유 식별자입니다. 표시 이름과는 별개입니다.",
"setupIdentifierMessage": "이것은 조직의 고유 식별자입니다.",
"setupErrorIdentifier": "조직 ID가 이미 사용 중입니다. 다른 것을 선택해 주세요.",
"componentsErrorNoMemberCreate": "현재 어떤 조직의 구성원도 아닙니다. 시작하려면 조직을 생성하세요.",
"componentsErrorNoMember": "현재 어떤 조직의 구성원도 아닙니다.",
@@ -50,10 +52,13 @@
"siteMessageRemove": "삭제되면 사이트에 더 이상 액세스할 수 없습니다. 사이트와 연결된 모든 대상도 삭제됩니다.",
"siteQuestionRemove": "조직에서 사이트를 제거하시겠습니까?",
"siteManageSites": "사이트 관리",
"siteDescription": "안전한 터널을 통해 네트워크 연결할 수 있도록 허용",
"siteDescription": "프라이빗 네트워크로의 연결을 활성화하려면 사이트를 생성하고 관리하세요.",
"sitesBannerTitle": "모든 네트워크 연결",
"sitesBannerDescription": "사이트는 원격 네트워크와의 연결로 Pangolin이 어디서나 사용자에게 공공 및 개인 리소스에 대한 접근을 제공할 수 있게 해 줍니다. 연결을 설정하려면 바이너리 또는 컨테이너로 실행할 수 있는 어디서든 사이트 네트워크 커넥터(Newt)를 설치하세요.",
"sitesBannerButtonText": "사이트 설치",
"siteCreate": "사이트 생성",
"siteCreateDescription2": "아래 단계를 따라 새 사이트를 생성하고 연결하십시오",
"siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하십시오.",
"siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하세요.",
"close": "닫기",
"siteErrorCreate": "사이트 생성 오류",
"siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다",
@@ -74,7 +79,7 @@
"siteInstallNewt": "Newt 설치",
"siteInstallNewtDescription": "시스템에서 Newt 실행하기",
"WgConfiguration": "WireGuard 구성",
"WgConfigurationDescription": "네트워크에 연결하기 위한 다음 구성을 사용하십시오.",
"WgConfigurationDescription": "네트워크에 연결하기 위한 다음 구성을 사용하세요.",
"operatingSystem": "운영 체제",
"commands": "명령",
"recommended": "추천",
@@ -87,25 +92,26 @@
"siteUpdated": "사이트가 업데이트되었습니다",
"siteUpdatedDescription": "사이트가 업데이트되었습니다.",
"siteGeneralDescription": "이 사이트에 대한 일반 설정을 구성하세요.",
"siteSettingDescription": "사이트에서 설정을 구성하세요",
"siteSettingDescription": "사이트에서 설정을 구성하세요.",
"siteSetting": "{siteName} 설정",
"siteNewtTunnel": "뉴트 터널 (추천)",
"siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.",
"siteNewtTunnel": "뉴트 사이트 (추천)",
"siteNewtTunnelDescription": "네트워크 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.",
"siteWg": "기본 WireGuard",
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
"siteLocalDescriptionSaas": "로컬 리소스 전용. 터널링 금지. 원격 노드에서만 사용 가능합니다.",
"siteSeeAll": "모든 사이트 보기",
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
"siteNewtCredentials": "Newt 자격 증명",
"siteNewtCredentialsDescription": "이것이 Newt가 서버와 인증하는 방법입니다",
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요.",
"siteNewtCredentials": "자격 증명",
"siteNewtCredentialsDescription": "이것이 사이트가 서버와 인증하는 방법입니다.",
"remoteNodeCredentialsDescription": "원격 노드가 서버와 인증하는 방법입니다.",
"siteCredentialsSave": "자격 증명 저장",
"siteCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.",
"siteInfo": "사이트 정보",
"status": "상태",
"shareTitle": "공유 링크 관리",
"shareDescription": "공유 가능한 링크를 생성하여 리소스에 대한 임시 또는 영구 액세스를 부여합니다.",
"shareDescription": "공유 가능한 링크를 생성하여 프록시 리소스에 임시 또는 영구적으로 액세스하세요.",
"shareSearch": "공유 링크 검색...",
"shareCreate": "공유 링크 생성",
"shareErrorDelete": "링크 삭제에 실패했습니다.",
@@ -144,8 +150,14 @@
"expires": "만료",
"never": "절대",
"shareErrorSelectResource": "리소스를 선택하세요",
"resourceTitle": "리소스 관리",
"resourceDescription": "개인 애플리케이션에 대한 보안 프록시 생성",
"proxyResourceTitle": "공개 리소스 관리",
"proxyResourceDescription": "웹 브라우저를 통해 공용으로 접근할 수 있는 리소스를 생성하고 관리하세요.",
"proxyResourcesBannerTitle": "웹 기반 공공 접근",
"proxyResourcesBannerDescription": "공공 자원은 누구나 웹 브라우저를 통해 접근 가능한 HTTPS 또는 TCP/UDP 프록시입니다. 개인 자원과 달리 클라이언트 측 소프트웨어가 필요하지 않으며, 아이덴티티 및 컨텍스트 인지 접근 정책을 포함할 수 있습니다.",
"clientResourceTitle": "개인 리소스 관리",
"clientResourceDescription": "연결된 클라이언트를 통해서만 접근할 수 있는 리소스를 생성하고 관리하세요.",
"privateResourcesBannerTitle": "제로 트러스트 개인 접근",
"privateResourcesBannerDescription": "개인 리소스는 제로 트러스트 보안을 사용하여, 사용자와 장치가 명시적으로 허용된 리소스에만 접근할 수 있도록 보장합니다. 이러한 리소스에 접근하려면 안전한 가상 사설 네트워크를 통해 사용자 장치 또는 머신 클라이언트를 연결하십시오.",
"resourcesSearch": "리소스 검색...",
"resourceAdd": "리소스 추가",
"resourceErrorDelte": "리소스 삭제 중 오류 발생",
@@ -155,9 +167,9 @@
"resourceMessageRemove": "제거되면 리소스에 더 이상 접근할 수 없습니다. 리소스와 연결된 모든 대상도 제거됩니다.",
"resourceQuestionRemove": "조직에서 리소스를 제거하시겠습니까?",
"resourceHTTP": "HTTPS 리소스",
"resourceHTTPDescription": "서브도메인 또는 기본 도메인을 사용하여 HTTPS를 통해 앱에 대한 요청을 프록시합니다.",
"resourceHTTPDescription": "완전한 도메인 이름을 사용해 RAW 또는 HTTPS로 프록시 요청을 수행합니다.",
"resourceRaw": "원시 TCP/UDP 리소스",
"resourceRawDescription": "TCP/UDP를 통해 포트 번호를 사용하여 앱에 요청을 프록시합니다.",
"resourceRawDescription": "포트 번호를 사용하여 RAW TCP/UDP로 요청을 프록시합니다.",
"resourceCreate": "리소스 생성",
"resourceCreateDescription": "아래 단계를 따라 새 리소스를 생성하세요.",
"resourceSeeAll": "모든 리소스 보기",
@@ -171,22 +183,22 @@
"noCountryFound": "국가를 찾을 수 없습니다.",
"siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.",
"resourceType": "리소스 유형",
"resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요",
"resourceTypeDescription": "리소스에 액세스하는 방법을 결정하세요.",
"resourceHTTPSSettings": "HTTPS 설정",
"resourceHTTPSSettingsDescription": "리소스에 대한 HTTPS 접근 방식을 구성하십시오.",
"resourceHTTPSSettingsDescription": "리소스 HTTPS 접근할 수 있는 방식을 구성합니다.",
"domainType": "도메인 유형",
"subdomain": "서브도메인",
"baseDomain": "기본 도메인",
"subdomnainDescription": "리소스에 접근할 수 있는 하위 도메인입니다.",
"resourceRawSettings": "TCP/UDP 설정",
"resourceRawSettingsDescription": "리소스를 TCP/UDP를 통해 액세스하는 방법을 구성합니다. 리소스를 호스트 Pangolin 서버의 포트에 매핑하여 서버-public-ip:매핑된 포트에서 리소스에 액세스할 수 있습니다.",
"resourceRawSettingsDescription": "TCP/UDP를 통해 리소스에 접근하는 방법을 구성하세요.",
"protocol": "프로토콜",
"protocolSelect": "프로토콜 선택",
"resourcePortNumber": "포트 번호",
"resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.",
"cancel": "취소",
"resourceConfig": "구성 스니펫",
"resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣으십시오.",
"resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣습니다.",
"resourceAddEntrypoints": "Traefik: 엔트리포인트 추가",
"resourceExposePorts": "Gerbil: Docker Compose에서 포트 노출",
"resourceLearnRaw": "TCP/UDP 리소스 구성 방법 알아보기",
@@ -204,10 +216,10 @@
"rules": "규칙",
"resourceSettingDescription": "리소스의 설정을 구성하세요.",
"resourceSetting": "{resourceName} 설정",
"alwaysAllow": "항상 허용",
"alwaysDeny": "항상 거부",
"alwaysAllow": "인증 우회",
"alwaysDeny": "접근 차단",
"passToAuth": "인증으로 전달",
"orgSettingsDescription": "조직의 일반 설정을 구성하세요",
"orgSettingsDescription": "조직의 일반 설정을 구성하세요.",
"orgGeneralSettings": "조직 설정",
"orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.",
"saveGeneralSettings": "일반 설정 저장",
@@ -232,7 +244,7 @@
"orgMissing": "조직 ID가 누락되었습니다",
"orgMissingMessage": "조직 ID 없이 초대장을 재생성할 수 없습니다.",
"accessUsersManage": "사용자 관리",
"accessUsersDescription": "사용자 초대하고 역할에 추가하여 조직에 대한 접근을 관리하세요",
"accessUsersDescription": "이 조직에 액세스할 사용자 초대 및 관리",
"accessUsersSearch": "사용자 검색...",
"accessUserCreate": "사용자 생성",
"accessUserRemove": "사용자 제거",
@@ -241,13 +253,13 @@
"role": "역할",
"nameRequired": "이름은 필수입니다",
"accessRolesManage": "역할 관리",
"accessRolesDescription": "조직에 대한 액세스를 관리할 역할 구성",
"accessRolesDescription": "조직의 사용자에 대한 역할을 생성하고 관리합니다.",
"accessRolesSearch": "역할 검색...",
"accessRolesAdd": "역할 추가",
"accessRoleDelete": "역할 삭제",
"description": "설명",
"inviteTitle": "열린 초대",
"inviteDescription": "다른 사용자에 대한 초대 관리하세요",
"inviteDescription": "다른 사용자가 조직에 참여하도록 초대장을 관리합니다.",
"inviteSearch": "초대 검색...",
"minutes": "분",
"hours": "시간",
@@ -264,10 +276,10 @@
"apiKeysCreateDescription": "조직을 위한 새로운 API 키 생성",
"apiKeysGeneralSettings": "권한",
"apiKeysGeneralSettingsDescription": "이 API 키가 수행할 수 있는 작업 결정",
"apiKeysList": "귀하의 API 키",
"apiKeysList": "새로운 API 키",
"apiKeysSave": "API 키 저장",
"apiKeysSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.",
"apiKeysInfo": "귀하의 API 키는 다음과 같습니다:",
"apiKeysInfo": "API 키는 다음과 같습니다:",
"apiKeysConfirmCopy": "API 키를 복사했습니다",
"generate": "생성",
"done": "완료",
@@ -424,7 +436,7 @@
"userCreated": "사용자가 생성되었습니다.",
"userCreatedDescription": "사용자가 성공적으로 생성되었습니다.",
"userTypeInternal": "내부 사용자",
"userTypeInternalDescription": "사용자를 초대하여 귀하의 조직에 직접 참여하게 하세요.",
"userTypeInternalDescription": "사용자를 초대하여 조직에 직접 참여하게 하세요.",
"userTypeExternal": "외부 사용자",
"userTypeExternalDescription": "외부 신원 공급자를 사용하여 사용자를 생성하세요.",
"accessUserCreateDescription": "새 사용자를 만들기 위한 아래 단계를 따르세요.",
@@ -436,6 +448,16 @@
"inviteEmailSent": "사용자에게 초대 이메일 보내기",
"inviteValid": "유효 기간",
"selectDuration": "지속 시간 선택",
"selectResource": "리소스 선택",
"filterByResource": "리소스별 필터",
"resetFilters": "필터 재설정",
"totalBlocked": "Pangolin으로 차단된 요청",
"totalRequests": "총 요청 수",
"requestsByCountry": "국가별 요청 수",
"requestsByDay": "일자별 요청 수",
"blocked": "차단됨",
"allowed": "허용됨",
"topCountries": "상위 국가",
"accessRoleSelect": "역할 선택",
"inviteEmailSentDescription": "아래의 접근 링크와 함께 사용자에게 이메일이 전송되었습니다. 사용자는 초대를 수락하기 위해 링크에 접근해야 합니다.",
"inviteSentDescription": "사용자가 초대되었습니다. 초대를 수락하려면 아래 링크에 접속해야 합니다.",
@@ -464,7 +486,7 @@
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
"proxyEnableSSL": "SSL 활성화",
"proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위 SSL/TLS 암호화를 활성화하세요.",
"proxyEnableSSLDescription": "타겟과의 안전한 HTTPS 연결을 위 SSL/TLS 암호화를 활성화하세요.",
"target": "대상",
"configureTarget": "대상 구성",
"targetErrorFetch": "대상 가져오는 데 실패했습니다.",
@@ -480,14 +502,14 @@
"targetsErrorUpdate": "대상 업데이트 실패",
"targetsErrorUpdateDescription": "대상 업데이트 중 오류가 발생했습니다.",
"targetTlsUpdate": "TLS 설정이 업데이트되었습니다.",
"targetTlsUpdateDescription": "TLS 설정이 성공적으로 업데이트되었습니다.",
"targetTlsUpdateDescription": "TLS 설정이 성공적으로 업데이트되었습니다",
"targetErrorTlsUpdate": "TLS 설정 업데이트에 실패했습니다.",
"targetErrorTlsUpdateDescription": "TLS 설정을 업데이트하는 동안 오류가 발생했습니다",
"proxyUpdated": "프록시 설정이 업데이트되었습니다.",
"proxyUpdatedDescription": "프록시 설정이 성공적으로 업데이트되었습니다",
"proxyErrorUpdate": "프록시 설정 업데이트에 실패했습니다.",
"proxyErrorUpdateDescription": "프록시 설정을 업데이트하는 동안 오류가 발생했습니다",
"targetAddr": "IP / 호스트 이름",
"targetAddr": "호스트",
"targetPort": "포트",
"targetProtocol": "프로토콜",
"targetTlsSettings": "보안 연결 구성",
@@ -502,7 +524,7 @@
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
"methodSelect": "선택 방법",
"targetSubmit": "대상 추가",
"targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보려면 대상을 추가하세요.",
"targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보낼 대상을 구성하려면 대상을 추가하세요.",
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
"targetsSubmit": "대상 저장",
"addTarget": "대상 추가",
@@ -516,6 +538,8 @@
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
"targetErrorCreate": "대상 생성 실패",
"targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.",
"tlsServerName": "TLS 서버 이름",
"tlsServerNameDescription": "SNI를 위한 TLS 서버 이름",
"save": "저장",
"proxyAdditional": "추가 프록시 설정",
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
@@ -607,9 +631,9 @@
"unknownCommand": "알 수 없는 명령",
"newtErrorFetchReleases": "릴리스 정보를 가져오는 데 실패했습니다: {err}",
"newtErrorFetchLatest": "최신 릴리스를 가져오는 중 오류 발생: {err}",
"newtEndpoint": "Newt 엔드포인트",
"newtId": "뉴트 ID",
"newtSecretKey": "Newt 비밀 키",
"newtEndpoint": "엔드포인트",
"newtId": "ID",
"newtSecretKey": "비밀",
"architecture": "아키텍처",
"sites": "사이트",
"siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.",
@@ -671,9 +695,9 @@
"resourcePincodeSetupTitle": "핀코드 설정",
"resourcePincodeSetupTitleDescription": "이 리소스를 보호하기 위해 핀 코드를 설정하십시오.",
"resourceRoleDescription": "관리자는 항상 이 리소스에 접근할 수 있습니다.",
"resourceUsersRoles": "사용자 및 역할",
"resourceUsersRoles": "접근 제어",
"resourceUsersRolesDescription": "이 리소스를 방문할 수 있는 사용자 및 역할을 구성하십시오",
"resourceUsersRolesSubmit": "사용자 및 역할 저장",
"resourceUsersRolesSubmit": "접근 제어 저장",
"resourceWhitelistSave": "성공적으로 저장되었습니다.",
"resourceWhitelistSaveDescription": "허용 목록 설정이 저장되었습니다.",
"ssoUse": "플랫폼 SSO 사용",
@@ -702,6 +726,7 @@
"resourceTransferSubmit": "리소스 전송",
"siteDestination": "대상 사이트",
"searchSites": "사이트 검색",
"countries": "국가",
"accessRoleCreate": "역할 생성",
"accessRoleCreateDescription": "사용자를 그룹화하고 권한을 관리하기 위해 새 역할을 생성하세요.",
"accessRoleCreateSubmit": "역할 생성",
@@ -909,6 +934,10 @@
"passwordResetSent": "이 이메일 주소로 비밀번호 재설정 코드를 전송하겠습니다.",
"passwordResetCode": "코드 재설정",
"passwordResetCodeDescription": "재설정 코드를 확인하려면 이메일을 확인하세요.",
"generatePasswordResetCode": "비밀번호 초기화 코드 생성",
"passwordResetCodeGenerated": "비밀번호 초기화 코드 생성됨",
"passwordResetCodeGeneratedDescription": "이 코드를 사용자와 공유하세요. 사용자는 이를 사용하여 비밀번호를 재설정할 수 있습니다.",
"passwordResetUrl": "리디렉션 URL",
"passwordNew": "새 비밀번호",
"passwordNewConfirm": "새 비밀번호 확인",
"changePassword": "비밀번호 변경",
@@ -926,6 +955,9 @@
"pincodeAuth": "인증 코드",
"pincodeSubmit2": "코드 제출",
"passwordResetSubmit": "재설정 요청",
"passwordResetAlreadyHaveCode": "코드를 입력하십시오.",
"passwordResetSmtpRequired": "관리자에게 문의하십시오",
"passwordResetSmtpRequiredDescription": "비밀번호를 재설정하려면 비밀번호 초기화 코드가 필요합니다. 지원을 받으려면 관리자에게 문의하십시오.",
"passwordBack": "비밀번호로 돌아가기",
"loginBack": "로그인으로 돌아가기",
"signup": "가입하기",
@@ -1013,6 +1045,7 @@
"updateOrgUser": "조직 사용자 업데이트",
"createOrgUser": "조직 사용자 생성",
"actionUpdateOrg": "조직 업데이트",
"actionRemoveInvitation": "초대 제거",
"actionUpdateUser": "사용자 업데이트",
"actionGetUser": "사용자 조회",
"actionGetOrgUser": "조직 사용자 가져오기",
@@ -1022,6 +1055,8 @@
"actionGetSite": "사이트 가져오기",
"actionListSites": "사이트 목록",
"actionApplyBlueprint": "청사진 적용",
"actionListBlueprints": "청사진 목록",
"actionGetBlueprint": "청사진 가져오기",
"setupToken": "설정 토큰",
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
"setupTokenRequired": "설정 토큰이 필요합니다",
@@ -1091,12 +1126,15 @@
"actionListSiteResources": "사이트 리소스 목록",
"actionUpdateSiteResource": "사이트 리소스 업데이트",
"actionListInvitations": "초대 목록",
"actionExportLogs": "로그 내보내기",
"actionViewLogs": "로그 보기",
"noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.",
"searchProgress": "검색...",
"create": "생성",
"orgs": "조직",
"loginError": "로그인 중 오류가 발생했습니다",
"loginRequiredForDevice": "장치를 인증하려면 로그인이 필요합니다.",
"passwordForgot": "비밀번호를 잊으셨나요?",
"otpAuth": "이중 인증",
"otpAuthDescription": "인증 앱에서 코드를 입력하거나 단일 사용 백업 코드 중 하나를 입력하세요.",
@@ -1151,19 +1189,29 @@
"sidebarHome": "홈",
"sidebarSites": "사이트",
"sidebarResources": "리소스",
"sidebarProxyResources": "공유",
"sidebarClientResources": "비공개",
"sidebarAccessControl": "액세스 제어",
"sidebarLogsAndAnalytics": "로그 및 분석",
"sidebarUsers": "사용자",
"sidebarAdmin": "관리자",
"sidebarInvitations": "초대",
"sidebarRoles": "역할",
"sidebarShareableLinks": "공유 가능한 링크",
"sidebarShareableLinks": "링크",
"sidebarApiKeys": "API 키",
"sidebarSettings": "설정",
"sidebarAllUsers": "모든 사용자",
"sidebarIdentityProviders": "신원 공급자",
"sidebarLicense": "라이선스",
"sidebarClients": "클라이언트",
"sidebarUserDevices": "사용자",
"sidebarMachineClients": "기계",
"sidebarDomains": "도메인",
"sidebarGeneral": "관리",
"sidebarLogAndAnalytics": "로그 & 통계",
"sidebarBluePrints": "청사진",
"sidebarOrganization": "조직",
"sidebarLogsAnalytics": "분석",
"blueprints": "청사진",
"blueprintsDescription": "선언적 구성을 적용하고 이전 실행을 봅니다",
"blueprintAdd": "청사진 추가",
@@ -1174,7 +1222,7 @@
"blueprintDetailsDescription": "적용된 청사진의 결과와 발생한 오류를 확인합니다",
"blueprintInfo": "청사진 정보",
"message": "메시지",
"blueprintContentsDescription": "인프라를 설명하는 YAML 콘텐츠를 정의하십시오",
"blueprintContentsDescription": "인프라를 설명하는 YAML 내용을 정의하십시오",
"blueprintErrorCreateDescription": "청사진을 적용하는 중 오류가 발생했습니다",
"blueprintErrorCreate": "청사진 생성 오류",
"searchBlueprintProgress": "청사진 검색...",
@@ -1230,14 +1278,14 @@
"loading": "로딩 중",
"restart": "재시작",
"domains": "도메인",
"domainsDescription": "조직의 도메인을 관리합니다",
"domainsDescription": "조직에서 사용 가능한 도메인 생성 및 관리",
"domainsSearch": "도메인 검색...",
"domainAdd": "도메인 추가",
"domainAddDescription": "조직에 새로운 도메인을 등록하세요",
"domainCreate": "도메인 생성",
"domainCreatedDescription": "도메인이 성공적으로 생성되었습니다",
"domainDeletedDescription": "도메인이 성공적으로 삭제되었습니다",
"domainQuestionRemove": "계정에서 도메인을 제거하시겠습니까?",
"domainQuestionRemove": "도메인을 제거하시겠습니까?",
"domainMessageRemove": "제거되면 도메인이 더 이상 계정과 연관되지 않습니다.",
"domainConfirmDelete": "도메인 삭제 확인",
"domainDelete": "도메인 삭제",
@@ -1273,8 +1321,11 @@
"accountSetupSuccess": "계정 설정이 완료되었습니다! 판골린에 오신 것을 환영합니다!",
"documentation": "문서",
"saveAllSettings": "모든 설정 저장",
"saveResourceTargets": "대상 저장",
"saveResourceHttp": "프록시 설정 저장",
"saveProxyProtocol": "프록시 프로토콜 설정 저장",
"settingsUpdated": "설정이 업데이트되었습니다",
"settingsUpdatedDescription": "모든 설정이 성공적으로 업데이트되었습니다",
"settingsUpdatedDescription": "설정이 성공적으로 업데이트되었습니다.",
"settingsErrorUpdate": "설정 업데이트 실패",
"settingsErrorUpdateDescription": "설정을 업데이트하는 동안 오류가 발생했습니다",
"sidebarCollapse": "줄이기",
@@ -1285,7 +1336,7 @@
"productUpdateTitle": "제품 업데이트",
"productUpdateEmpty": "업데이트 없음",
"dismissAll": "모두 해제",
"pangolinUpdateAvailable": "새 버전 사용 가능",
"pangolinUpdateAvailable": "업데이트 가능",
"pangolinUpdateAvailableInfo": "버전 {version}을(를) 설치할 준비가 되었습니다",
"pangolinUpdateAvailableReleaseNotes": "릴리스 노트 보기",
"newtUpdateAvailable": "업데이트 가능",
@@ -1430,28 +1481,31 @@
"and": "및",
"privacyPolicy": "개인 정보 보호 정책"
},
"signUpMarketing": {
"keepMeInTheLoop": "이메일을 통해 소식, 업데이트 및 새로운 기능을 받아보세요."
},
"siteRequired": "사이트가 필요합니다.",
"olmTunnel": "Olm 터널",
"olmTunnelDescription": "클라이언트 연결에 Olm 사용",
"errorCreatingClient": "클라이언트 생성 오류",
"clientDefaultsNotFound": "클라이언트 기본값을 찾을 수 없습니다.",
"createClient": "클라이언트 생성",
"createClientDescription": "사이트에 연결하기 위한 새 클라이언트를 생성하십시오.",
"createClientDescription": "개인 리소스에 액세스할 새 클라이언트를 생성하십시오",
"seeAllClients": "모든 클라이언트 보기",
"clientInformation": "클라이언트 정보",
"clientNamePlaceholder": "클라이언트 이름",
"address": "주소",
"subnetPlaceholder": "서브넷",
"addressDescription": "클라이언트가 연결에 사용할 주소",
"addressDescription": "클라이언트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.",
"selectSites": "사이트 선택",
"sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.",
"clientInstallOlm": "Olm 설치",
"clientInstallOlmDescription": "시스템에서 Olm을 실행하기",
"clientOlmCredentials": "Olm 자격 증명",
"clientOlmCredentialsDescription": "Olm이 서버와 인증하는 방법입니다.",
"clientOlmCredentialsDescription": "이것이 Newt가 서버와 인증하는 방법입니다",
"olmEndpoint": "Olm 엔드포인트",
"olmId": "Olm ID",
"olmSecretKey": "Olm 비밀 키",
"olmId": "ID",
"olmSecretKey": "비밀",
"clientCredentialsSave": "자격 증명 저장",
"clientCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.",
"generalSettingsDescription": "이 클라이언트에 대한 일반 설정을 구성하세요.",
@@ -1463,15 +1517,14 @@
"sitesFetchError": "사이트 가져오는 중 오류가 발생했습니다.",
"olmErrorFetchReleases": "Olm 릴리즈 가져오는 중 오류가 발생했습니다.",
"olmErrorFetchLatest": "최신 Olm 릴리즈 가져오는 중 오류가 발생했습니다.",
"remoteSubnets": "원격 서브넷",
"enterCidrRange": "CIDR 범위 입력",
"remoteSubnetsDescription": "이 사이트에서 원격으로 액세스할 수 있는 CIDR 범위를 추가하세요. 10.0.0.0/24와 같은 형식을 사용하세요. 이는 VPN 클라이언트 연결에만 적용됩니다.",
"resourceEnableProxy": "공개 프록시 사용",
"resourceEnableProxyDescription": "이 리소스에 대한 공개 프록시를 활성화하십시오. 이를 통해 네트워크 외부로부터 클라우드를 통해 열린 포트에서 리소스에 액세스할 수 있습니다. Traefik 구성이 필요합니다.",
"externalProxyEnabled": "외부 프록시 활성화됨",
"addNewTarget": "새 대상 추가",
"targetsList": "대상 목록",
"advancedMode": "고급 모드",
"advancedSettings": "고급 설정",
"targetErrorDuplicateTargetFound": "중복 대상 발견",
"healthCheckHealthy": "정상",
"healthCheckUnhealthy": "비정상",
@@ -1483,14 +1536,15 @@
"enableHealthChecksDescription": "이 대상을 모니터링하여 건강 상태를 확인하세요. 필요에 따라 대상과 다른 엔드포인트를 모니터링할 수 있습니다.",
"healthScheme": "방법",
"healthSelectScheme": "방법 선택",
"healthCheckPortInvalid": "올바르지 않은 서브넷 마스크입니다. 1에서 65535 사이여야 합니다",
"healthCheckPath": "경로",
"healthHostname": "IP / 호스트",
"healthPort": "포트",
"healthCheckPathDescription": "상태 확인을 위한 경로입니다.",
"healthyIntervalSeconds": "정상 간격",
"unhealthyIntervalSeconds": "비정상 간격",
"healthyIntervalSeconds": "정상 간격(초)",
"unhealthyIntervalSeconds": "비정상 간격(초)",
"IntervalSeconds": "정상 간격",
"timeoutSeconds": "시간 초과",
"timeoutSeconds": "타임아웃(초)",
"timeIsInSeconds": "시간은 초 단위입니다",
"retryAttempts": "재시도 횟수",
"expectedResponseCodes": "예상 응답 코드",
@@ -1526,12 +1580,12 @@
"resourceEditDomain": "도메인 수정",
"siteName": "사이트 이름",
"proxyPort": "포트",
"resourcesTableProxyResources": "프록시 리소스",
"resourcesTableClientResources": "클라이언트 리소스",
"resourcesTableProxyResources": "공유",
"resourcesTableClientResources": "비공개",
"resourcesTableNoProxyResourcesFound": "프록시 리소스를 찾을 수 없습니다.",
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
"resourcesTableDestination": "대상지",
"resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.",
"resourcesTableAlias": "별칭",
"resourcesTableClients": "클라이언트",
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
"resourcesTableNoTargets": "대상 없음",
@@ -1540,8 +1594,8 @@
"resourcesTableOffline": "오프라인",
"resourcesTableUnknown": "알 수 없음",
"resourcesTableNotMonitored": "모니터링되지 않음",
"editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정",
"editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.",
"editInternalResourceDialogEditClientResource": "비공개 리소스 수정",
"editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요",
"editInternalResourceDialogResourceProperties": "리소스 속성",
"editInternalResourceDialogName": "이름",
"editInternalResourceDialogProtocol": "프로토콜",
@@ -1560,17 +1614,27 @@
"editInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식",
"editInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.",
"editInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.",
"editInternalResourceDialogPortModeRequired": "포트 모드에는 프로토콜, 프록시 포트 및 대상 포트가 필요합니다",
"editInternalResourceDialogMode": "모드",
"editInternalResourceDialogModePort": "포트",
"editInternalResourceDialogModeHost": "호스트",
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogDestination": "대상지",
"editInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.",
"editInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 또는 호스트 네임 주소입니다.",
"editInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.",
"editInternalResourceDialogAlias": "별칭",
"editInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
"createInternalResourceDialogNoSitesAvailable": "사용 가능한 사이트가 없습니다.",
"createInternalResourceDialogNoSitesAvailableDescription": "내부 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.",
"createInternalResourceDialogClose": "닫기",
"createInternalResourceDialogCreateClientResource": "클라이언트 리소스 생성",
"createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다.",
"createInternalResourceDialogCreateClientResource": "사이트 리소스 생성",
"createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다",
"createInternalResourceDialogResourceProperties": "리소스 속성",
"createInternalResourceDialogName": "이름",
"createInternalResourceDialogSite": "사이트",
"createInternalResourceDialogSelectSite": "사이트 선택...",
"createInternalResourceDialogSearchSites": "사이트 검색...",
"createInternalResourceDialogNoSitesFound": "사이트를 찾을 수 없습니다.",
"selectSite": "사이트 선택...",
"noSitesFound": "사이트를 찾을 수 없습니다.",
"createInternalResourceDialogProtocol": "프로토콜",
"createInternalResourceDialogTcp": "TCP",
"createInternalResourceDialogUdp": "UDP",
@@ -1593,13 +1657,24 @@
"createInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식",
"createInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.",
"createInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.",
"createInternalResourceDialogPortModeRequired": "포트 모드에는 프로토콜, 프록시 포트 및 대상 포트가 필요합니다",
"createInternalResourceDialogMode": "모드",
"createInternalResourceDialogModePort": "포트",
"createInternalResourceDialogModeHost": "호스트",
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogDestination": "대상지",
"createInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.",
"createInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.",
"createInternalResourceDialogAlias": "별칭",
"createInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
"siteConfiguration": "설정",
"siteAcceptClientConnections": "클라이언트 연결 허용",
"siteAcceptClientConnectionsDescription": "이 Newt 인스턴스를 게이트웨이로 사용하여 다른 장치가 연결될 수 있도록 허용합니다.",
"siteAddress": "사이트 주소",
"siteAddressDescription": "클라이언트가 연결하기 위한 호스트의 IP 주소를 지정합니다. 이는 클라이언트가 주소를 지정하기 위한 Pangolin 네트워크의 사이트 내부 주소입니다. 조직 서브넷 내에 있어야 합니다.",
"siteAcceptClientConnectionsDescription": "사용자 장치와 클라이언트가 이 사이트의 리소스에 접근할 수 있도록 허용하세요. 나중에 변경할 수 있습니다.",
"siteAddress": "사이트 주소(고급)",
"siteAddressDescription": "사이트 내부 주소. 조직 서브넷 내에 있어야 합니다.",
"siteNameDescription": "나중에 변경할 수 있는 사이트의 표시 이름입니다.",
"autoLoginExternalIdp": "외부 IDP로 자동 로그인",
"autoLoginExternalIdpDescription": "인증을 위해 외부 IDP로 사용자를 즉시 리디렉션합니다.",
"autoLoginExternalIdpDescription": "인증을 위해 사용자를 외부 IDP로 즉시 리디렉션합니다.",
"selectIdp": "IDP 선택",
"selectIdpPlaceholder": "IDP 선택...",
"selectIdpRequired": "자동 로그인이 활성화된 경우 IDP를 선택하십시오.",
@@ -1611,7 +1686,7 @@
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
"remoteExitNodeManageRemoteExitNodes": "원격 노드",
"remoteExitNodeDescription": "네트워크 연결성을 확장하고 클라우드 의존도를 줄이기 위해 하나 이상의 원격 노드를 자체 호스하십시오.",
"remoteExitNodeDescription": "자체 원격 중계 및 프록시 서버 노드를 호스하십시오.",
"remoteExitNodes": "노드",
"searchRemoteExitNodes": "노드 검색...",
"remoteExitNodeAdd": "노드 추가",
@@ -1621,20 +1696,22 @@
"remoteExitNodeConfirmDelete": "노드 삭제 확인",
"remoteExitNodeDelete": "노드 삭제",
"sidebarRemoteExitNodes": "원격 노드",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "비밀",
"remoteExitNodeCreate": {
"title": "노드 생성",
"description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요",
"title": "원격 노드 생성",
"description": "새로운 자체 호스팅 원격 중계 및 프록시 서버 노드를 생성하십시오.",
"viewAllButton": "모든 노드 보기",
"strategy": {
"title": "생성 전략",
"description": "노드를 직접 구성하거나 새 자격 증명을 생성하려면 이것을 선택하세요.",
"description": "원격 노드를 생성하는 방법을 선택합니다.",
"adopt": {
"title": "노드 채택",
"description": "이미 노드의 자격 증명이 있는 경우 이것을 선택하세요."
},
"generate": {
"title": "키 생성",
"description": "노드에 대한 새 키를 생성하려면 이것을 선택하세요"
"description": "노드에 대한 새 키를 생성하려면 이것을 선택하세요."
}
},
"adopt": {
@@ -1730,7 +1807,7 @@
"idpAzureConfiguration": "Azure Entra ID 구성",
"idpAzureConfigurationDescription": "Azure Entra ID OAuth2 자격 증명을 구성합니다.",
"idpTenantId": "테넌트 ID",
"idpTenantIdPlaceholder": "your-tenant-id",
"idpTenantIdPlaceholder": "테넌트 ID",
"idpAzureTenantIdDescription": "Azure 액티브 디렉터리 개요에서 찾은 Azure 테넌트 ID",
"idpAzureClientIdDescription": "Azure 앱 등록 클라이언트 ID",
"idpAzureClientSecretDescription": "Azure 앱 등록 클라이언트 비밀",
@@ -1747,9 +1824,30 @@
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 공급자",
"subnet": "서브넷",
"subnetDescription": "이 조직의 네트워크 구성에 대한 서브넷입니다.",
"customDomain": "사용자 정의 도메인",
"authPage": "인증 페이지",
"authPageDescription": "조직에 대한 인증 페이지를 구성합니다.",
"authPageDescription": "조직 인증 페이지에 대한 사용자 정의 도메인을 설정하세요.",
"authPageDomain": "인증 페이지 도메인",
"authPageBranding": "사용자 정의 브랜딩",
"authPageBrandingDescription": "이 조직의 인증 페이지에 표시될 브랜딩을 구성합니다.",
"authPageBrandingUpdated": "인증 페이지 브랜딩이 성공적으로 업데이트되었습니다.",
"authPageBrandingRemoved": "인증 페이지 브랜딩이 성공적으로 제거되었습니다.",
"authPageBrandingRemoveTitle": "인증 페이지 브랜딩 제거",
"authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?",
"authPageBrandingDeleteConfirm": "브랜딩 삭제 확인",
"brandingLogoURL": "로고 URL",
"brandingPrimaryColor": "기본 색상",
"brandingLogoWidth": "너비(px)",
"brandingLogoHeight": "높이(px)",
"brandingOrgTitle": "조직 인증 페이지의 제목",
"brandingOrgDescription": "{orgName}은 조직의 이름으로 대체됩니다.",
"brandingOrgSubtitle": "조직 인증 페이지의 부제목",
"brandingResourceTitle": "리소스 인증 페이지의 제목",
"brandingResourceSubtitle": "리소스 인증 페이지의 부제목",
"brandingResourceDescription": "{resourceName} 은 조직의 이름으로 대체됩니다.",
"saveAuthPageDomain": "도메인 저장",
"saveAuthPageBranding": "브랜딩 저장",
"removeAuthPageBranding": "브랜딩 제거",
"noDomainSet": "도메인 설정 없음",
"changeDomain": "도메인 변경",
"selectDomain": "도메인 선택",
@@ -1758,7 +1856,7 @@
"setAuthPageDomain": "인증 페이지 도메인 설정",
"failedToFetchCertificate": "인증서 가져오기 실패",
"failedToRestartCertificate": "인증서 재시작 실패",
"addDomainToEnableCustomAuthPages": "조직의 맞춤 인증 페이지를 활성화하려면 도메인을 추가하세요.",
"addDomainToEnableCustomAuthPages": "사용자는 이 도메인을 사용하여 조직의 로그인 페이지에 액세스하고 리소스 인증을 완료할 수 있습니다.",
"selectDomainForOrgAuthPage": "조직 인증 페이지에 대한 도메인을 선택하세요.",
"domainPickerProvidedDomain": "제공된 도메인",
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
@@ -1773,10 +1871,19 @@
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\"을(를) {domain}에 대해 유효하게 만들 수 없습니다.",
"domainPickerSubdomainSanitized": "하위 도메인 정리됨",
"domainPickerSubdomainCorrected": "\"{sub}\"이(가) \"{sanitized}\"로 수정되었습니다",
"orgAuthSignInTitle": "조직 로그인",
"orgAuthSignInTitle": "조직 로그인",
"orgAuthChooseIdpDescription": "계속하려면 신원 공급자를 선택하세요.",
"orgAuthNoIdpConfigured": "이 조직은 구성된 신원 공급자가 없습니다. 대신 Pangolin 아이덴티티로 로그인할 수 있습니다.",
"orgAuthSignInWithPangolin": "Pangolin으로 로그인",
"orgAuthSignInToOrg": "조직에 로그인합니다.",
"orgAuthSelectOrgTitle": "조직 로그인",
"orgAuthSelectOrgDescription": "계속하려면 조직 ID를 입력하십시오.",
"orgAuthOrgIdPlaceholder": "your-organization",
"orgAuthOrgIdHelp": "조직의 고유 식별자를 입력하십시오.",
"orgAuthSelectOrgHelp": "조직 ID를 입력하면, SSO 또는 조직 인증 정보를 사용할 수 있는 조직의 로그인 페이지로 이동합니다.",
"orgAuthRememberOrgId": "이 조직 ID 기억하기",
"orgAuthBackToSignIn": "표준 로그인을 통해 돌아가기",
"orgAuthNoAccount": "계정이 없으신가요?",
"subscriptionRequiredToUse": "이 기능을 사용하려면 구독이 필요합니다.",
"idpDisabled": "신원 공급자가 비활성화되었습니다.",
"orgAuthPageDisabled": "조직 인증 페이지가 비활성화되었습니다.",
@@ -1791,7 +1898,9 @@
"enableTwoFactorAuthentication": "이중 인증 활성화",
"completeSecuritySteps": "보안 단계 완료",
"securitySettings": "보안 설정",
"securitySettingsDescription": "조직에 대한 보안 정책을 구성합니다",
"dangerSection": "위험 구역",
"dangerSectionDescription": "이 조직에 관련된 모든 데이터를 영구적으로 삭제합니다.",
"securitySettingsDescription": "조직의 보안 정책을 구성하세요",
"requireTwoFactorForAllUsers": "모든 사용자에 대해 이중 인증 요구",
"requireTwoFactorDescription": "활성화되면, 이 조직의 모든 내부 사용자는 조직에 접근하기 위해 이중 인증을 활성화해야 합니다.",
"requireTwoFactorDisabledDescription": "이 기능을 사용하려면 유효한 라이선스(Enterprise) 또는 활성 구독(SaaS)가 필요합니다.",
@@ -1828,7 +1937,7 @@
"securityPolicyChangeWarningText": "이 작업은 조직의 모든 사용자에게 영향을 미칩니다",
"authPageErrorUpdateMessage": "인증 페이지 설정을 업데이트하는 동안 오류가 발생했습니다",
"authPageErrorUpdate": "인증 페이지를 업데이트할 수 없습니다",
"authPageUpdated": "인증 페이지 성공적으로 업데이트되었습니다",
"authPageDomainUpdated": "인증 페이지 도메인이 성공적으로 업데이트되었습니다.",
"healthCheckNotAvailable": "로컬",
"rewritePath": "경로 재작성",
"rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다.",
@@ -1854,8 +1963,19 @@
"enterpriseEdition": "엔터프라이즈 에디션",
"unlicensed": "라이선스 없음",
"beta": "베타",
"manageClients": "클라이언트 관리",
"manageClientsDescription": "클라이언트는 당신의 사이트에 연결할 수 있는 디바이스입니다.",
"manageUserDevices": "사용자 초대를 제어",
"manageUserDevicesDescription": "리소스에 개인적으로 연결하기 위해 사용자가 사용하는 장치를 보고 관리하세요",
"downloadClientBannerTitle": "Pangolin 클라이언트 다운로드",
"downloadClientBannerDescription": "Pangolin 네트워크에 연결하고 리소스를 비공개로 접근하기위해 시스템에 맞는 Pangolin 클라이언트를 다운로드하십시오.",
"manageMachineClients": "기계 클라이언트 관리",
"manageMachineClientsDescription": "서버와 시스템이 리소스에 개인적으로 연결하는 데 사용하는 클라이언트를 생성하고 관리하십시오",
"machineClientsBannerTitle": "서버 및 자동 시스템",
"machineClientsBannerDescription": "머신 클라이언트는 특정 사용자와 연결되지 않은 서버 및 자동화된 시스템을 위한 것입니다. 이들은 ID와 비밀을 통해 인증하며, Pangolin CLI, Olm CLI, 또는 Olm 컨테이너로 실행될 수 있습니다.",
"machineClientsBannerPangolinCLI": "Pangolin CLI",
"machineClientsBannerOlmCLI": "Olm CLI",
"machineClientsBannerOlmContainer": "Olm 컨테이너",
"clientsTableUserClients": "사용자",
"clientsTableMachineClients": "기계",
"licenseTableValidUntil": "유효 기한",
"saasLicenseKeysSettingsTitle": "엔터프라이즈 라이선스",
"saasLicenseKeysSettingsDescription": "자체 호스팅된 Pangolin 인스턴스를 위한 엔터프라이즈 라이선스 키를 생성하고 관리합니다.",
@@ -1990,19 +2110,22 @@
"pathRewriteStripLabel": "제거",
"sidebarEnableEnterpriseLicense": "엔터프라이즈 라이선스 활성화",
"cannotbeUndone": "이 작업은 되돌릴 수 없습니다.",
"toConfirm": "확인하려면",
"toConfirm": "확인을 위해.",
"deleteClientQuestion": "고객을 사이트와 조직에서 제거하시겠습니까?",
"clientMessageRemove": "제거되면 클라이언트는 사이트에 더 이상 연결할 수 없습니다.",
"sidebarLogs": "로그",
"request": "요청",
"requests": "요청",
"logs": "로그",
"logsSettingsDescription": "이 조직에서 수집된 로그를 모니터링합니다",
"logsSettingsDescription": "이 조직에서 수집된 로그를 모니터링합니다.",
"searchLogs": "로그 검색...",
"action": "작업",
"actor": "행위자",
"timestamp": "타임스탬프",
"accessLogs": "접근 로그",
"exportCsv": "CSV 내보내기",
"exportError": "CSV로 내보내는 중 알 수 없는 오류가 발생했습니다.",
"exportCsvTooltip": "시간 범위 내",
"actorId": "행위자 ID",
"allowedByRule": "룰에 의해 허용됨",
"allowedNoAuth": "인증 없음 허용됨",
@@ -2020,6 +2143,7 @@
"ip": "IP",
"reason": "이유",
"requestLogs": "요청 로그",
"requestAnalytics": "요청 분석",
"host": "호스트",
"location": "위치",
"actionLogs": "작업 로그",
@@ -2029,6 +2153,7 @@
"logRetention": "로그 보관",
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
"logRetentionRequestLabel": "요청 로그 보관",
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
"logRetentionAccessLabel": "접근 로그 보관",
@@ -2042,6 +2167,7 @@
"logRetention30Days": "30 일",
"logRetention90Days": "90 일",
"logRetentionForever": "영구",
"logRetentionEndOfFollowingYear": "다음 연도 말",
"actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다",
"accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다",
"licenseRequiredToUse": "이 기능을 사용하려면 Enterprise 라이선스가 필요합니다.",
@@ -2052,8 +2178,8 @@
"preferWildcardCert": "와일드카드 인증서 선호",
"unverified": "검증되지 않음",
"domainSetting": "도메인 설정",
"domainSettingDescription": "도메인에 대한 설정 구성하세요.",
"preferWildcardCertDescription": "와일드카드 인증서를 생성하려고 시도합니다 (올바르게 구성된 인증서 해결가 필요합니다).",
"domainSettingDescription": "도메인 설정 구성",
"preferWildcardCertDescription": "와일드카드 인증서를 생성하려고 시도합니다(적절한 인증서 해결 장치가 필요).",
"recordName": "레코드 이름",
"auto": "자동",
"TTL": "TTL",
@@ -2066,9 +2192,9 @@
"olmUpdateAvailableInfo": "올름의 새 버전이 이용 가능합니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
"client": "클라이언트",
"proxyProtocol": "프록시 프로토콜 설정",
"proxyProtocolDescription": "프록시 프로토콜을 구성하여 TCP/UDP 서비스에 대한 클라이언트 IP 주소를 보존하십시오.",
"proxyProtocolDescription": "TCP 서비스에 대한 클라이언트 IP 주소를 유지하도록 프록시 프로토콜을 구성하세요.",
"enableProxyProtocol": "프록시 프로토콜 활성화",
"proxyProtocolInfo": "TCP/UDP 백엔드의 클라이언트 IP 주소를 보존합니다",
"proxyProtocolInfo": "TCP 백엔드에 대한 클라이언트 IP 주소를 유지합니다.",
"proxyProtocolVersion": "프록시 프로토콜 버전",
"version1": " 버전 1 (추천)",
"version2": "버전 2",
@@ -2097,6 +2223,43 @@
"supportMessageSent": "메시지 전송 완료!",
"supportWillContact": "곧 연락드리겠습니다!",
"selectLogRetention": "로그 보존 선택",
"terms": "약관",
"privacy": "개인정보 보호",
"security": "보안",
"docs": "문서",
"deviceActivation": "장치 활성화",
"deviceCodeInvalidFormat": "코드는 9자리여야 합니다 (예: A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "무효하거나 만료된 코드",
"deviceCodeVerifyFailed": "이메일 확인에 실패했습니다:",
"signedInAs": "로그인한 사용자",
"deviceCodeEnterPrompt": "기기에 표시된 코드를 입력하세요",
"continue": "계속 진행하기",
"deviceUnknownLocation": "알 수 없는 위치",
"deviceAuthorizationRequested": "이 인증 요청은 {location}에서 {date}에 요청되었습니다. 이 장치에 액세스 권한을 부여할 신뢰할 수 있는 경우 확인하세요.",
"deviceLabel": "장치: {deviceName}",
"deviceWantsAccess": "계정에 액세스하려고 합니다",
"deviceExistingAccess": "기존 액세스:",
"deviceFullAccess": "계정에 대한 전체 액세스",
"deviceOrganizationsAccess": "계정이 접근할 수 있는 모든 조직에 대한 접근",
"deviceAuthorize": "{applicationName} 권한 부여",
"deviceConnected": "장치가 연결되었습니다!",
"deviceAuthorizedMessage": "장치가 계정에 액세스할 수 있도록 승인되었습니다.",
"pangolinCloud": "판골린 클라우드",
"viewDevices": "장치 보기",
"viewDevicesDescription": "연결된 장치를 관리하십시오",
"noDevices": "장치를 찾을 수 없습니다",
"dateCreated": "생성 날짜",
"unnamedDevice": "이름 없는 장치",
"deviceQuestionRemove": "이 장치를 삭제하시겠습니까?",
"deviceMessageRemove": "이 작업은 취소할 수 없습니다.",
"deviceDeleteConfirm": "장치 삭제",
"deleteDevice": "장치 삭제",
"errorLoadingDevices": "장치 로딩 오류",
"failedToLoadDevices": "장치를 로드하지 못했습니다",
"deviceDeleted": "장치 삭제 완료",
"deviceDeletedDescription": "장치가 성공적으로 삭제되었습니다.",
"errorDeletingDevice": "장치 삭제 오류",
"failedToDeleteDevice": "장치를 삭제하지 못했습니다",
"showColumns": "열 표시",
"hideColumns": "열 숨기기",
"columnVisibility": "열 가시성",
@@ -2111,10 +2274,14 @@
"enableSelected": "선택된 항목 활성화",
"disableSelected": "선택된 항목 비활성화",
"checkSelectedStatus": "선택된 항목 상태 확인",
"clients": "클라이언트",
"accessClientSelect": "기계 클라이언트 선택",
"resourceClientDescription": "관리자는 항상 이 리소스에 접근할 수 있습니다",
"regenerate": "재생성",
"credentials": "자격 증명",
"savecredentials": "자격 증명 저장",
"regeneratecredentials": "생성",
"regenerateCredentials": "자격 증명을 재생성하고 저장합니다",
"regenerateCredentialsButton": "자격 증명 다시 생성",
"regenerateCredentials": "자격 증명 다시 생성",
"generatedcredentials": "생성된 자격 증명",
"copyandsavethesecredentials": "이 자격 증명을 복사하여 저장합니다",
"copyandsavethesecredentialsdescription": "이 페이지를 떠난 후에는 자격 증명이 다시 표시되지 않습니다. 지금 안전하게 저장하십시오.",
@@ -2122,13 +2289,12 @@
"credentialsSavedDescription": "자격 증명이 성공적으로 재생성 및 저장되었습니다.",
"credentialsSaveError": "자격 증명 저장 오류",
"credentialsSaveErrorDescription": "자격 증명을 재생성하고 저장하는 동안 오류가 발생했습니다.",
"regenerateCredentialsWarning": "자격 증명을 생성하면 이전 자격 증명이 무효화됩니다. 이 자격 증명을 사용하는 모든 구성을 업데이트하십시오.",
"regenerateCredentialsWarning": "자격 증명을 다시 생성하면 이전 것들이 무효화되면서 연결이 끊어집니다. 이러한 자격 증명을 사용하는 모든 구성을 업데이트하세요.",
"confirm": "확인",
"regenerateCredentialsConfirmation": "자격 증명을 재생성하시겠습니까?",
"endpoint": "엔드포인트",
"Id": "아이디",
"SecretKey": "비밀 키",
"featureDisabledTooltip": "이 기능은 엔터프라이즈 플랜에서만 사용할 수 있으며 라이센스가 필요합니다.",
"niceId": "예쁜 ID",
"niceIdUpdated": "예쁜 ID 업데이트됨",
"niceIdUpdatedSuccessfully": "예쁜 ID가 성공적으로 업데이트되었습니다",
@@ -2136,5 +2302,95 @@
"niceIdUpdateErrorDescription": "예쁜 ID를 업데이트하는 동안 오류가 발생했습니다.",
"niceIdCannotBeEmpty": "예쁜 ID는 비워둘 수 없습니다",
"enterIdentifier": "식별자 입력",
"identifier": "식별자"
"identifier": "식별자",
"deviceLoginUseDifferentAccount": "본인이 아닙니까? 다른 계정을 사용하세요.",
"deviceLoginDeviceRequestingAccessToAccount": "장치가 이 계정에 접근하려고 합니다.",
"noData": "데이터 없음",
"machineClients": "기계 클라이언트",
"install": "설치",
"run": "실행",
"clientNameDescription": "나중에 변경할 수 있는 클라이언트의 표시 이름입니다.",
"clientAddress": "클라이언트 주소(고급)",
"setupFailedToFetchSubnet": "기본값 로드 실패",
"setupSubnetAdvanced": "서브넷(고급)",
"setupSubnetDescription": "이 조직의 네트워크 구성에 대한 서브넷입니다.",
"setupUtilitySubnet": "유틸리티 서브넷 (고급)",
"setupUtilitySubnetDescription": "이 조직의 별칭 주소 및 DNS 서버에 대한 서브넷입니다.",
"siteRegenerateAndDisconnect": "재생성 및 연결 해제",
"siteRegenerateAndDisconnectConfirmation": "자격 증명을 재생성하고 이 사이트와의 연결을 해제하시겠습니까?",
"siteRegenerateAndDisconnectWarning": "이 과정은 자격 증명을 다시 생성하고 사이트와의 연결을 즉시 해제합니다. 사이트는 새 자격 증명으로 다시 시작되어야 합니다.",
"siteRegenerateCredentialsConfirmation": "이 사이트에 대한 자격 증명을 다시 생성하시겠습니까?",
"siteRegenerateCredentialsWarning": "이 과정은 자격 증명을 다시 생성합니다. 수동으로 다시 시작하고 새 자격 증명을 사용하기 전까지 사이트는 연결된 상태로 유지됩니다.",
"clientRegenerateAndDisconnect": "재생성 및 연결 해제",
"clientRegenerateAndDisconnectConfirmation": "자격 증명을 재생성하고 이 클라이언트와의 연결을 해제하시겠습니까?",
"clientRegenerateAndDisconnectWarning": "이 과정은 자격 증명을 다시 생성하고 클라이언트와의 연결을 즉시 해제합니다. 클라이언트는 새 자격 증명으로 다시 시작되어야 합니다.",
"clientRegenerateCredentialsConfirmation": "이 클라이언트에 대한 자격 증명을 다시 생성하시겠습니까?",
"clientRegenerateCredentialsWarning": "이 과정은 자격 증명을 다시 생성합니다. 수동으로 다시 시작하고 새 자격 증명을 사용하기 전까지 클라이언트는 연결된 상태로 유지됩니다.",
"remoteExitNodeRegenerateAndDisconnect": "재생성 및 연결 해제",
"remoteExitNodeRegenerateAndDisconnectConfirmation": "자격 증명을 재생성하고 이 원격 종료 노드와의 연결을 해제하시겠습니까?",
"remoteExitNodeRegenerateAndDisconnectWarning": "이 과정은 자격 증명을 다시 생성하고 원격 종료 노드와의 연결을 즉시 해제합니다. 원격 종료 노드는 새 자격 증명으로 다시 시작되어야 합니다.",
"remoteExitNodeRegenerateCredentialsConfirmation": "이 원격 종료 노드에 대한 자격 증명을 다시 생성하시겠습니까?",
"remoteExitNodeRegenerateCredentialsWarning": "이 과정은 자격 증명을 다시 생성합니다. 수동으로 다시 시작하고 새 자격 증명을 사용하기 전까지 원격 종료 노드는 연결된 상태로 유지됩니다.",
"agent": "에이전트",
"personalUseOnly": "개인 용도로만 사용",
"loginPageLicenseWatermark": "이 인스턴스는 개인 용도로만 라이선스가 부여되었습니다.",
"instanceIsUnlicensed": "이 인스턴스에는 라이선스가 없습니다.",
"portRestrictions": "포트 제한",
"allPorts": "모두",
"custom": "사용자 정의",
"allPortsAllowed": "모든 포트 허용",
"allPortsBlocked": "모든 포트 차단",
"tcpPortsDescription": "이 리소스에 허용된 TCP 포트를 지정하세요. 모든 포트에 '*'를 사용하고, 모든 포트를 차단하려면 비워두거나 쉼표로 구분된 포트 및 범위 목록(예: 80,443,8000-9000)을 입력하십시오.",
"udpPortsDescription": "이 리소스에 허용된 UDP 포트를 지정하세요. 모든 포트에 '*'를 사용하고, 모든 포트를 차단하려면 비워두거나 쉼표로 구분된 포트 및 범위 목록(예: 53,123,500-600)을 입력하십시오.",
"organizationLoginPageTitle": "조직 로그인 페이지",
"organizationLoginPageDescription": "이 조직의 로그인 페이지를 사용자 정의합니다.",
"resourceLoginPageTitle": "리소스 로그인 페이지",
"resourceLoginPageDescription": "각 리소스의 로그인 페이지를 사용자 정의합니다.",
"enterConfirmation": "확인 입력",
"blueprintViewDetails": "세부 정보",
"defaultIdentityProvider": "기본 아이덴티티 공급자",
"editInternalResourceDialogNetworkSettings": "네트워크 설정",
"editInternalResourceDialogAccessPolicy": "액세스 정책",
"editInternalResourceDialogAddRoles": "역할 추가",
"editInternalResourceDialogAddUsers": "사용자 추가",
"editInternalResourceDialogAddClients": "클라이언트 추가",
"editInternalResourceDialogDestinationLabel": "대상지",
"editInternalResourceDialogDestinationDescription": "내부 리소스의 목적지 주소를 지정하세요. 선택한 모드에 따라 이 주소는 호스트명, IP 주소, 또는 CIDR 범위가 될 수 있습니다. 더욱 쉽게 식별할 수 있도록 내부 DNS 별칭을 설정할 수 있습니다.",
"editInternalResourceDialogPortRestrictionsDescription": "특정 TCP/UDP 포트에 대한 접근을 제한하거나 모든 포트를 허용/차단하십시오.",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
"editInternalResourceDialogIcmp": "ICMP",
"editInternalResourceDialogAccessControl": "액세스 제어",
"editInternalResourceDialogAccessControlDescription": "연결 시 이 리소스에 대한 액세스 권한을 가지는 역할, 사용자, 그리고 머신 클라이언트를 제어합니다. 관리자는 항상 접근할 수 있습니다.",
"editInternalResourceDialogPortRangeValidationError": "모든 포트에 대해서는 \"*\"로, 아니면 쉼표로 구분된 포트 및 범위 목록(예: \"80,443,8000-9000\")을 설정해야 합니다. 포트는 1에서 65535 사이여야 합니다.",
"orgAuthWhatsThis": "조직 ID를 어디에서 찾을 수 있습니까?",
"learnMore": "자세히 알아보기",
"backToHome": "홈으로 돌아가기",
"needToSignInToOrg": "조직의 아이덴티티 공급자를 사용해야 합니까?",
"maintenanceMode": "유지보수 모드",
"maintenanceModeDescription": "방문자에게 유지보수 페이지 표시",
"maintenanceModeType": "유지보수 모드 유형",
"showMaintenancePage": "방문자에게 유지보수 페이지 표시",
"enableMaintenanceMode": "유지보수 모드 활성화",
"automatic": "자동",
"automaticModeDescription": "백엔드 타깃이 모두 다운되거나 건강하지 않을 때만 유지보수 페이지를 표시합니다. 적어도 하나의 타깃이 건강한 한 리소스는 정상 작동합니다.",
"forced": "강제",
"forcedModeDescription": "백엔드 상태와 무관하게 항상 유지보수 페이지를 표시하십시오. 모든 접근을 차단하려는 계획된 유지보수 시 사용하세요.",
"warning:": "경고:",
"forcedeModeWarning": "모든 트래픽이 유지보수 페이지로 전달됩니다. 백엔드 리소스는 어떠한 요청도 받지 않습니다.",
"pageTitle": "페이지 제목",
"pageTitleDescription": "유지보수 페이지에 표시될 주요 제목",
"maintenancePageMessage": "유지보수 메시지",
"maintenancePageMessagePlaceholder": "곧 돌아오겠습니다! 사이트는 현재 예정된 유지보수를 진행 중입니다.",
"maintenancePageMessageDescription": "유지보수를 설명하는 상세 메시지",
"maintenancePageTimeTitle": "예상 완료 시간(선택 사항)",
"maintenanceTime": "예: 2시간, 11월 1일 오후 5시",
"maintenanceEstimatedTimeDescription": "유지보수가 완료될 것으로 예상되는 시간",
"editDomain": "도메인 수정",
"editDomainDescription": "리소스를 위한 도메인을 선택하십시오.",
"maintenanceModeDisabledTooltip": "이 기능을 사용하려면 유효한 라이선스가 필요합니다.",
"maintenanceScreenTitle": "서비스 일시 중단",
"maintenanceScreenMessage": "현재 기술적 문제를 겪고 있습니다. 곧 다시 확인하십시오.",
"maintenanceScreenEstimatedCompletion": "예상 완료:",
"createInternalResourceDialogDestinationRequired": "목적지가 필요합니다."
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,14 @@
{
"setupCreate": "Organizasyonunuzu, sitenizi ve kaynaklarınızı oluşturun",
"setupCreate": "Organizasyonu, siteyi ve kaynakları oluşturun",
"headerAuthCompatibilityInfo": "Kimlik doğrulama belirteci eksik olduğunda 401 Yetkisiz yanıtı zorlamak için bunu etkinleştirin. Bu, sunucu sorunu olmadan kimlik bilgilerini göndermeyen tarayıcılar veya belirli HTTP kütüphaneleri için gereklidir.",
"headerAuthCompatibility": "Genişletilmiş Uyumluluk",
"setupNewOrg": "Yeni Organizasyon",
"setupCreateOrg": "Organizasyon Oluştur",
"setupCreateResources": "Kaynaklar Oluştur",
"setupOrgName": "Organizasyon Adı",
"orgDisplayName": "Bu, organizasyonunuzun görünen adıdır.",
"orgDisplayName": "Bu organizasyonun görünen adıdır.",
"orgId": "Organizasyon ID",
"setupIdentifierMessage": "Bu, organizasyonunuzun benzersiz kimliğidir. Görünen adtan ayrı olarak.",
"setupIdentifierMessage": "Bu organizasyonun benzersiz tanımlayıcısıdır.",
"setupErrorIdentifier": "Organizasyon ID'si zaten alınmış. Lütfen başka bir tane seçin.",
"componentsErrorNoMemberCreate": "Şu anda herhangi bir organizasyona üye değilsiniz. Başlamak için bir organizasyon oluşturun.",
"componentsErrorNoMember": "Şu anda herhangi bir organizasyona üye değilsiniz.",
@@ -33,7 +35,7 @@
"password": "Şifre",
"confirmPassword": "Şifreyi Onayla",
"createAccount": "Hesap Oluştur",
"viewSettings": "Ayarları görüntüle",
"viewSettings": "Ayarları Görüntüle",
"delete": "Sil",
"name": "Ad",
"online": "Çevrimiçi",
@@ -50,7 +52,10 @@
"siteMessageRemove": "Kaldırıldıktan sonra site artık erişilebilir olmayacaktır. Siteyle ilişkilendirilmiş tüm hedefler de kaldırılacaktır.",
"siteQuestionRemove": "Siteyi organizasyondan kaldırmak istediğinizden emin misiniz?",
"siteManageSites": "Siteleri Yönet",
"siteDescription": "ınıza güvenli tüneller üzerinden bağlantı izni verin",
"siteDescription": "Özel ağlara erişimi etkinleştirmek için siteler oluşturun ve yönetin",
"sitesBannerTitle": "Herhangi Bir Ağa Bağlan",
"sitesBannerDescription": "Bir site, Pangolin'in kullanıcılara, halka açık veya özel kaynaklara, her yerden erişim sağlamak için uzak bir ağa bağlantı sunmasıdır. Site ağı bağlantısını (Newt) çalıştırabileceğiniz her yere kurarak bağlantıyı kurunuz.",
"sitesBannerButtonText": "Site Kur",
"siteCreate": "Site Oluştur",
"siteCreateDescription2": "Yeni bir site oluşturup bağlanmak için aşağıdaki adımları izleyin",
"siteCreateDescription": "Kaynaklarınızı bağlamaya başlamak için yeni bir site oluşturun",
@@ -89,7 +94,7 @@
"siteGeneralDescription": "Bu site için genel ayarları yapılandırın",
"siteSettingDescription": "Sitenizdeki ayarları yapılandırın",
"siteSetting": "{siteName} Ayarları",
"siteNewtTunnel": "Newt Tüneli (Önerilen)",
"siteNewtTunnel": "Newt Site (Önerilen)",
"siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.",
"siteWg": "Temel WireGuard",
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
@@ -98,8 +103,9 @@
"siteLocalDescriptionSaas": "Yerel kaynaklar yalnızca. Tünel oluşturma yok. Yalnızca uzak düğümlerde mevcuttur.",
"siteSeeAll": "Tüm Siteleri Gör",
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
"siteNewtCredentials": "Newt Kimlik Bilgileri",
"siteNewtCredentialsDescription": "Bu, Newt'in sunucu ile kimlik doğrulaması yapacağı yöntemdir",
"siteNewtCredentials": "Kimlik Bilgileri",
"siteNewtCredentialsDescription": "Bu, sitenin sunucu ile kimlik doğrulaması yapacağı yöntemdir",
"remoteNodeCredentialsDescription": "Uzak düğümün sunucu ile kimliği nasıl doğrulayacağı budur",
"siteCredentialsSave": "Kimlik Bilgilerinizi Kaydedin",
"siteCredentialsSaveDescription": "Yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.",
"siteInfo": "Site Bilgilendirmesi",
@@ -144,8 +150,14 @@
"expires": "Süresi Doluyor",
"never": "Asla",
"shareErrorSelectResource": "Lütfen bir kaynak seçin",
"resourceTitle": "Kaynakları Yönet",
"resourceDescription": "Özel uygulamalarınıza güvenli vekil sunucular oluşturun",
"proxyResourceTitle": "Herkese Açık Kaynakları Yönet",
"proxyResourceDescription": "Bir web tarayıcısı aracılığıyla kamuya açık kaynaklar oluşturun ve yönetin",
"proxyResourcesBannerTitle": "Web Tabanlı Genel Erişim",
"proxyResourcesBannerDescription": "Genel kaynaklar, web tarayıcısı aracılığıyla herkesin internette erişebileceği HTTPS veya TCP/UDP proxy'leridir. Özel kaynakların aksine, istemci tarafı yazılıma ihtiyaç duymazlar ve kimlik ve bağlam farkındalığı erişim politikalarını içerebilirler.",
"clientResourceTitle": "Özel Kaynakları Yönet",
"clientResourceDescription": "Sadece bağlı bir istemci aracılığıyla erişilebilen kaynakları oluşturun ve yönetin",
"privateResourcesBannerTitle": "Sıfır Güven Özel Erişim",
"privateResourcesBannerDescription": "Özel kaynaklar sıfır güven güvenliği kullanır, kullanıcılar ve makinelerin yalnızca açıkça izin verdiğiniz kaynaklara erişmesini sağlar. Bu kaynaklara güvenli bir sanal özel ağ üzerinden erişmek için kullanıcı cihazlarını veya makine müşterilerini bağlayın.",
"resourcesSearch": "Kaynakları ara...",
"resourceAdd": "Kaynak Ekle",
"resourceErrorDelte": "Kaynak silinirken hata",
@@ -155,9 +167,9 @@
"resourceMessageRemove": "Kaldırıldıktan sonra kaynak artık erişilebilir olmayacaktır. Kaynakla ilişkili tüm hedefler de kaldırılacaktır.",
"resourceQuestionRemove": "Kaynağı organizasyondan kaldırmak istediğinizden emin misiniz?",
"resourceHTTP": "HTTPS Kaynağı",
"resourceHTTPDescription": "Bir alt alan adı veya temel alan adı kullanarak uygulamanıza HTTPS üzerinden vekil istek gönderin.",
"resourceHTTPDescription": "Tam nitelikli bir etki alanı adı kullanarak HTTPS üzerinden proxy isteklerini yönlendirin.",
"resourceRaw": "Ham TCP/UDP Kaynağı",
"resourceRawDescription": "Uygulamanıza TCP/UDP üzerinden port numarası ile vekil istek gönderin.",
"resourceRawDescription": "Port numarası kullanarak ham TCP/UDP üzerinden proxy isteklerini yönlendirin.",
"resourceCreate": "Kaynak Oluştur",
"resourceCreateDescription": "Yeni bir kaynak oluşturmak için aşağıdaki adımları izleyin",
"resourceSeeAll": "Tüm Kaynakları Gör",
@@ -179,7 +191,7 @@
"baseDomain": "Temel Alan Adı",
"subdomnainDescription": "Kaynağınızın erişilebileceği alt alan adı.",
"resourceRawSettings": "TCP/UDP Ayarları",
"resourceRawSettingsDescription": "Kaynağınızın TCP/UDP üzerinden nasıl erişileceğini yapılandırın. Kaynağı, sunucudan erişebilmeniz için bir ana bilgisayar Pangolin sunucusundaki bir bağlantı noktasına eşlersiniz: sunucu genel-IP: eşlenen-bağlantı-noktası.",
"resourceRawSettingsDescription": "Kaynaklara TCP/UDP üzerinden nasıl erişileceğini yapılandırın",
"protocol": "Protokol",
"protocolSelect": "Bir protokol seçin",
"resourcePortNumber": "Port Numarası",
@@ -204,8 +216,8 @@
"rules": "Kurallar",
"resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın",
"resourceSetting": "{resourceName} Ayarları",
"alwaysAllow": "Her Zaman İzin Ver",
"alwaysDeny": "Her Zaman Reddet",
"alwaysAllow": "Kimlik Doğrulamayı Atla",
"alwaysDeny": "Erişimi Engelle",
"passToAuth": "Kimlik Doğrulamasına Geç",
"orgSettingsDescription": "Organizasyonunuzun genel ayarlarını yapılandırın",
"orgGeneralSettings": "Organizasyon Ayarları",
@@ -232,7 +244,7 @@
"orgMissing": "Organizasyon Kimliği Eksik",
"orgMissingMessage": "Organizasyon kimliği olmadan daveti yeniden oluşturmanız mümkün değildir.",
"accessUsersManage": "Kullanıcıları Yönet",
"accessUsersDescription": "Kullanıcıları davet edin ve erişimi yönetmek için rollere ekleyin",
"accessUsersDescription": "Bu organizasyona erişimi olan kullanıcıları davet edin ve yönetin",
"accessUsersSearch": "Kullanıcıları ara...",
"accessUserCreate": "Kullanıcı Oluştur",
"accessUserRemove": "Kullanıcıyı Kaldır",
@@ -241,13 +253,13 @@
"role": "Rol",
"nameRequired": "Ad gereklidir",
"accessRolesManage": "Rolleri Yönet",
"accessRolesDescription": "Organizasyonunuza erişimi yönetmek için rolleri yapılandırın",
"accessRolesDescription": "Organizasyondaki kullanıcılar için rolleri oluşturun ve yönetin",
"accessRolesSearch": "Rolleri ara...",
"accessRolesAdd": "Rol Ekle",
"accessRoleDelete": "Rolü Sil",
"description": "Açıklama",
"inviteTitle": "Açık Davetiyeler",
"inviteDescription": "Davetiyelerinizi diğer kullanıcılarla yönetin",
"inviteDescription": "Organizasyona katılmak için diğer kullanıcılar için davetleri yönetin",
"inviteSearch": "Davetiyeleri ara...",
"minutes": "Dakika",
"hours": "Saat",
@@ -424,7 +436,7 @@
"userCreated": "Kullanıcı oluşturuldu",
"userCreatedDescription": "Kullanıcı başarıyla oluşturulmuştur.",
"userTypeInternal": "Dahili Kullanıcı",
"userTypeInternalDescription": "Kullanıcınızı doğrudan organizasyonunuza davet edin.",
"userTypeInternalDescription": "Kullanıcıyı doğrudan organizasyona davet edin.",
"userTypeExternal": "Harici Kullanıcı",
"userTypeExternalDescription": "Harici bir kimlik sağlayıcısıyla kullanıcı oluşturun.",
"accessUserCreateDescription": "Yeni bir kullanıcı oluşturmak için aşağıdaki adımları izleyin",
@@ -436,6 +448,16 @@
"inviteEmailSent": "Kullanıcıya davet e-postası gönder",
"inviteValid": "Geçerli Süresi",
"selectDuration": "Süreyi seçin",
"selectResource": "Kaynak Seçin",
"filterByResource": "Kaynağa Göre Filtrele",
"resetFilters": "Filtreleri Sıfırla",
"totalBlocked": "Pangolin Tarafından Engellenen İstekler",
"totalRequests": "Toplam İstekler",
"requestsByCountry": "Ülkeye Göre İstekler",
"requestsByDay": "Güne Göre İstekler",
"blocked": "Engellendi",
"allowed": "İzin Verildi",
"topCountries": "En İyi Ülkeler",
"accessRoleSelect": "Rol seçin",
"inviteEmailSentDescription": "Kullanıcıya erişim bağlantısı ile bir e-posta gönderildi. Daveti kabul etmek için bağlantıya erişmelidirler.",
"inviteSentDescription": "Kullanıcı davet edilmiştir. Daveti kabul etmek için aşağıdaki bağlantıya erişmelidirler.",
@@ -458,13 +480,13 @@
"accessControlsSubmit": "Erişim Kontrollerini Kaydet",
"roles": "Roller",
"accessUsersRoles": "Kullanıcılar ve Roller Yönetin",
"accessUsersRolesDescription": "Kullanıcılara davet gönderin ve organizasyonunuza erişim yönetmek için rollere ekleyin",
"accessUsersRolesDescription": "Kullanıcılara davet gönderin ve organizasyona erişimi yönetmek için rollere ekleyin",
"key": "Anahtar",
"createdAt": "Oluşturulma Tarihi",
"proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.",
"proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.",
"proxyEnableSSL": "SSL Etkinleştir",
"proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.",
"proxyEnableSSLDescription": "Hedeflere güvenli HTTPS bağlantıları için SSL/TLS şifrelemesini etkinleştirin.",
"target": "Hedef",
"configureTarget": "Hedefleri Yapılandır",
"targetErrorFetch": "Hedefleri alamadı",
@@ -480,29 +502,29 @@
"targetsErrorUpdate": "Hedefler güncellenemedi",
"targetsErrorUpdateDescription": "Hedefler güncellenirken bir hata oluştu",
"targetTlsUpdate": "TLS ayarları güncellendi",
"targetTlsUpdateDescription": "TLS ayarlarınız başarıyla güncellendi",
"targetTlsUpdateDescription": "TLS ayarları başarıyla güncellendi",
"targetErrorTlsUpdate": "TLS ayarları güncellenemedi",
"targetErrorTlsUpdateDescription": "TLS ayarlarını güncellerken bir hata oluştu",
"proxyUpdated": "Proxy ayarları güncellendi",
"proxyUpdatedDescription": "Proxy ayarlarınız başarıyla güncellenmiştir",
"proxyUpdatedDescription": "Proxy ayarları başarıyla güncellendi",
"proxyErrorUpdate": "Proxy ayarları güncellenemedi",
"proxyErrorUpdateDescription": "Proxy ayarlarını güncellerken bir hata oluştu",
"targetAddr": "IP / Hostname",
"targetAddr": "Host",
"targetPort": "Bağlantı Noktası",
"targetProtocol": "Protokol",
"targetTlsSettings": "HTTPS & TLS Settings",
"targetTlsSettingsDescription": "Configure TLS settings for your resource",
"targetTlsSettingsDescription": "SSL/TLS ayarlarını kaynak için yapılandırın",
"targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları",
"targetTlsSni": "TLS Sunucu Adı",
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
"targetTlsSubmit": "Ayarları Kaydet",
"targets": "Hedefler Konfigürasyonu",
"targetsDescription": "Trafiği arka uç hizmetlerinize yönlendirmek için hedefleri ayarlayın",
"targetsDescription": "Trafiği arka uç hizmetlerine yönlendirmek için hedefleri ayarlayın",
"targetStickySessions": "Yapışkan Oturumları Etkinleştir",
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
"methodSelect": "Yöntemi Seç",
"targetSubmit": "Hedef Ekle",
"targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.",
"targetNoOne": "Bu kaynağın hedefleri yok. Arka uca gönderilecek istekleri yapılandırmak için bir hedef ekleyin.",
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
"targetsSubmit": "Hedefleri Kaydet",
"addTarget": "Hedef Ekle",
@@ -516,9 +538,11 @@
"targetCreatedDescription": "Hedef başarıyla oluşturuldu",
"targetErrorCreate": "Hedef oluşturma başarısız oldu",
"targetErrorCreateDescription": "Hedef oluşturulurken bir hata oluştu",
"tlsServerName": "TLS Sunucu Adı",
"tlsServerNameDescription": "SNI için kullanılacak TLS sunucu adı",
"save": "Kaydet",
"proxyAdditional": "Ek Proxy Ayarları",
"proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın",
"proxyAdditionalDescription": "Kaynağın proxy ayarlarını nasıl yöneteceği yapılandırın",
"proxyCustomHeader": "Özel Ana Bilgisayar Başlığı",
"proxyCustomHeaderDescription": "İstekleri proxy'lerken ayarlanacak ana bilgisayar başlığı. Varsayılanı kullanmak için boş bırakılır.",
"proxyAdditionalSubmit": "Proxy Ayarlarını Kaydet",
@@ -558,7 +582,7 @@
"rulesMatchType": "Eşleşme Türü",
"value": "Değer",
"rulesAbout": "Kurallar Hakkında",
"rulesAboutDescription": "Kurallar, kaynağınıza erişimi belirli bir kriterlere göre kontrol etmenizi sağlar. IP adresi veya URL yolu temelinde erişimi izin vermek veya engellemek için kurallar oluşturabilirsiniz.",
"rulesAboutDescription": "Kurallar, kaynağa erişimi belirli kriterlere göre kontrol etmenizi sağlar. IP adresi veya URL yolu temelinde erişimi izin vermek veya engellemek için kurallar oluşturabilirsiniz.",
"rulesActions": "Aksiyonlar",
"rulesActionAlwaysAllow": "Her Zaman İzin Ver: Tüm kimlik doğrulama yöntemlerini atlayın",
"rulesActionAlwaysDeny": "Her Zaman Reddedin: Tüm istekleri engelleyin; kimlik doğrulaması yapılamaz",
@@ -570,7 +594,7 @@
"rulesEnable": "Kuralları Etkinleştir",
"rulesEnableDescription": "Bu kaynak için kural değerlendirmesini etkinleştirin veya devre dışı bırakın",
"rulesResource": "Kaynak Kuralları Yapılandırması",
"rulesResourceDescription": "Kaynağınıza erişimi kontrol etmek için kuralları yapılandırın",
"rulesResourceDescription": "Kaynağa erişimi kontrol etmek için kuralları yapılandırın",
"ruleSubmit": "Kural Ekle",
"rulesNoOne": "Kural yok. Formu kullanarak bir kural ekleyin.",
"rulesOrder": "Kurallar, artan öncelik sırasına göre değerlendirilir.",
@@ -586,7 +610,7 @@
"none": "Hiçbiri",
"unknown": "Bilinmiyor",
"resources": "Kaynaklar",
"resourcesDescription": "Kaynaklar, özel ağınızda çalışan uygulamalara proxy görevi görür. Özel ağınızdaki herhangi bir HTTP/HTTPS veya ham TCP/UDP hizmeti için bir kaynak oluşturun. Her kaynak, şifreli bir WireGuard tüneli aracılığıyla özel ve güvenli bağlantıyı etkinleştirmek için bir siteye bağlı olmalıdır.",
"resourcesDescription": "Kaynaklar, özel ağda çalışan uygulamalara proxy olarak hizmet eder. Özel ağınızdaki herhangi bir HTTP/HTTPS veya ham TCP/UDP hizmeti için bir kaynak oluşturun. Her kaynak, şifreli bir WireGuard tüneli aracılığıyla özel, güvenli bağlanabilirliği etkinleştirmek için bir siteye bağlı olmalıdır.",
"resourcesWireGuardConnect": "WireGuard şifreleme ile güvenli bağlantı",
"resourcesMultipleAuthenticationMethods": "Birden fazla kimlik doğrulama yöntemi yapılandırın",
"resourcesUsersRolesAccess": "Kullanıcı ve rol tabanlı erişim kontrolü",
@@ -607,19 +631,19 @@
"unknownCommand": "Bilinmeyen komut",
"newtErrorFetchReleases": "Sürüm bilgileri alınamadı: {err}",
"newtErrorFetchLatest": "Son sürüm alınırken hata: {err}",
"newtEndpoint": "Newt Uç Noktası",
"newtId": "Newt Kimliği",
"newtSecretKey": "Newt Gizli Anahtarı",
"newtEndpoint": "Uç Nokta",
"newtId": "Kimlik",
"newtSecretKey": "Gizli",
"architecture": "Mimari",
"sites": "Siteler",
"siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklarınıza eş IP adresini kullanarak erişmeniz gerekecek.",
"siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.",
"siteWgCompatibleAllClients": "Tüm WireGuard istemcileriyle uyumlu",
"siteWgManualConfigurationRequired": "Manuel yapılandırma gerekli",
"userErrorNotAdminOrOwner": "Kullanıcı yönetici veya sahibi değil",
"pangolinSettings": "Ayarlar - Pangolin",
"accessRoleYour": "Rolünüz:",
"accessRoleSelect2": "Bir rol seçin",
"accessUserSelect": "Bir kullanıcı seçin",
"accessRoleSelect2": "Rolleri seçin",
"accessUserSelect": "Kullanıcıları seçin",
"otpEmailEnter": "Bir e-posta girin",
"otpEmailEnterDescription": "E-posta girdikten sonra girdi alanına yazıp enter'a basın.",
"otpEmailErrorInvalid": "Geçersiz e-posta adresi. Joker karakter (*) yerel kısmın tamamı olmalıdır.",
@@ -671,9 +695,9 @@
"resourcePincodeSetupTitle": "Pincode Ayarla",
"resourcePincodeSetupTitleDescription": "Bu kaynağı korumak için bir pincode ayarlayın",
"resourceRoleDescription": "Yöneticiler her zaman bu kaynağa erişebilir.",
"resourceUsersRoles": "Kullanıcılar ve Roller",
"resourceUsersRoles": "Erişim Kontrolleri",
"resourceUsersRolesDescription": "Bu kaynağı kimlerin ziyaret edebileceği kullanıcıları ve rolleri yapılandırın",
"resourceUsersRolesSubmit": "Kullanıcıları ve Rolleri Kaydet",
"resourceUsersRolesSubmit": "Erişim Kontrollerini Kaydet",
"resourceWhitelistSave": "Başarıyla kaydedildi",
"resourceWhitelistSaveDescription": "Beyaz liste ayarları kaydedildi",
"ssoUse": "Platform SSO'sunu Kullanın",
@@ -702,6 +726,7 @@
"resourceTransferSubmit": "Kaynağı Aktar",
"siteDestination": "Hedef Site",
"searchSites": "Siteleri ara",
"countries": "Ülkeler",
"accessRoleCreate": "Rol Oluştur",
"accessRoleCreateDescription": "Kullanıcıları gruplamak ve izinlerini yönetmek için yeni bir rol oluşturun.",
"accessRoleCreateSubmit": "Rol Oluştur",
@@ -909,6 +934,10 @@
"passwordResetSent": "Bu e-posta adresine bir şifre sıfırlama kodu gönderilecektir.",
"passwordResetCode": "Sıfırlama Kodu",
"passwordResetCodeDescription": "E-posta gelen kutunuzda sıfırlama kodunu kontrol edin.",
"generatePasswordResetCode": "Parola Sıfırlama Kodunu Oluştur",
"passwordResetCodeGenerated": "Parola Sıfırlama Kodu Oluşturuldu",
"passwordResetCodeGeneratedDescription": "Bu kodu kullanıcı ile paylaşın. Parolalarını sıfırlamak için bunu kullanabilirler.",
"passwordResetUrl": "Parola Sıfırlama URL'si",
"passwordNew": "Yeni Şifre",
"passwordNewConfirm": "Yeni Şifreyi Onayla",
"changePassword": "Parola Değiştir",
@@ -926,6 +955,9 @@
"pincodeAuth": "Kimlik Doğrulama Kodu",
"pincodeSubmit2": "Kodu Gönder",
"passwordResetSubmit": "Sıfırlama İsteği",
"passwordResetAlreadyHaveCode": "Kodu Girin",
"passwordResetSmtpRequired": "Yönetici ile iletişime geçin",
"passwordResetSmtpRequiredDescription": "Parolanızı sıfırlamak için bir parola sıfırlama kodu gereklidir. Yardım için yönetici ile iletişime geçin.",
"passwordBack": "Şifreye Geri Dön",
"loginBack": "Girişe geri dön",
"signup": "Kaydol",
@@ -1013,6 +1045,7 @@
"updateOrgUser": "Organizasyon Kullanıcısını Güncelle",
"createOrgUser": "Organizasyon Kullanıcısı Oluştur",
"actionUpdateOrg": "Kuruluşu Güncelle",
"actionRemoveInvitation": "Daveti Kaldır",
"actionUpdateUser": "Kullanıcıyı Güncelle",
"actionGetUser": "Kullanıcıyı Getir",
"actionGetOrgUser": "Kuruluş Kullanıcısını Al",
@@ -1022,6 +1055,8 @@
"actionGetSite": "Siteyi Al",
"actionListSites": "Siteleri Listele",
"actionApplyBlueprint": "Planı Uygula",
"actionListBlueprints": "Plan Listesini Görüntüle",
"actionGetBlueprint": "Planı Elde Et",
"setupToken": "Kurulum Simgesi",
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
"setupTokenRequired": "Kurulum simgesi gerekli",
@@ -1091,12 +1126,15 @@
"actionListSiteResources": "Site Kaynaklarını Listele",
"actionUpdateSiteResource": "Site Kaynağını Güncelle",
"actionListInvitations": "Davetiyeleri Listele",
"actionExportLogs": "Kayıtları Dışa Aktar",
"actionViewLogs": "Kayıtları Görüntüle",
"noneSelected": "Hiçbiri seçili değil",
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
"searchProgress": "Ara...",
"create": "Oluştur",
"orgs": "Organizasyonlar",
"loginError": "Giriş yaparken bir hata oluştu",
"loginRequiredForDevice": "Cihazınızı kimlik doğrulamak için giriş yapılması gereklidir.",
"passwordForgot": "Şifrenizi mi unuttunuz?",
"otpAuth": "İki Faktörlü Kimlik Doğrulama",
"otpAuthDescription": "Authenticator uygulamanızdan veya tek kullanımlık yedek kodlarınızdan birini girin.",
@@ -1151,19 +1189,29 @@
"sidebarHome": "Ana Sayfa",
"sidebarSites": "Siteler",
"sidebarResources": "Kaynaklar",
"sidebarProxyResources": "Herkese Açık",
"sidebarClientResources": "Özel",
"sidebarAccessControl": "Erişim Kontrolü",
"sidebarLogsAndAnalytics": "Kayıtlar & Analitik",
"sidebarUsers": "Kullanıcılar",
"sidebarAdmin": "Yönetici",
"sidebarInvitations": "Davetiye",
"sidebarRoles": "Roller",
"sidebarShareableLinks": "Paylaşılabilir Bağlantılar",
"sidebarShareableLinks": "Bağlantılar",
"sidebarApiKeys": "API Anahtarları",
"sidebarSettings": "Ayarlar",
"sidebarAllUsers": "Tüm Kullanıcılar",
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
"sidebarLicense": "Lisans",
"sidebarClients": "İstemciler",
"sidebarUserDevices": "Kullanıcılar",
"sidebarMachineClients": "Makineler",
"sidebarDomains": "Alan Adları",
"sidebarGeneral": "Yönet",
"sidebarLogAndAnalytics": "Kayıt & Analiz",
"sidebarBluePrints": "Planlar",
"sidebarOrganization": "Organizasyon",
"sidebarLogsAnalytics": "Analitik",
"blueprints": "Planlar",
"blueprintsDescription": "Deklaratif yapılandırmaları uygulayın ve önceki çalışmaları görüntüleyin",
"blueprintAdd": "Plan Ekle",
@@ -1174,7 +1222,7 @@
"blueprintDetailsDescription": "Uygulanan mavi yazılımın sonucunu ve oluşan hataları görün",
"blueprintInfo": "Plan Bilgileri",
"message": "Mesaj",
"blueprintContentsDescription": "Altyapınızı tanımlayan YAML içeriğini tanımlayın",
"blueprintContentsDescription": "Altyapıyı tanımlayan YAML içeriğini belirleyin",
"blueprintErrorCreateDescription": "Plan uygulanırken bir hata oluştu",
"blueprintErrorCreate": "Plan oluşturulurken hata oluştu",
"searchBlueprintProgress": "Planlarda ara...",
@@ -1230,15 +1278,15 @@
"loading": "Yükleniyor",
"restart": "Yeniden Başlat",
"domains": "Alan Adları",
"domainsDescription": "Organizasyonunuz için alan adlarını yönetin",
"domainsDescription": "Organizasyonda kullanılabilir alan adlarını oluşturun ve yönetin",
"domainsSearch": "Alan adlarını ara...",
"domainAdd": "Alan Adı Ekle",
"domainAddDescription": "Organizasyonunuz için yeni bir alan adı kaydedin",
"domainCreate": "Alan Adı Oluştur",
"domainCreatedDescription": "Alan adı başarıyla oluşturuldu",
"domainDeletedDescription": "Alan adı başarıyla silindi",
"domainQuestionRemove": "Alan adını hesabınızdan kaldırmak istediğinizden emin misiniz?",
"domainMessageRemove": "Kaldırıldığında, alan adı hesabınızla ilişkilendirilmeyecek.",
"domainQuestionRemove": "Alan adını kaldırmak istediğinizden emin misiniz?",
"domainMessageRemove": "Kaldırıldığında, alan adı artık organizasyonunuzla ilişkilendirilmez.",
"domainConfirmDelete": "Alan Adı Silinmesini Onayla",
"domainDelete": "Alan Adını Sil",
"domain": "Alan Adı",
@@ -1273,8 +1321,11 @@
"accountSetupSuccess": "Hesap kurulumu tamamlandı! Pangolin'e hoş geldiniz!",
"documentation": "Dokümantasyon",
"saveAllSettings": "Tüm Ayarları Kaydet",
"saveResourceTargets": "Hedefleri Kaydet",
"saveResourceHttp": "Proxy Ayarlarını Kaydet",
"saveProxyProtocol": "Proxy protokol ayarlarını kaydet",
"settingsUpdated": "Ayarlar güncellendi",
"settingsUpdatedDescription": "Tüm ayarlar başarıyla güncellendi",
"settingsUpdatedDescription": "Ayarlar başarıyla güncellendi",
"settingsErrorUpdate": "Ayarlar güncellenemedi",
"settingsErrorUpdateDescription": "Ayarları güncellerken bir hata oluştu",
"sidebarCollapse": "Daralt",
@@ -1285,9 +1336,9 @@
"productUpdateTitle": "Ürün Güncellemeleri",
"productUpdateEmpty": "Güncelleme yok",
"dismissAll": "Hepsini Kapat",
"pangolinUpdateAvailable": "Yeni sürüm mevcut",
"pangolinUpdateAvailable": "Güncelleme Mevcut",
"pangolinUpdateAvailableInfo": "Sürüm {version} yüklenmeye hazır",
"pangolinUpdateAvailableReleaseNotes": "Sürüm notlarını görüntüleyin",
"pangolinUpdateAvailableReleaseNotes": "Yayın Notlarını Görüntüle",
"newtUpdateAvailable": "Güncelleme Mevcut",
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
"domainPickerEnterDomain": "Alan Adı",
@@ -1300,7 +1351,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Kullanılabilirlik kontrol ediliyor...",
"domainPickerNoMatchingDomains": "Eşleşen domain bulunamadı. Farklı bir domain deneyin veya organizasyonunuzun domain ayarlarını kontrol edin.",
"domainPickerNoMatchingDomains": "Eşleşen alan adı bulunamadı. Farklı bir alan adı deneyin veya organizasyonunuzun alan ayarlarını kontrol edin.",
"domainPickerOrganizationDomains": "Organizasyon Alan Adları",
"domainPickerProvidedDomains": "Sağlanan Alan Adları",
"domainPickerSubdomain": "Alt Alan: {subdomain}",
@@ -1345,9 +1396,9 @@
"billingPortalError": "Portal Hatası",
"billingDataUsageInfo": "Buluta bağlandığınızda, güvenli tünellerinizden aktarılan tüm verilerden ücret alınırsınız. Bu, tüm sitelerinizdeki gelen ve giden trafiği içerir. Limitinize ulaştığınızda, planınızı yükseltmeli veya kullanımı azaltmalısınız, aksi takdirde siteleriniz bağlantıyı keser. Düğümler kullanırken verilerden ücret alınmaz.",
"billingOnlineTimeInfo": "Sitelerinizin buluta ne kadar süre bağlı kaldığına göre ücretlendirilirsiniz. Örneğin, 44,640 dakika, bir sitenin 24/7 boyunca tam bir ay boyunca çalışması anlamına gelir. Limitinize ulaştığınızda, planınızı yükseltmeyip kullanımı azaltmazsanız siteleriniz bağlantıyı keser. Düğümler kullanırken zamandan ücret alınmaz.",
"billingUsersInfo": "Kuruluşunuzdaki her kullanıcı için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif kullanıcı hesaplarının sayısına göre günlük olarak hesaplanır.",
"billingDomainInfo": "Kuruluşunuzdaki her alan adı için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif alan adları hesaplarının sayısına göre günlük olarak hesaplanır.",
"billingRemoteExitNodesInfo": "Kuruluşunuzdaki her yönetilen Düğüm için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif yönetilen Düğümler sayısına göre günlük olarak hesaplanır.",
"billingUsersInfo": "Kuruluşunuzdaki her kullanıcı için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif kullanıcı hesaplarının sayısına göre günlük olarak hesaplanır.",
"billingDomainInfo": "Kuruluşunuzdaki her alan adı için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif alan adları hesaplarının sayısına göre günlük olarak hesaplanır.",
"billingRemoteExitNodesInfo": "Kuruluşunuzdaki her yönetilen Düğüm için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif yönetilen Düğümler sayısına göre günlük olarak hesaplanır.",
"domainNotFound": "Alan Adı Bulunamadı",
"domainNotFoundDescription": "Bu kaynak devre dışıdır çünkü alan adı sistemimizde artık mevcut değil. Bu kaynak için yeni bir alan adı belirleyin.",
"failed": "Başarısız",
@@ -1430,28 +1481,31 @@
"and": "ve",
"privacyPolicy": "gizlilik politikası"
},
"signUpMarketing": {
"keepMeInTheLoop": "Bana e-posta yoluyla haberler, güncellemeler ve yeni özellikler hakkında bilgi verin."
},
"siteRequired": "Site gerekli.",
"olmTunnel": "Olm Tüneli",
"olmTunnelDescription": "Müşteri bağlantıları için Olm kullanın",
"errorCreatingClient": "Müşteri oluşturulurken hata oluştu",
"clientDefaultsNotFound": "Müşteri varsayılanları bulunamadı",
"createClient": "Müşteri Oluştur",
"createClientDescription": "Sitelerinize bağlanmak için yeni bir müşteri oluşturun",
"createClientDescription": "Özel kaynaklara erişmek için yeni bir istemci oluşturun",
"seeAllClients": "Tüm Müşterileri Gör",
"clientInformation": "Müşteri Bilgileri",
"clientNamePlaceholder": "Müşteri adı",
"address": "Adres",
"subnetPlaceholder": "Alt ağ",
"addressDescription": "Bu müşteri için bağlantıda kullanılacak adres",
"addressDescription": "İstemcinin dahili adresi. Organizasyon alt ağı içinde olmalıdır.",
"selectSites": "Siteleri seçin",
"sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır",
"clientInstallOlm": "Olm Yükle",
"clientInstallOlmDescription": "Sisteminizde Olm çalıştırın",
"clientOlmCredentials": "Olm Kimlik Bilgileri",
"clientOlmCredentialsDescription": "Bu, Olm'in sunucu ile kimlik doğrulaması yapacağı yöntemdir",
"olmEndpoint": "Olm Uç Noktası",
"olmId": "Olm Kimliği",
"olmSecretKey": "Olm Gizli Anahtarı",
"clientOlmCredentialsDescription": "Bu, istemcinin sunucu ile kimlik doğrulaması yapacağı yöntemdir",
"olmEndpoint": "Uç Nokta",
"olmId": "Kimlik",
"olmSecretKey": "Gizli",
"clientCredentialsSave": "Kimlik Bilgilerinizi Kaydedin",
"clientCredentialsSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.",
"generalSettingsDescription": "Bu müşteri için genel ayarları yapılandırın",
@@ -1463,15 +1517,14 @@
"sitesFetchError": "Siteler alınırken bir hata oluştu.",
"olmErrorFetchReleases": "Olm yayınları alınırken bir hata oluştu.",
"olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.",
"remoteSubnets": "Uzak Alt Ağlar",
"enterCidrRange": "CIDR aralığını girin",
"remoteSubnetsDescription": "Bu siteye uzaktan erişilebilen CIDR aralıklarını ekleyin. 10.0.0.0/24 formatını kullanın. Bu YALNIZCA VPN istemci bağlantıları için geçerlidir.",
"resourceEnableProxy": "Genel Proxy'i Etkinleştir",
"resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.",
"externalProxyEnabled": "Dış Proxy Etkinleştirildi",
"addNewTarget": "Yeni Hedef Ekle",
"targetsList": "Hedefler Listesi",
"advancedMode": "Gelişmiş Mod",
"advancedSettings": "Gelişmiş Ayarlar",
"targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu",
"healthCheckHealthy": "Sağlıklı",
"healthCheckUnhealthy": "Sağlıksız",
@@ -1483,14 +1536,15 @@
"enableHealthChecksDescription": "Bu hedefin sağlığını izleyin. Gerekirse hedef dışındaki bir son noktayı izleyebilirsiniz.",
"healthScheme": "Yöntem",
"healthSelectScheme": "Yöntem Seç",
"healthCheckPortInvalid": "Sağlık Kontrolü portu 1 ile 65535 arasında olmalıdır",
"healthCheckPath": "Yol",
"healthHostname": "IP / Hostname",
"healthPort": "Bağlantı Noktası",
"healthCheckPathDescription": "Sağlık durumunu kontrol etmek için yol.",
"healthyIntervalSeconds": "Sağlıklı Aralık",
"unhealthyIntervalSeconds": "Sağlıksız Aralık",
"healthyIntervalSeconds": "Sağlıklı Aralık (saniye)",
"unhealthyIntervalSeconds": "Sağlıksız Aralık (saniye)",
"IntervalSeconds": "Sağlıklı Aralık",
"timeoutSeconds": "Zaman Aşımı",
"timeoutSeconds": "Zaman Aşımı (saniye)",
"timeIsInSeconds": "Zaman saniye cinsindendir",
"retryAttempts": "Tekrar Deneme Girişimleri",
"expectedResponseCodes": "Beklenen Yanıt Kodları",
@@ -1526,12 +1580,12 @@
"resourceEditDomain": "Alan Adını Düzenle",
"siteName": "Site Adı",
"proxyPort": "Bağlantı Noktası",
"resourcesTableProxyResources": "Proxy Kaynaklar",
"resourcesTableClientResources": "İstemci Kaynaklar",
"resourcesTableProxyResources": "Herkese Açık",
"resourcesTableClientResources": "Özel",
"resourcesTableNoProxyResourcesFound": "Hiçbir proxy kaynağı bulunamadı.",
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
"resourcesTableDestination": "Hedef",
"resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için",
"resourcesTableAlias": "Takma Ad",
"resourcesTableClients": "İstemciler",
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
"resourcesTableNoTargets": "Hedef yok",
@@ -1540,8 +1594,8 @@
"resourcesTableOffline": "Çevrimdışı",
"resourcesTableUnknown": "Bilinmiyor",
"resourcesTableNotMonitored": "İzlenmiyor",
"editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin",
"editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.",
"editInternalResourceDialogEditClientResource": "Özel Kaynak Düzenleyin",
"editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak ayarlarını ve erişim kontrollerini güncelleyin",
"editInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
"editInternalResourceDialogName": "Ad",
"editInternalResourceDialogProtocol": "Protokol",
@@ -1560,17 +1614,27 @@
"editInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı",
"editInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır",
"editInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır",
"editInternalResourceDialogPortModeRequired": "Port modu için protokol, proxy portu ve hedef porta ihtiyaç vardır",
"editInternalResourceDialogMode": "Mod",
"editInternalResourceDialogModePort": "Bağlantı Noktası",
"editInternalResourceDialogModeHost": "Ev Sahibi",
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogDestination": "Hedef",
"editInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.",
"editInternalResourceDialogDestinationIPDescription": "Kaynağın site ağındaki IP veya ana bilgisayar adresi.",
"editInternalResourceDialogDestinationCidrDescription": "Site ağındaki kaynağın CIDR aralığı.",
"editInternalResourceDialogAlias": "Takma Ad",
"editInternalResourceDialogAliasDescription": "Bu kaynak için isteğe bağlı dahili DNS takma adı.",
"createInternalResourceDialogNoSitesAvailable": "Site Bulunamadı",
"createInternalResourceDialogNoSitesAvailableDescription": "Dahili kaynak oluşturmak için en az bir Newt sitesine ve alt ağa sahip olmalısınız.",
"createInternalResourceDialogClose": "Kapat",
"createInternalResourceDialogCreateClientResource": "İstemci Kaynağı Oluştur",
"createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemciler için erişilebilir olacak yeni bir kaynak oluşturun.",
"createInternalResourceDialogCreateClientResource": "Özel Kaynak Oluştur",
"createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemcilere erişilebilir olacak yeni bir kaynak oluşturun",
"createInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
"createInternalResourceDialogName": "Ad",
"createInternalResourceDialogSite": "Site",
"createInternalResourceDialogSelectSite": "Site seç...",
"createInternalResourceDialogSearchSites": "Siteleri ara...",
"createInternalResourceDialogNoSitesFound": "Site bulunamadı.",
"selectSite": "Site seç...",
"noSitesFound": "Site bulunamadı.",
"createInternalResourceDialogProtocol": "Protokol",
"createInternalResourceDialogTcp": "TCP",
"createInternalResourceDialogUdp": "UDP",
@@ -1593,13 +1657,24 @@
"createInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı",
"createInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır",
"createInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır",
"createInternalResourceDialogPortModeRequired": "Port modu için protokol, proxy portu ve hedef porta ihtiyaç vardır",
"createInternalResourceDialogMode": "Mod",
"createInternalResourceDialogModePort": "Bağlantı Noktası",
"createInternalResourceDialogModeHost": "Ev Sahibi",
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogDestination": "Hedef",
"createInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.",
"createInternalResourceDialogDestinationCidrDescription": "Site ağındaki kaynağın CIDR aralığı.",
"createInternalResourceDialogAlias": "Takma Ad",
"createInternalResourceDialogAliasDescription": "Bu kaynak için isteğe bağlı dahili DNS takma adı.",
"siteConfiguration": "Yapılandırma",
"siteAcceptClientConnections": "İstemci Bağlantılarını Kabul Et",
"siteAcceptClientConnectionsDescription": "Bu Newt örneğini bir geçit olarak kullanarak diğer cihazların bağlanmasına izin verin.",
"siteAddress": "Site Adresi",
"siteAddressDescription": "İstemcilerin bağlanması için hostun IP adresini belirtin. Bu, Pangolin ağındaki sitenin iç adresidir ve istemciler için atlas olmalıdır. Org alt ağına düşmelidir.",
"siteAcceptClientConnectionsDescription": "Kullanıcı cihazları ve istemcilerin bu sitedeki kaynaklara erişmesine izin verin. Bu daha sonra değiştirilebilir.",
"siteAddress": "Site Adresi (Gelişmiş)",
"siteAddressDescription": "Site için dahili adres. Organizasyon alt ağı içinde olmalıdır.",
"siteNameDescription": "Sonradan değiştirilebilecek sitenin görünen adı.",
"autoLoginExternalIdp": "Harici IDP ile Otomatik Giriş",
"autoLoginExternalIdpDescription": "Kullanıcıyı kimlik doğrulama için otomatik olarak harici IDP'ye yönlendirin.",
"autoLoginExternalIdpDescription": "Kullanıcıyı kimlik doğrulama için hemen harici kimlik sağlayıcısına yönlendirin.",
"selectIdp": "IDP Seç",
"selectIdpPlaceholder": "IDP seçin...",
"selectIdpRequired": "Otomatik giriş etkinleştirildiğinde lütfen bir IDP seçin.",
@@ -1611,7 +1686,7 @@
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
"remoteExitNodeManageRemoteExitNodes": "Uzak Düğümler",
"remoteExitNodeDescription": "Kendi konum ağlarınızdan bir veya daha fazlasını barındırarak, bağlantı kurulumları için buluta bağımlılığı azaltın.",
"remoteExitNodeDescription": "Kendi uzaktan yönetilen ileti ve ara sunucu düğümlerinizi barındırın",
"remoteExitNodes": "Düğümler",
"searchRemoteExitNodes": "Düğüm ara...",
"remoteExitNodeAdd": "Düğüm Ekle",
@@ -1621,20 +1696,22 @@
"remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla",
"remoteExitNodeDelete": "Düğümü Sil",
"sidebarRemoteExitNodes": "Uzak Düğümler",
"remoteExitNodeId": "Kimlik",
"remoteExitNodeSecretKey": "Gizli",
"remoteExitNodeCreate": {
"title": "Düğüm Oluştur",
"description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun",
"title": "Uzak Düğüm Oluştur",
"description": "Yeni bir kendine misafir uzaktan ileti ve ara sunucu düğümü oluşturun",
"viewAllButton": "Tüm Düğümleri Gör",
"strategy": {
"title": "Oluşturma Stratejisi",
"description": "Düğümünüzü manuel olarak yapılandırmak veya yeni kimlik bilgileri oluşturmak için bunu seçin.",
"description": "Uzak düğümü nasıl oluşturmak istediğinizi seçin",
"adopt": {
"title": "Düğüm Benimse",
"description": "Zaten düğüm için kimlik bilgilerine sahipseniz bunu seçin."
},
"generate": {
"title": "Anahtarları Oluştur",
"description": "Düğüm için yeni anahtarlar oluşturmak istiyorsanız bunu seçin"
"description": "Düğüm için yeni anahtarlar oluşturmak istiyorsanız bunu seçin."
}
},
"adopt": {
@@ -1648,7 +1725,7 @@
},
"generate": {
"title": "Oluşturulan Kimlik Bilgileri",
"description": "Düğümünüzü yapılandırmak için oluşturulan bu kimlik bilgilerini kullanın",
"description": "Düğümünüzü yapılandırmak için bu oluşturulan kimlik bilgilerini kullanın",
"nodeIdTitle": "Düğüm ID",
"secretTitle": "Gizli",
"saveCredentialsTitle": "Kimlik Bilgilerini Yapılandırmaya Ekle",
@@ -1730,7 +1807,7 @@
"idpAzureConfiguration": "Azure Entra ID Yapılandırması",
"idpAzureConfigurationDescription": "Azure Entra ID OAuth2 kimlik bilgilerinizi yapılandırın",
"idpTenantId": "Kiracı Kimliği",
"idpTenantIdPlaceholder": "kiraci-kimliginiz",
"idpTenantIdPlaceholder": "kiracı Kimliği",
"idpAzureTenantIdDescription": "Azure kiracı kimliğiniz (Azure Active Directory genel bakışında bulunur)",
"idpAzureClientIdDescription": "Azure Uygulama Kaydı İstemci Kimliğiniz",
"idpAzureClientSecretDescription": "Azure Uygulama Kaydı İstemci Sırrınız",
@@ -1747,9 +1824,30 @@
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC sağlayıcısı",
"subnet": "Alt ağ",
"subnetDescription": "Bu organizasyonun ağ yapılandırması için alt ağ.",
"authPage": "Yetkilendirme Sayfası",
"authPageDescription": "Kuruluşunuz için yetkilendirme sayfasını yapılandırın",
"customDomain": "Özel Alan",
"authPage": "Kimlik Sayfaları",
"authPageDescription": "Kuruluşun kimlik doğrulama sayfaları için özel bir alan belirleyin",
"authPageDomain": "Yetkilendirme Sayfası Alanı",
"authPageBranding": "Özel Marka",
"authPageBrandingDescription": "Bu kuruluşa ait kimlik doğrulama sayfalarında görünecek markayı yapılandırın",
"authPageBrandingUpdated": "Kimlik doğrulama sayfası marka güncellemesi başarıyla tamamlandı",
"authPageBrandingRemoved": "Kimlik doğrulama sayfası marka kaldırıldı",
"authPageBrandingRemoveTitle": "Kimlik Doğrulama Sayfası Markası Kaldır",
"authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?",
"authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla",
"brandingLogoURL": "Logo URL",
"brandingPrimaryColor": "Ana Renk",
"brandingLogoWidth": "Genişlik (px)",
"brandingLogoHeight": "Yükseklik (px)",
"brandingOrgTitle": "Kuruluş Kimlik Sayfası için Başlık",
"brandingOrgDescription": "{orgName} kuruluş adı ile değiştirilecek",
"brandingOrgSubtitle": "Kuruluş Kimlik Sayfası için Alt Başlık",
"brandingResourceTitle": "Kaynak Yetkilendirme Sayfası için Başlık",
"brandingResourceSubtitle": "Kaynak Yetkilendirme Sayfası için Alt Başlık",
"brandingResourceDescription": "{resourceName} organizasyon adıyla değiştirilecek",
"saveAuthPageDomain": "Alan Adını Kaydet",
"saveAuthPageBranding": "Markayı Kaydet",
"removeAuthPageBranding": "Markayı Kaldır",
"noDomainSet": "Alan belirlenmedi",
"changeDomain": "Alanı Değiştir",
"selectDomain": "Alan Seçin",
@@ -1758,7 +1856,7 @@
"setAuthPageDomain": "Yetkilendirme Sayfası Alanını Ayarla",
"failedToFetchCertificate": "Sertifika getirilemedi",
"failedToRestartCertificate": "Sertifika yeniden başlatılamadı",
"addDomainToEnableCustomAuthPages": "Kuruluşunuz için özel kimlik doğrulama sayfalarını etkinleştirmek için bir alan ekleyin",
"addDomainToEnableCustomAuthPages": "Kullanıcılar, bu alanı kullanarak kuruluşun giriş sayfasına erişebilir ve kaynak kimlik doğrulamasını tamamlayabilir.",
"selectDomainForOrgAuthPage": "Kuruluşun kimlik doğrulama sayfası için bir alan seçin",
"domainPickerProvidedDomain": "Sağlanan Alan Adı",
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
@@ -1773,10 +1871,19 @@
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" {domain} için geçerli yapılamadı.",
"domainPickerSubdomainSanitized": "Alt alan adı temizlendi",
"domainPickerSubdomainCorrected": "\"{sub}\" \"{sanitized}\" olarak düzeltildi",
"orgAuthSignInTitle": "Kuruluşunuza giriş yapın",
"orgAuthSignInTitle": "Kuruluş Giriş",
"orgAuthChooseIdpDescription": "Devam etmek için kimlik sağlayıcınızı seçin",
"orgAuthNoIdpConfigured": "Bu kuruluşta yapılandırılmış kimlik sağlayıcı yok. Bunun yerine Pangolin kimliğinizle giriş yapabilirsiniz.",
"orgAuthSignInWithPangolin": "Pangolin ile Giriş Yap",
"orgAuthSignInToOrg": "Bir kuruluşa giriş yapın",
"orgAuthSelectOrgTitle": "Kuruluş Giriş",
"orgAuthSelectOrgDescription": "Devam etmek için kuruluş kimliğinizi girin",
"orgAuthOrgIdPlaceholder": "kuruluşunuz",
"orgAuthOrgIdHelp": "Kuruluşunuzun benzersiz tanımlayıcısını girin",
"orgAuthSelectOrgHelp": "Kuruluş kimliğinizi girdikten sonra, SSO veya kuruluş kimlik bilgilerinizi kullanabileceğiniz kuruluş giriş sayfanıza yönlendirileceksiniz.",
"orgAuthRememberOrgId": "Bu kuruluş kimliğini hatırla",
"orgAuthBackToSignIn": "Standart girişe geri dön",
"orgAuthNoAccount": "Hesabınız yok mu?",
"subscriptionRequiredToUse": "Bu özelliği kullanmak için abonelik gerekmektedir.",
"idpDisabled": "Kimlik sağlayıcılar devre dışı bırakılmıştır.",
"orgAuthPageDisabled": "Kuruluş kimlik doğrulama sayfası devre dışı bırakılmıştır.",
@@ -1791,6 +1898,8 @@
"enableTwoFactorAuthentication": "İki faktörlü kimlik doğrulamayı etkinleştir",
"completeSecuritySteps": "Güvenlik Adımlarını Tamamla",
"securitySettings": "Güvenlik Ayarları",
"dangerSection": "Tehlike Alanı",
"dangerSectionDescription": "Bu organizasyonla ilişkili tüm verileri kalıcı olarak silin",
"securitySettingsDescription": "Kuruluşunuz için güvenlik politikalarını yapılandırın",
"requireTwoFactorForAllUsers": "Tüm Kullanıcılar için İki Faktörlü Kimlik Doğrulama Gerektir",
"requireTwoFactorDescription": "Etkinleştirildiğinde, bu kuruluştaki tüm dahili kullanıcıların, kuruluşa erişmek için iki faktörlü kimlik doğrulama etkinleştirilmiş olmalıdır.",
@@ -1828,7 +1937,7 @@
"securityPolicyChangeWarningText": "Bu, organizasyondaki tüm kullanıcıları etkileyecektir",
"authPageErrorUpdateMessage": "Kimlik doğrulama sayfası ayarları güncellenirken bir hata oluştu.",
"authPageErrorUpdate": "Kimlik doğrulama sayfası güncellenemedi",
"authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi",
"authPageDomainUpdated": "Kimlik doğrulama sayfası alanı başarıyla güncellendi",
"healthCheckNotAvailable": "Yerel",
"rewritePath": "Yolu Yeniden Yaz",
"rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın.",
@@ -1854,8 +1963,19 @@
"enterpriseEdition": "Kurumsal Sürüm",
"unlicensed": "Lisansız",
"beta": "Beta",
"manageClients": "Müşteri Yönetimi",
"manageClientsDescription": "Müşteriler, sitelerinize bağlanabilen cihazlardır.",
"manageUserDevices": "Kullanıcı Cihazları",
"manageUserDevicesDescription": "Kullanıcıların kaynaklara özel olarak bağlanmak için kullandığı cihazları görüntüleyin ve yönetin",
"downloadClientBannerTitle": "Pangolin İstemcisini İndir",
"downloadClientBannerDescription": "Sisteminize Pangolin istemcisini indirerek Pangolin ağına bağlanın ve kaynaklara özel olarak erişim sağlayın.",
"manageMachineClients": "Makine İstemcilerini Yönetin",
"manageMachineClientsDescription": "Sunucuların ve sistemlerin kaynaklara özel olarak bağlanmak için kullandığı istemcileri oluşturun ve yönetin",
"machineClientsBannerTitle": "Sunucular ve Otomatik Sistemler",
"machineClientsBannerDescription": "Makine müşterileri, belirli bir kullanıcı ile ilişkilendirilmemiş sunucular ve otomatik sistemler içindir. Kimlik ve şifreyle doğrulama yaparlar ve Pangolin CLI, Olm CLI veya Olm'yi bir konteyner olarak çalıştırabilirler.",
"machineClientsBannerPangolinCLI": "Pangolin CLI",
"machineClientsBannerOlmCLI": "Olm CLI",
"machineClientsBannerOlmContainer": "Olm Konteyner",
"clientsTableUserClients": "Kullanıcı",
"clientsTableMachineClients": "Makine",
"licenseTableValidUntil": "Geçerli İki Tarih Kadar",
"saasLicenseKeysSettingsTitle": "Kurumsal Lisanslar",
"saasLicenseKeysSettingsDescription": "Kendi barındırdığınız Pangolin örnekleri için kurumsal lisans anahtarları oluşturun ve yönetin.",
@@ -1990,19 +2110,22 @@
"pathRewriteStripLabel": "sil",
"sidebarEnableEnterpriseLicense": "Kurumsal Lisans Etkinleştir",
"cannotbeUndone": "Bu geri alınamaz.",
"toConfirm": "doğrulamak için",
"toConfirm": "onaylamak için.",
"deleteClientQuestion": "Müşteriyi siteden ve organizasyondan kaldırmak istediğinizden emin misiniz?",
"clientMessageRemove": "Kaldırıldıktan sonra müşteri siteye bağlanamayacaktır.",
"sidebarLogs": "Kayıtlar",
"request": "İstek",
"requests": "İstekler",
"logs": "Günlükler",
"logsSettingsDescription": "Bu organizasyondan toplanan günlükleri izleyin",
"logsSettingsDescription": "Bu kuruluştan toplanan günlükleri izleyin",
"searchLogs": "Günlüklerde ara...",
"action": "Eylem",
"actor": "Aktör",
"timestamp": "Zaman damgası",
"accessLogs": "Erişim Günlükleri",
"exportCsv": "CSV Dışa Aktar",
"exportError": "CSV dışa aktarılırken bilinmeyen hata",
"exportCsvTooltip": "Zaman Aralığında",
"actorId": "Aktör Kimliği",
"allowedByRule": "Kurallara Göre İzin Verildi",
"allowedNoAuth": "Kimlik Doğrulama Yok İzin Verildi",
@@ -2020,6 +2143,7 @@
"ip": "IP",
"reason": "Sebep",
"requestLogs": "İstek Günlükleri",
"requestAnalytics": "İstek Analizi",
"host": "Sunucu",
"location": "Konum",
"actionLogs": "Eylem Günlükleri",
@@ -2029,6 +2153,7 @@
"logRetention": "Kayıt Saklama",
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
"logRetentionRequestLabel": "İstek Günlüğü Saklama",
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
"logRetentionAccessLabel": "Erişim Günlüğü Saklama",
@@ -2042,6 +2167,7 @@
"logRetention30Days": "30 gün",
"logRetention90Days": "90 gün",
"logRetentionForever": "Sonsuza kadar",
"logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu",
"actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin",
"accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin",
"licenseRequiredToUse": "Bu özelliği kullanmak için bir kurumsal lisans gereklidir.",
@@ -2052,8 +2178,8 @@
"preferWildcardCert": "Joker Sertifikayı Tercih Et",
"unverified": "Doğrulanmadı",
"domainSetting": "Alan Adı Ayarları",
"domainSettingDescription": "Alan adınız için ayarları yapılandırın",
"preferWildcardCertDescription": "Joker sertifika üretmeye çalışın (doğru yapılandırılmış bir sertifika çözü gereklidir).",
"domainSettingDescription": "Alan adı için ayarları yapılandırın",
"preferWildcardCertDescription": "Joker karakter sertifikası oluşturmayı deneyin (doğru yapılandırılmış bir sertifika çözümleyici gerektirir).",
"recordName": "Kayıt Adı",
"auto": "Otomatik",
"TTL": "TTL",
@@ -2066,9 +2192,9 @@
"olmUpdateAvailableInfo": "Olm'nin güncellenmiş bir sürümü mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
"client": "İstemci",
"proxyProtocol": "Proxy Protokol Ayarları",
"proxyProtocolDescription": "TCP/UDP hizmetleri için istemci IP adreslerini korumak için Proxy Protokolünü yapılandırın.",
"proxyProtocolDescription": "TCP hizmetleri için istemci IP adreslerini korumak amacıyla Proxy Protokolünü yapılandırın.",
"enableProxyProtocol": "Proxy Protokolünü Etkinleştir",
"proxyProtocolInfo": "TCP/UDP arka uçları için istemci IP adreslerini koruyun",
"proxyProtocolInfo": "TCP ara yüzlerini koruyarak istemci IP adreslerini saklayın",
"proxyProtocolVersion": "Proxy Protokol Versiyonu",
"version1": " Versiyon 1 (Önerilen)",
"version2": "Versiyon 2",
@@ -2097,6 +2223,43 @@
"supportMessageSent": "Mesaj Gönderildi!",
"supportWillContact": "En kısa sürede size geri döneceğiz!",
"selectLogRetention": "Kayıt saklama seç",
"terms": "Şartlar",
"privacy": "Gizlilik",
"security": "Güvenlik",
"docs": "Belgeler",
"deviceActivation": "Cihaz aktivasyonu",
"deviceCodeInvalidFormat": "Kod 9 karakter olmalı (ör. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Geçersiz veya süresi dolmuş kod",
"deviceCodeVerifyFailed": "Cihaz kodu doğrulanamadı",
"signedInAs": "Olarak giriş yapıldı",
"deviceCodeEnterPrompt": "Cihazda gösterilen kodu girin",
"continue": "Devam Et",
"deviceUnknownLocation": "Bilinmeyen konum",
"deviceAuthorizationRequested": "Bu yetkilendirme {tarih} tarihinde {konum} konumundan talep edildi. Bu cihaza güvenmenizi sağlayın, çünkü hesap erişimine sahip olacaktır.",
"deviceLabel": "Cihaz: {cihazadı}",
"deviceWantsAccess": "hesabınıza erişmek istiyor",
"deviceExistingAccess": "Mevcut erişim:",
"deviceFullAccess": "Hesabınıza tam erişim",
"deviceOrganizationsAccess": "Hesabınızın erişim hakkına sahip olduğu tüm organizasyonlara erişim",
"deviceAuthorize": "{uygulamaAdi} yetkilendir",
"deviceConnected": "Cihaz Bağlandı!",
"deviceAuthorizedMessage": "Cihazınız, hesabınıza erişim izni almıştır.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "Cihazları Görüntüle",
"viewDevicesDescription": "Bağlantılı cihazlarınızı yönetin",
"noDevices": "Cihaz bulunamadı",
"dateCreated": "Oluşturulma Tarihi",
"unnamedDevice": "Adı Olmayan Cihaz",
"deviceQuestionRemove": "Bu cihazı silmek istediğinizden emin misiniz?",
"deviceMessageRemove": "Bu eylem geri alınamaz.",
"deviceDeleteConfirm": "Cihazı Sil",
"deleteDevice": "Cihazı Sil",
"errorLoadingDevices": "Cihaz yüklenirken hata oluştu",
"failedToLoadDevices": "Cihazlar yüklenemedi",
"deviceDeleted": "Cihaz silindi",
"deviceDeletedDescription": "Cihaz başarıyla silindi.",
"errorDeletingDevice": "Cihaz silinirken hata",
"failedToDeleteDevice": "Cihaz silinirken hata oluştu",
"showColumns": "Sütunları Göster",
"hideColumns": "Sütunları Gizle",
"columnVisibility": "Sütun Görünürlüğü",
@@ -2111,10 +2274,14 @@
"enableSelected": "Seçilenleri Etkinleştir",
"disableSelected": "Seçilenleri Devre Dışı Bırak",
"checkSelectedStatus": "Seçilenlerin Durumunu Kontrol Et",
"clients": "İstemciler",
"accessClientSelect": "Makine istemcilerini seçiniz",
"resourceClientDescription": "Bu kaynağa erişebilecek makine istemcileri",
"regenerate": "Yeniden Üret",
"credentials": "Kimlik Bilgileri",
"savecredentials": "Kimlik Bilgilerini Kaydet",
"regeneratecredentials": "Yeniden Anahtarla",
"regenerateCredentials": "Kimlik bilgilerinizi yeniden oluşturun ve kaydedin",
"regenerateCredentialsButton": "Kimlik Bilgilerini Yeniden Oluştur",
"regenerateCredentials": "Kimlik Bilgilerini Yeniden Oluştur",
"generatedcredentials": "Oluşturulan Kimlik Bilgileri",
"copyandsavethesecredentials": "Bu kimlik bilgilerini kopyalayın ve kaydedin",
"copyandsavethesecredentialsdescription": "Bu sayfadan ayrıldıktan sonra bu kimlik bilgileri tekrar gösterilmeyecek. Onları şimdi güvenli bir şekilde saklayın.",
@@ -2122,13 +2289,12 @@
"credentialsSavedDescription": "Kimlik bilgileri başarılı bir şekilde yeniden oluşturuldu ve kaydedildi.",
"credentialsSaveError": "Kimlik Bilgileri Kayıt Hatası",
"credentialsSaveErrorDescription": "Kimlik bilgilerini yeniden oluştururken ve kaydederken bir hata oluştu.",
"regenerateCredentialsWarning": "Kimlik bilgilerini yeniden oluşturmak önceki bilgileri geçersiz kılacaktır. Bu kimlik bilgilerini kullanan tüm yapılandırmaları güncellediğinizden emin olun.",
"regenerateCredentialsWarning": "Kimlik bilgilerini yeniden oluşturmak, öncekilerini geçersiz kılacak ve bağlantı kesintisine neden olacaktır. Bu kimlik bilgilerini kullanan yapılandırmaları güncellediğinizden emin olun.",
"confirm": "Onayla",
"regenerateCredentialsConfirmation": "Kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?",
"endpoint": "Uç Nokta",
"Id": "Kimlik",
"SecretKey": "Gizli Anahtar",
"featureDisabledTooltip": "Bu özellik yalnızca kurumsal planda mevcuttur ve kullanmak için lisans gerektirir.",
"niceId": "Güzel Kimlik",
"niceIdUpdated": "Güzel Kimlik Güncellendi",
"niceIdUpdatedSuccessfully": "Güzel Kimlik Başarıyla Güncellendi",
@@ -2136,5 +2302,95 @@
"niceIdUpdateErrorDescription": "Güzel Kimlik güncellenirken bir hata oluştu.",
"niceIdCannotBeEmpty": "Güzel Kimlik boş olamaz",
"enterIdentifier": "Tanımlayıcıyı girin",
"identifier": "Tanımlayıcı"
"identifier": "Tanımlayıcı",
"deviceLoginUseDifferentAccount": "Siz değil misiniz? Farklı bir hesap kullanın.",
"deviceLoginDeviceRequestingAccessToAccount": "Bir cihaz bu hesaba erişim talep ediyor.",
"noData": "Veri Yok",
"machineClients": "Makine İstemcileri",
"install": "Yükle",
"run": "Çalıştır",
"clientNameDescription": "Daha sonra değiştirilebilecek istemcinin görünen adı.",
"clientAddress": "İstemci Adresi (Gelişmiş)",
"setupFailedToFetchSubnet": "Varsayılan alt ağ alınamadı",
"setupSubnetAdvanced": "Alt Ağ (Gelişmiş)",
"setupSubnetDescription": "Bu organizasyonun dahili ağı için alt ağ.",
"setupUtilitySubnet": "Yardımcı Alt Ağ (Gelişmiş)",
"setupUtilitySubnetDescription": "Bu kuruluşun alias adresleri ve DNS sunucusu için alt ağ.",
"siteRegenerateAndDisconnect": "Yeniden Oluştur ve Bağlantıyı Kes",
"siteRegenerateAndDisconnectConfirmation": "Kimlik bilgilerini yeniden oluşturmak ve bu sitenin bağlantısını kesmek istediğinizden emin misiniz?",
"siteRegenerateAndDisconnectWarning": "Bu, kimlik bilgilerini yeniden oluşturacak ve sitenin bağlantısını anında kesecektir. Site yeni kimlik bilgilerle yeniden başlatılmalıdır.",
"siteRegenerateCredentialsConfirmation": "Bu site için kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?",
"siteRegenerateCredentialsWarning": "Bu, kimlik bilgilerini yeniden oluşturacak. Site manuel olarak yeniden başlatılana ve yeni kimlik bilgileri kullanılana kadar bağlı kalacak.",
"clientRegenerateAndDisconnect": "Yeniden Oluştur ve Bağlantıyı Kes",
"clientRegenerateAndDisconnectConfirmation": "Kimlik bilgilerini yeniden oluşturmak ve bu istemcinin bağlantısını kesmek istediğinizden emin misiniz?",
"clientRegenerateAndDisconnectWarning": "Bu, kimlik bilgilerini yeniden oluşturacak ve istemcinin bağlantısını hemen kesecek. İstemci, yeni kimlik bilgilerle yeniden başlatılmalıdır.",
"clientRegenerateCredentialsConfirmation": "Bu istemci için kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?",
"clientRegenerateCredentialsWarning": "Bu, kimlik bilgilerini yeniden oluşturacak. İstemci, manuel olarak yeniden başlatılana ve yeni kimlik bilgileri kullanılana kadar bağlı kalacak.",
"remoteExitNodeRegenerateAndDisconnect": "Yeniden Oluştur ve Bağlantıyı Kes",
"remoteExitNodeRegenerateAndDisconnectConfirmation": "Kimlik bilgilerini yeniden oluşturmak ve bu uzak çıkış düğümünün bağlantısını kesmek istediğinizden emin misiniz?",
"remoteExitNodeRegenerateAndDisconnectWarning": "Bu, kimlik bilgilerini yeniden oluşturacak ve hemen uzak çıkış düğümünün bağlantısını kesecek. Uzak çıkış düğümü, yeni kimlik bilgileri ile yeniden başlatılmalıdır.",
"remoteExitNodeRegenerateCredentialsConfirmation": "Bu uzak çıkış düğümü için kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?",
"remoteExitNodeRegenerateCredentialsWarning": "Bu, kimlik bilgilerini yeniden oluşturacak. Uzak çıkış düğümü, manuel olarak yeniden başlatılana ve yeni kimlik bilgiler kullanılana kadar bağlı kalacak.",
"agent": "Aracı",
"personalUseOnly": "Sadece Kişisel Kullanım",
"loginPageLicenseWatermark": "Bu örnek yalnızca kişisel kullanım için lisanslıdır.",
"instanceIsUnlicensed": "Bu örnek lisanssızdır.",
"portRestrictions": "Port Kısıtlamaları",
"allPorts": "Tümü",
"custom": "Özel",
"allPortsAllowed": "Tüm Portlar İzinli",
"allPortsBlocked": "Tüm Portlar Engelli",
"tcpPortsDescription": "Bu kaynak için izin verilen TCP portlarını belirtin. Tüm portlar için '*' kullanın, hepsini engellemek için boş bırakın veya virgülle ayrılmış port ve aralık listesi girin (ör. 80,443,8000-9000).",
"udpPortsDescription": "Bu kaynak için izin verilen UDP portlarını belirtin. Tüm portlar için '*' kullanın, hepsini engellemek için boş bırakın veya virgülle ayrılmış port ve aralık listesi girin (ör. 53,123,500-600).",
"organizationLoginPageTitle": "Kuruluş Giriş Sayfası",
"organizationLoginPageDescription": "Bu kuruluş için giriş sayfasını özelleştirin",
"resourceLoginPageTitle": "Kaynak Giriş Sayfası",
"resourceLoginPageDescription": "Bağımsız kaynaklar için giriş sayfasını özelleştirin",
"enterConfirmation": "Onayı girin",
"blueprintViewDetails": "Detaylar",
"defaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı",
"editInternalResourceDialogNetworkSettings": "Ağ Ayarları",
"editInternalResourceDialogAccessPolicy": "Erişim Politikası",
"editInternalResourceDialogAddRoles": "Roller Ekle",
"editInternalResourceDialogAddUsers": "Kullanıcılar Ekle",
"editInternalResourceDialogAddClients": "Müşteriler Ekle",
"editInternalResourceDialogDestinationLabel": "Hedef",
"editInternalResourceDialogDestinationDescription": "Dahili kaynak için hedef adresi belirtin. Seçilen moda bağlı olarak bu bir ana bilgisayar adı, IP adresi veya CIDR aralığı olabilir. Daha kolay tanımlama için isteğe bağlı olarak dahili bir DNS takma adı ayarlayın.",
"editInternalResourceDialogPortRestrictionsDescription": "Belirtilen TCP/UDP portlarına erişimi kısıtlayın veya tüm portlara izin/engelleme verin.",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
"editInternalResourceDialogIcmp": "ICMP",
"editInternalResourceDialogAccessControl": "Erişim Kontrolü",
"editInternalResourceDialogAccessControlDescription": "Bağlandığında bu kaynağa erişimi olan roller, kullanıcılar ve makine müşterilerini kontrol edin. Yöneticiler her zaman erişime sahiptir.",
"editInternalResourceDialogPortRangeValidationError": "Port aralığı, tüm portlar için \"*\" veya virgülle ayrılmış bir port ve aralık listesi olmalıdır (ör. \"80,443,8000-9000\"). Portlar 1 ile 65535 arasında olmalıdır.",
"orgAuthWhatsThis": "Kuruluş kimliğimi nerede bulabilirim?",
"learnMore": "Daha fazla bilgi",
"backToHome": "Ana sayfaya geri dön",
"needToSignInToOrg": "Kuruluşunuzun kimlik sağlayıcısını kullanmanız mı gerekiyor?",
"maintenanceMode": "Bakım Modu",
"maintenanceModeDescription": "Ziyaretçilere bir bakım sayfası gösterin",
"maintenanceModeType": "Bakım Modu Türü",
"showMaintenancePage": "Ziyaretçilere bir bakım sayfası gösterin",
"enableMaintenanceMode": "Bakım Modunu Etkinleştir",
"automatic": "Otomatik",
"automaticModeDescription": "Tüm arka uç hedefleri kapalı veya sağlıksız olduğunda yalnızca bakım sayfasını gösterin. Sağlıklı en az bir hedef olduğu sürece kaynağınız normal şekilde çalışmaya devam eder.",
"forced": "Zorunlu",
"forcedModeDescription": "Arka plan sağlığına bakılmaksızın her zaman bakım sayfasını gösterin. Tüm erişimi engellemek istediğiniz planlı bakım için bunu kullanın.",
"warning:": "Uyarı:",
"forcedeModeWarning": "Tüm trafik bakım sayfasına yönlendirilecek. Arka plan kaynaklarınız herhangi bir isteği almayacaktır.",
"pageTitle": "Sayfa Başlığı",
"pageTitleDescription": "Bakım sayfasında gösterilen ana başlık",
"maintenancePageMessage": "Bakım Mesajı",
"maintenancePageMessagePlaceholder": "Yakında geri döneceğiz! Sitemiz şu anda planlı bakım altındadır.",
"maintenancePageMessageDescription": "Bakımın detaylarınııklayan mesaj",
"maintenancePageTimeTitle": "Tahmini Tamamlanma Süresi (İsteğe Bağlı)",
"maintenanceTime": "ör. 2 saat, 1 Kasım saat 17:00",
"maintenanceEstimatedTimeDescription": "Bakımın ne zaman tamamlanmasını bekliyorsunuz",
"editDomain": "Alan Adını Düzenle",
"editDomainDescription": "Kaynak için bir alan adı seçin",
"maintenanceModeDisabledTooltip": "Bu özelliği etkinleştirmek için geçerli bir lisans gereklidir.",
"maintenanceScreenTitle": "Servis Geçici Olarak Kullanılamıyor",
"maintenanceScreenMessage": "Şu anda teknik zorluklar yaşıyoruz. Lütfen yakında tekrar kontrol edin.",
"maintenanceScreenEstimatedCompletion": "Tahmini Tamamlama:",
"createInternalResourceDialogDestinationRequired": "Hedef gereklidir"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1022,6 +1022,8 @@
"actionGetSite": "獲取站點",
"actionListSites": "站點列表",
"actionApplyBlueprint": "應用藍圖",
"actionListBlueprints": "藍圖列表",
"actionGetBlueprint": "獲取藍圖",
"setupToken": "設置令牌",
"setupTokenDescription": "從伺服器控制台輸入設定令牌。",
"setupTokenRequired": "需要設置令牌",

View File

@@ -4,6 +4,7 @@ import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
reactStrictMode: false,
eslint: {
ignoreDuringBuilds: true
},

8770
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,9 @@
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
"db:clear-migrations": "rm -rf server/migrations",
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
"set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
"set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
"next:build": "next build",
@@ -29,16 +29,17 @@
"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"
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
"format": "prettier --write ."
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.1.0",
"@aws-sdk/client-s3": "3.922.0",
"@faker-js/faker": "^10.1.0",
"@headlessui/react": "^2.2.9",
"@asteasolutions/zod-to-openapi": "8.2.0",
"@aws-sdk/client-s3": "3.955.0",
"@faker-js/faker": "10.1.0",
"@headlessui/react": "2.2.9",
"@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "^4.7.0",
"@node-rs/argon2": "^2.0.2",
"@monaco-editor/react": "4.7.0",
"@node-rs/argon2": "2.0.2",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.11",
@@ -49,138 +50,132 @@
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.8",
"@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-progress": "1.1.8",
"@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-scroll-area": "1.2.10",
"@radix-ui/react-select": "2.2.6",
"@radix-ui/react-separator": "1.1.8",
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-email/components": "0.5.7",
"@react-email/render": "^1.3.2",
"@react-email/tailwind": "1.2.2",
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.2",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-query": "^5.90.6",
"@radix-ui/react-tooltip": "1.2.8",
"@react-email/components": "1.0.2",
"@react-email/render": "2.0.0",
"@react-email/tailwind": "2.0.2",
"@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2",
"@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.90.12",
"@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0",
"axios": "^1.13.2",
"better-sqlite3": "11.7.0",
"arctic": "3.7.0",
"axios": "1.13.2",
"better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4",
"class-variance-authority": "^0.7.1",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"cookie": "^1.0.2",
"cookie": "1.1.1",
"cookie-parser": "1.4.7",
"cookies": "^0.9.1",
"cookies": "0.9.1",
"cors": "2.8.5",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"crypto-js": "4.2.0",
"d3": "7.9.0",
"date-fns": "4.1.0",
"drizzle-orm": "0.44.7",
"eslint": "9.39.1",
"eslint-config-next": "16.0.3",
"express": "5.1.0",
"drizzle-orm": "0.45.1",
"eslint": "9.39.2",
"eslint-config-next": "16.1.0",
"express": "5.2.1",
"express-rate-limit": "8.2.1",
"glob": "11.1.0",
"glob": "13.0.0",
"helmet": "8.1.0",
"http-errors": "2.0.0",
"i": "^0.3.7",
"http-errors": "2.0.1",
"i": "0.3.7",
"input-otp": "1.4.2",
"ioredis": "5.8.2",
"jmespath": "^0.16.0",
"jmespath": "0.16.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.552.0",
"jsonwebtoken": "9.0.3",
"lucide-react": "0.562.0",
"maxmind": "5.0.1",
"moment": "2.30.1",
"next": "15.5.7",
"next-intl": "^4.4.0",
"next": "15.5.9",
"next-intl": "4.6.1",
"next-themes": "0.4.6",
"nextjs-toploader": "^3.9.17",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
"node-fetch": "3.3.2",
"nodemailer": "7.0.10",
"npm": "^11.6.4",
"nprogress": "^0.2.0",
"nodemailer": "7.0.11",
"npm": "11.7.0",
"nprogress": "0.2.0",
"oslo": "1.2.1",
"pg": "^8.16.2",
"posthog-node": "^5.11.2",
"pg": "8.16.3",
"posthog-node": "5.17.4",
"qrcode.react": "4.2.0",
"react": "19.2.1",
"react-day-picker": "9.11.1",
"react-dom": "19.2.1",
"react-easy-sort": "^1.8.0",
"react-hook-form": "7.66.0",
"react-icons": "^5.5.0",
"react": "19.2.3",
"react-day-picker": "9.13.0",
"react-dom": "19.2.3",
"react-easy-sort": "1.8.0",
"react-hook-form": "7.68.0",
"react-icons": "5.5.0",
"rebuild": "0.1.2",
"recharts": "^2.15.4",
"reodotdev": "^1.0.0",
"resend": "^6.4.2",
"semver": "^7.7.3",
"stripe": "18.2.1",
"swagger-ui-express": "^5.0.1",
"tailwind-merge": "3.3.1",
"topojson-client": "^3.1.0",
"tw-animate-css": "^1.3.8",
"uuid": "^13.0.0",
"recharts": "2.15.4",
"reodotdev": "1.0.0",
"resend": "6.6.0",
"semver": "7.7.3",
"stripe": "20.1.0",
"swagger-ui-express": "5.0.1",
"tailwind-merge": "3.4.0",
"topojson-client": "3.1.0",
"tw-animate-css": "1.4.0",
"uuid": "13.0.0",
"vaul": "1.1.2",
"visionscarto-world-atlas": "^1.0.0",
"winston": "3.18.3",
"visionscarto-world-atlas": "1.0.0",
"winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0",
"ws": "8.18.3",
"yaml": "^2.8.1",
"yaml": "2.8.2",
"yargs": "18.0.0",
"zod": "4.1.12",
"zod": "4.2.1",
"zod-validation-error": "5.0.0"
},
"devDependencies": {
"@dotenvx/dotenvx": "1.51.1",
"@dotenvx/dotenvx": "1.51.2",
"@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/preview-server": "4.3.2",
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query-devtools": "^5.90.2",
"@types/better-sqlite3": "7.6.12",
"@tailwindcss/postcss": "4.1.18",
"@tanstack/react-query-devtools": "5.91.1",
"@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/express": "5.0.5",
"@types/express-session": "^1.18.2",
"@types/jmespath": "^0.15.2",
"@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "24.10.1",
"@types/nodemailer": "7.0.3",
"@types/nprogress": "^0.2.3",
"@types/pg": "8.15.6",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/semver": "^7.7.1",
"@types/swagger-ui-express": "^4.1.8",
"@types/topojson-client": "^3.1.5",
"@types/crypto-js": "4.2.2",
"@types/d3": "7.4.3",
"@types/express": "5.0.6",
"@types/express-session": "1.18.2",
"@types/jmespath": "0.15.2",
"@types/jsonwebtoken": "9.0.10",
"@types/node": "24.10.2",
"@types/nodemailer": "7.0.4",
"@types/nprogress": "0.2.3",
"@types/pg": "8.16.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
"@types/swagger-ui-express": "4.1.8",
"@types/topojson-client": "3.1.5",
"@types/ws": "8.18.1",
"@types/yargs": "17.0.34",
"babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "0.31.6",
"esbuild": "0.27.0",
"esbuild-node-externals": "1.19.1",
"postcss": "^8",
"react-email": "4.3.2",
"tailwindcss": "^4.1.4",
"@types/yargs": "17.0.35",
"@types/js-yaml": "4.0.9",
"babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.8",
"esbuild": "0.27.2",
"esbuild-node-externals": "1.20.1",
"postcss": "8.5.6",
"prettier": "3.7.4",
"react-email": "5.0.7",
"tailwindcss": "4.1.18",
"tsc-alias": "1.8.16",
"tsx": "4.20.6",
"typescript": "^5",
"typescript-eslint": "^8.46.3"
},
"overrides": {
"emblor": {
"react": "19.0.0",
"react-dom": "19.0.0"
}
"tsx": "4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "8.49.0"
}
}

View File

@@ -1,8 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
"@tailwindcss/postcss": {}
}
};
export default config;

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@@ -2,13 +2,13 @@ import { hash, verify } from "@node-rs/argon2";
export async function verifyPassword(
password: string,
hash: string,
hash: string
): Promise<boolean> {
const validPassword = await verify(hash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
parallelism: 1
});
return validPassword;
}
@@ -18,7 +18,7 @@ export async function hashPassword(password: string): Promise<string> {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
parallelism: 1
});
return passwordHash;

View File

@@ -4,10 +4,13 @@ export const passwordSchema = z
.string()
.min(8, { message: "Password must be at least 8 characters long" })
.max(128, { message: "Password must be at most 128 characters long" })
.regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/, {
message: `Your password must meet the following conditions:
.regex(
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/,
{
message: `Your password must meet the following conditions:
at least one uppercase English letter,
at least one lowercase English letter,
at least one digit,
at least one special character.`
});
}
);

View File

@@ -1,6 +1,4 @@
import {
encodeHexLowerCase,
} from "@oslojs/encoding";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { Newt, newts, newtSessions, NewtSession } from "@server/db";
import { db } from "@server/db";
@@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
export async function createNewtSession(
token: string,
newtId: string,
newtId: string
): Promise<NewtSession> {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
sha256(new TextEncoder().encode(token))
);
const session: NewtSession = {
sessionId: sessionId,
newtId,
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
expiresAt: new Date(Date.now() + EXPIRES).getTime()
};
await db.insert(newtSessions).values(session);
return session;
}
export async function validateNewtSessionToken(
token: string,
token: string
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
sha256(new TextEncoder().encode(token))
);
const result = await db
.select({ newt: newts, session: newtSessions })
@@ -45,14 +43,12 @@ export async function validateNewtSessionToken(
.where(eq(newtSessions.sessionId, session.sessionId));
return { session: null, newt: null };
}
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
session.expiresAt = new Date(
Date.now() + EXPIRES,
).getTime();
if (Date.now() >= session.expiresAt - EXPIRES / 2) {
session.expiresAt = new Date(Date.now() + EXPIRES).getTime();
await db
.update(newtSessions)
.set({
expiresAt: session.expiresAt,
expiresAt: session.expiresAt
})
.where(eq(newtSessions.sessionId, session.sessionId));
}

View File

@@ -1,6 +1,4 @@
import {
encodeHexLowerCase,
} from "@oslojs/encoding";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { Olm, olms, olmSessions, OlmSession } from "@server/db";
import { db } from "@server/db";
@@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
export async function createOlmSession(
token: string,
olmId: string,
olmId: string
): Promise<OlmSession> {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
sha256(new TextEncoder().encode(token))
);
const session: OlmSession = {
sessionId: sessionId,
olmId,
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
expiresAt: new Date(Date.now() + EXPIRES).getTime()
};
await db.insert(olmSessions).values(session);
return session;
}
export async function validateOlmSessionToken(
token: string,
token: string
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
sha256(new TextEncoder().encode(token))
);
const result = await db
.select({ olm: olms, session: olmSessions })
@@ -45,14 +43,12 @@ export async function validateOlmSessionToken(
.where(eq(olmSessions.sessionId, session.sessionId));
return { session: null, olm: null };
}
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
session.expiresAt = new Date(
Date.now() + EXPIRES,
).getTime();
if (Date.now() >= session.expiresAt - EXPIRES / 2) {
session.expiresAt = new Date(Date.now() + EXPIRES).getTime();
await db
.update(olmSessions)
.set({
expiresAt: session.expiresAt,
expiresAt: session.expiresAt
})
.where(eq(olmSessions.sessionId, session.sessionId));
}

View File

@@ -1,4 +1,4 @@
import { cleanup as wsCleanup } from "@server/routers/ws";
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
async function cleanup() {
await wsCleanup();
@@ -10,4 +10,4 @@ export async function initCleanup() {
// Handle process termination
process.on("SIGTERM", () => cleanup());
process.on("SIGINT", () => cleanup());
}
}

321
server/db/asns.ts Normal file
View File

@@ -0,0 +1,321 @@
// Curated list of major ASNs (Cloud Providers, CDNs, ISPs, etc.)
// This is not exhaustive - there are 100,000+ ASNs globally
// Users can still enter any ASN manually in the input field
export const MAJOR_ASNS = [
{
name: "ALL ASNs",
code: "ALL",
asn: 0 // Special value that will match all
},
// Major Cloud Providers
{
name: "Google LLC",
code: "AS15169",
asn: 15169
},
{
name: "Amazon AWS",
code: "AS16509",
asn: 16509
},
{
name: "Amazon AWS (EC2)",
code: "AS14618",
asn: 14618
},
{
name: "Microsoft Azure",
code: "AS8075",
asn: 8075
},
{
name: "Microsoft Corporation",
code: "AS8068",
asn: 8068
},
{
name: "DigitalOcean",
code: "AS14061",
asn: 14061
},
{
name: "Linode",
code: "AS63949",
asn: 63949
},
{
name: "Hetzner Online",
code: "AS24940",
asn: 24940
},
{
name: "OVH SAS",
code: "AS16276",
asn: 16276
},
{
name: "Oracle Cloud",
code: "AS31898",
asn: 31898
},
{
name: "Alibaba Cloud",
code: "AS45102",
asn: 45102
},
{
name: "IBM Cloud",
code: "AS36351",
asn: 36351
},
// CDNs
{
name: "Cloudflare",
code: "AS13335",
asn: 13335
},
{
name: "Fastly",
code: "AS54113",
asn: 54113
},
{
name: "Akamai Technologies",
code: "AS20940",
asn: 20940
},
{
name: "Akamai (Primary)",
code: "AS16625",
asn: 16625
},
// Mobile Carriers - US
{
name: "T-Mobile USA",
code: "AS21928",
asn: 21928
},
{
name: "Verizon Wireless",
code: "AS6167",
asn: 6167
},
{
name: "AT&T Mobility",
code: "AS20057",
asn: 20057
},
{
name: "Sprint (T-Mobile)",
code: "AS1239",
asn: 1239
},
{
name: "US Cellular",
code: "AS6430",
asn: 6430
},
// Mobile Carriers - Europe
{
name: "Vodafone UK",
code: "AS25135",
asn: 25135
},
{
name: "EE (UK)",
code: "AS12576",
asn: 12576
},
{
name: "Three UK",
code: "AS29194",
asn: 29194
},
{
name: "O2 UK",
code: "AS13285",
asn: 13285
},
{
name: "Telefonica Spain Mobile",
code: "AS12430",
asn: 12430
},
// Mobile Carriers - Asia
{
name: "NTT DoCoMo (Japan)",
code: "AS9605",
asn: 9605
},
{
name: "SoftBank Mobile (Japan)",
code: "AS17676",
asn: 17676
},
{
name: "SK Telecom (Korea)",
code: "AS9318",
asn: 9318
},
{
name: "KT Corporation Mobile (Korea)",
code: "AS4766",
asn: 4766
},
{
name: "Airtel India",
code: "AS24560",
asn: 24560
},
{
name: "China Mobile",
code: "AS9808",
asn: 9808
},
// Major US ISPs
{
name: "AT&T Services",
code: "AS7018",
asn: 7018
},
{
name: "Comcast Cable",
code: "AS7922",
asn: 7922
},
{
name: "Verizon",
code: "AS701",
asn: 701
},
{
name: "Cox Communications",
code: "AS22773",
asn: 22773
},
{
name: "Charter Communications",
code: "AS20115",
asn: 20115
},
{
name: "CenturyLink",
code: "AS209",
asn: 209
},
// Major European ISPs
{
name: "Deutsche Telekom",
code: "AS3320",
asn: 3320
},
{
name: "Vodafone",
code: "AS1273",
asn: 1273
},
{
name: "British Telecom",
code: "AS2856",
asn: 2856
},
{
name: "Orange",
code: "AS3215",
asn: 3215
},
{
name: "Telefonica",
code: "AS12956",
asn: 12956
},
// Major Asian ISPs
{
name: "China Telecom",
code: "AS4134",
asn: 4134
},
{
name: "China Unicom",
code: "AS4837",
asn: 4837
},
{
name: "NTT Communications",
code: "AS2914",
asn: 2914
},
{
name: "KDDI Corporation",
code: "AS2516",
asn: 2516
},
{
name: "Reliance Jio (India)",
code: "AS55836",
asn: 55836
},
// VPN/Proxy Providers
{
name: "Private Internet Access",
code: "AS46562",
asn: 46562
},
{
name: "NordVPN",
code: "AS202425",
asn: 202425
},
{
name: "Mullvad VPN",
code: "AS213281",
asn: 213281
},
// Social Media / Major Tech
{
name: "Facebook/Meta",
code: "AS32934",
asn: 32934
},
{
name: "Twitter/X",
code: "AS13414",
asn: 13414
},
{
name: "Apple",
code: "AS714",
asn: 714
},
{
name: "Netflix",
code: "AS2906",
asn: 2906
},
// Academic/Research
{
name: "MIT",
code: "AS3",
asn: 3
},
{
name: "Stanford University",
code: "AS32",
asn: 32
},
{
name: "CERN",
code: "AS513",
asn: 513
}
];

File diff suppressed because it is too large Load Diff

13
server/db/maxmindAsn.ts Normal file
View File

@@ -0,0 +1,13 @@
import maxmind, { AsnResponse, Reader } from "maxmind";
import config from "@server/lib/config";
let maxmindAsnLookup: Reader<AsnResponse> | null;
if (config.getRawConfig().server.maxmind_asn_path) {
maxmindAsnLookup = await maxmind.open<AsnResponse>(
config.getRawConfig().server.maxmind_asn_path!
);
} else {
maxmindAsnLookup = null;
}
export { maxmindAsnLookup };

View File

@@ -1708,4 +1708,4 @@
"Desert Box Turtle",
"African Striped Weasel"
]
}
}

View File

@@ -1,6 +1,7 @@
import { join } from "path";
import { readFileSync } from "fs";
import { db, resources, siteResources } from "@server/db";
import { clients, db, resources, siteResources } from "@server/db";
import { randomInt } from "crypto";
import { exitNodes, sites } from "@server/db";
import { eq, and } from "drizzle-orm";
import { __DIRNAME } from "@server/lib/consts";
@@ -15,6 +16,25 @@ if (!dev) {
}
export const names = JSON.parse(readFileSync(file, "utf-8"));
export async function getUniqueClientName(orgId: string): Promise<string> {
let loops = 0;
while (true) {
if (loops > 100) {
throw new Error("Could not generate a unique name");
}
const name = generateName();
const count = await db
.select({ niceId: clients.niceId, orgId: clients.orgId })
.from(clients)
.where(and(eq(clients.niceId, name), eq(clients.orgId, orgId)));
if (count.length === 0) {
return name;
}
loops++;
}
}
export async function getUniqueSiteName(orgId: string): Promise<string> {
let loops = 0;
while (true) {
@@ -46,11 +66,21 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
db
.select({ niceId: resources.niceId, orgId: resources.orgId })
.from(resources)
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
.where(
and(eq(resources.niceId, name), eq(resources.orgId, orgId))
),
db
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
.select({
niceId: siteResources.niceId,
orgId: siteResources.orgId
})
.from(siteResources)
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
.where(
and(
eq(siteResources.niceId, name),
eq(siteResources.orgId, orgId)
)
)
]);
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
return name;
@@ -59,7 +89,9 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
}
}
export async function getUniqueSiteResourceName(orgId: string): Promise<string> {
export async function getUniqueSiteResourceName(
orgId: string
): Promise<string> {
let loops = 0;
while (true) {
if (loops > 100) {
@@ -71,11 +103,21 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
db
.select({ niceId: resources.niceId, orgId: resources.orgId })
.from(resources)
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
.where(
and(eq(resources.niceId, name), eq(resources.orgId, orgId))
),
db
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
.select({
niceId: siteResources.niceId,
orgId: siteResources.orgId
})
.from(siteResources)
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
.where(
and(
eq(siteResources.niceId, name),
eq(siteResources.orgId, orgId)
)
)
]);
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
return name;
@@ -86,9 +128,7 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
export async function getUniqueExitNodeEndpointName(): Promise<string> {
let loops = 0;
const count = await db
.select()
.from(exitNodes);
const count = await db.select().from(exitNodes);
while (true) {
if (loops > 100) {
throw new Error("Could not generate a unique name");
@@ -107,14 +147,11 @@ export async function getUniqueExitNodeEndpointName(): Promise<string> {
}
}
export function generateName(): string {
const name = (
names.descriptors[
Math.floor(Math.random() * names.descriptors.length)
] +
names.descriptors[randomInt(names.descriptors.length)] +
"-" +
names.animals[Math.floor(Math.random() * names.animals.length)]
names.animals[randomInt(names.animals.length)]
)
.toLowerCase()
.replace(/\s/g, "-");

View File

@@ -6,28 +6,28 @@ import { withReplicas } from "drizzle-orm/pg-core";
function createDb() {
const config = readConfigFile();
if (!config.postgres) {
// check the environment variables for postgres config
if (process.env.POSTGRES_CONNECTION_STRING) {
config.postgres = {
connection_string: process.env.POSTGRES_CONNECTION_STRING
};
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
const replicas =
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
","
).map((conn) => ({
// check the environment variables for postgres config first before the config file
if (process.env.POSTGRES_CONNECTION_STRING) {
config.postgres = {
connection_string: process.env.POSTGRES_CONNECTION_STRING
};
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
const replicas =
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map(
(conn) => ({
connection_string: conn.trim()
}));
config.postgres.replicas = replicas;
}
} else {
throw new Error(
"Postgres configuration is missing in the configuration file."
);
})
);
config.postgres.replicas = replicas;
}
}
if (!config.postgres) {
throw new Error(
"Postgres configuration is missing in the configuration file."
);
}
const connectionString = config.postgres?.connection_string;
const replicaConnections = config.postgres?.replicas || [];
@@ -51,7 +51,7 @@ function createDb() {
if (!replicaConnections.length) {
replicas.push(
DrizzlePostgres(primaryPool, {
logger: process.env.NODE_ENV === "development"
logger: process.env.QUERY_LOGGING == "true"
})
);
} else {
@@ -65,7 +65,7 @@ function createDb() {
});
replicas.push(
DrizzlePostgres(replicaPool, {
logger: process.env.NODE_ENV === "development"
logger: process.env.QUERY_LOGGING == "true"
})
);
}
@@ -73,7 +73,7 @@ function createDb() {
return withReplicas(
DrizzlePostgres(primaryPool, {
logger: process.env.QUERY_LOGGING === "true"
logger: process.env.QUERY_LOGGING == "true"
}),
replicas as any
);
@@ -81,6 +81,7 @@ function createDb() {
export const db = createDb();
export default db;
export const primaryDb = db.$primary;
export type Transaction = Parameters<
Parameters<(typeof db)["transaction"]>[0]
>[0];

View File

@@ -10,7 +10,7 @@ const runMigrations = async () => {
await migrate(db as any, {
migrationsFolder: migrationsFolder
});
console.log("Migrations completed successfully.");
console.log("Migrations completed successfully.");
process.exit(0);
} catch (error) {
console.error("Error running migrations:", error);

View File

@@ -204,6 +204,29 @@ export const loginPageOrg = pgTable("loginPageOrg", {
.references(() => orgs.orgId, { onDelete: "cascade" })
});
export const loginPageBranding = pgTable("loginPageBranding", {
loginPageBrandingId: serial("loginPageBrandingId").primaryKey(),
logoUrl: text("logoUrl").notNull(),
logoWidth: integer("logoWidth").notNull(),
logoHeight: integer("logoHeight").notNull(),
primaryColor: text("primaryColor"),
resourceTitle: text("resourceTitle").notNull(),
resourceSubtitle: text("resourceSubtitle"),
orgTitle: text("orgTitle"),
orgSubtitle: text("orgSubtitle")
});
export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", {
loginPageBrandingId: integer("loginPageBrandingId")
.notNull()
.references(() => loginPageBranding.loginPageBrandingId, {
onDelete: "cascade"
}),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
});
export const sessionTransferToken = pgTable("sessionTransferToken", {
token: varchar("token").primaryKey(),
sessionId: varchar("sessionId")
@@ -215,42 +238,56 @@ export const sessionTransferToken = pgTable("sessionTransferToken", {
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const actionAuditLog = pgTable("actionAuditLog", {
id: serial("id").primaryKey(),
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: varchar("actorType", { length: 50 }).notNull(),
actor: varchar("actor", { length: 255 }).notNull(),
actorId: varchar("actorId", { length: 255 }).notNull(),
action: varchar("action", { length: 100 }).notNull(),
metadata: text("metadata")
}, (table) => ([
index("idx_actionAuditLog_timestamp").on(table.timestamp),
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
]));
export const actionAuditLog = pgTable(
"actionAuditLog",
{
id: serial("id").primaryKey(),
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: varchar("actorType", { length: 50 }).notNull(),
actor: varchar("actor", { length: 255 }).notNull(),
actorId: varchar("actorId", { length: 255 }).notNull(),
action: varchar("action", { length: 100 }).notNull(),
metadata: text("metadata")
},
(table) => [
index("idx_actionAuditLog_timestamp").on(table.timestamp),
index("idx_actionAuditLog_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export const accessAuditLog = pgTable("accessAuditLog", {
id: serial("id").primaryKey(),
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: varchar("actorType", { length: 50 }),
actor: varchar("actor", { length: 255 }),
actorId: varchar("actorId", { length: 255 }),
resourceId: integer("resourceId"),
ip: varchar("ip", { length: 45 }),
type: varchar("type", { length: 100 }).notNull(),
action: boolean("action").notNull(),
location: text("location"),
userAgent: text("userAgent"),
metadata: text("metadata")
}, (table) => ([
index("idx_identityAuditLog_timestamp").on(table.timestamp),
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
]));
export const accessAuditLog = pgTable(
"accessAuditLog",
{
id: serial("id").primaryKey(),
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: varchar("actorType", { length: 50 }),
actor: varchar("actor", { length: 255 }),
actorId: varchar("actorId", { length: 255 }),
resourceId: integer("resourceId"),
ip: varchar("ip", { length: 45 }),
type: varchar("type", { length: 100 }).notNull(),
action: boolean("action").notNull(),
location: text("location"),
userAgent: text("userAgent"),
metadata: text("metadata")
},
(table) => [
index("idx_identityAuditLog_timestamp").on(table.timestamp),
index("idx_identityAuditLog_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
@@ -269,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel<
>;
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
export type LoginPage = InferSelectModel<typeof loginPage>;
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;

View File

@@ -7,7 +7,8 @@ import {
bigint,
real,
text,
index
index,
uniqueIndex
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";
import { randomUUID } from "crypto";
@@ -46,13 +47,13 @@ export const orgs = pgTable("orgs", {
requireTwoFactor: boolean("requireTwoFactor"),
maxSessionLengthHours: integer("maxSessionLengthHours"),
passwordExpiryDays: integer("passwordExpiryDays"),
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever, and 9001 = end of the following year
.notNull()
.default(7),
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0)
});
@@ -131,7 +132,15 @@ export const resources = pgTable("resources", {
}),
headers: text("headers"), // comma-separated list of headers to add to the request
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: boolean("maintenanceModeEnabled").notNull().default(false),
maintenanceModeType: text("maintenanceModeType", {
enum: ["forced", "automatic"]
}).default("forced"), // "forced" = always show, "automatic" = only when down
maintenanceTitle: text("maintenanceTitle"),
maintenanceMessage: text("maintenanceMessage"),
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
});
export const targets = pgTable("targets", {
@@ -177,7 +186,7 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
hcMethod: varchar("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName"),
hcTlsServerName: text("hcTlsServerName")
});
export const exitNodes = pgTable("exitNodes", {
@@ -213,7 +222,10 @@ export const siteResources = pgTable("siteResources", {
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
enabled: boolean("enabled").notNull().default(true),
alias: varchar("alias"),
aliasAddress: varchar("aliasAddress")
aliasAddress: varchar("aliasAddress"),
tcpPortRangeString: varchar("tcpPortRangeString"),
udpPortRangeString: varchar("udpPortRangeString"),
disableIcmp: boolean("disableIcmp").notNull().default(false)
});
export const clientSiteResources = pgTable("clientSiteResources", {
@@ -452,6 +464,14 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
headerAuthHash: varchar("headerAuthHash").notNull()
});
export const resourceHeaderAuthExtendedCompatibility = pgTable("resourceHeaderAuthExtendedCompatibility", {
headerAuthExtendedCompatibilityId: serial("headerAuthExtendedCompatibilityId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
extendedCompatibilityIsActivated: boolean("extendedCompatibilityIsActivated").notNull().default(true),
});
export const resourceAccessToken = pgTable("resourceAccessToken", {
accessTokenId: varchar("accessTokenId").primaryKey(),
orgId: varchar("orgId")
@@ -644,6 +664,7 @@ export const clients = pgTable("clients", {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
}),
niceId: varchar("niceId").notNull(),
olmId: text("olmId"), // to lock it to a specific olm optionally
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
@@ -851,6 +872,7 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;

View File

@@ -1,4 +1,6 @@
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
import {
db, loginPage, LoginPage, loginPageOrg, Org, orgs,
} from "@server/db";
import {
Resource,
ResourcePassword,
@@ -14,7 +16,9 @@ import {
sessions,
userOrgs,
userResources,
users
users,
ResourceHeaderAuthExtendedCompatibility,
resourceHeaderAuthExtendedCompatibility
} from "@server/db";
import { and, eq } from "drizzle-orm";
@@ -23,6 +27,7 @@ export type ResourceWithAuth = {
pincode: ResourcePincode | null;
password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null
org: Org;
};
@@ -52,6 +57,10 @@ export async function getResourceByDomain(
resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId)
)
.leftJoin(
resourceHeaderAuthExtendedCompatibility,
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
)
.innerJoin(
orgs,
eq(orgs.orgId, resources.orgId)
@@ -68,6 +77,7 @@ export async function getResourceByDomain(
pincode: result.resourcePincode,
password: result.resourcePassword,
headerAuth: result.resourceHeaderAuth,
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility,
org: result.orgs
};
}

View File

@@ -20,6 +20,7 @@ function createDb() {
export const db = createDb();
export default db;
export const primaryDb = db;
export type Transaction = Parameters<
Parameters<(typeof db)["transaction"]>[0]
>[0];

View File

@@ -8,7 +8,7 @@ const runMigrations = async () => {
console.log("Running migrations...");
try {
migrate(db as any, {
migrationsFolder: migrationsFolder,
migrationsFolder: migrationsFolder
});
console.log("Migrations completed successfully.");
} catch (error) {

View File

@@ -1,13 +1,12 @@
import {
sqliteTable,
integer,
text,
real,
index
} from "drizzle-orm/sqlite-core";
import { InferSelectModel } from "drizzle-orm";
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
import { metadata } from "@app/app/[orgId]/settings/layout";
import {
index,
integer,
real,
sqliteTable,
text
} from "drizzle-orm/sqlite-core";
import { domains, exitNodes, orgs, sessions, users } from "./schema";
export const certificates = sqliteTable("certificates", {
certId: integer("certId").primaryKey({ autoIncrement: true }),
@@ -29,7 +28,9 @@ export const certificates = sqliteTable("certificates", {
});
export const dnsChallenge = sqliteTable("dnsChallenges", {
dnsChallengeId: integer("dnsChallengeId").primaryKey({ autoIncrement: true }),
dnsChallengeId: integer("dnsChallengeId").primaryKey({
autoIncrement: true
}),
domain: text("domain").notNull(),
token: text("token").notNull(),
keyAuthorization: text("keyAuthorization").notNull(),
@@ -61,9 +62,7 @@ export const customers = sqliteTable("customers", {
});
export const subscriptions = sqliteTable("subscriptions", {
subscriptionId: text("subscriptionId")
.primaryKey()
.notNull(),
subscriptionId: text("subscriptionId").primaryKey().notNull(),
customerId: text("customerId")
.notNull()
.references(() => customers.customerId, { onDelete: "cascade" }),
@@ -75,7 +74,9 @@ export const subscriptions = sqliteTable("subscriptions", {
});
export const subscriptionItems = sqliteTable("subscriptionItems", {
subscriptionItemId: integer("subscriptionItemId").primaryKey({ autoIncrement: true }),
subscriptionItemId: integer("subscriptionItemId").primaryKey({
autoIncrement: true
}),
subscriptionId: text("subscriptionId")
.notNull()
.references(() => subscriptions.subscriptionId, {
@@ -129,7 +130,9 @@ export const limits = sqliteTable("limits", {
});
export const usageNotifications = sqliteTable("usageNotifications", {
notificationId: integer("notificationId").primaryKey({ autoIncrement: true }),
notificationId: integer("notificationId").primaryKey({
autoIncrement: true
}),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
@@ -199,6 +202,31 @@ export const loginPageOrg = sqliteTable("loginPageOrg", {
.references(() => orgs.orgId, { onDelete: "cascade" })
});
export const loginPageBranding = sqliteTable("loginPageBranding", {
loginPageBrandingId: integer("loginPageBrandingId").primaryKey({
autoIncrement: true
}),
logoUrl: text("logoUrl").notNull(),
logoWidth: integer("logoWidth").notNull(),
logoHeight: integer("logoHeight").notNull(),
primaryColor: text("primaryColor"),
resourceTitle: text("resourceTitle").notNull(),
resourceSubtitle: text("resourceSubtitle"),
orgTitle: text("orgTitle"),
orgSubtitle: text("orgSubtitle")
});
export const loginPageBrandingOrg = sqliteTable("loginPageBrandingOrg", {
loginPageBrandingId: integer("loginPageBrandingId")
.notNull()
.references(() => loginPageBranding.loginPageBrandingId, {
onDelete: "cascade"
}),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
});
export const sessionTransferToken = sqliteTable("sessionTransferToken", {
token: text("token").primaryKey(),
sessionId: text("sessionId")
@@ -210,42 +238,56 @@ export const sessionTransferToken = sqliteTable("sessionTransferToken", {
expiresAt: integer("expiresAt").notNull()
});
export const actionAuditLog = sqliteTable("actionAuditLog", {
id: integer("id").primaryKey({ autoIncrement: true }),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: text("actorType").notNull(),
actor: text("actor").notNull(),
actorId: text("actorId").notNull(),
action: text("action").notNull(),
metadata: text("metadata")
}, (table) => ([
index("idx_actionAuditLog_timestamp").on(table.timestamp),
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
]));
export const actionAuditLog = sqliteTable(
"actionAuditLog",
{
id: integer("id").primaryKey({ autoIncrement: true }),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: text("actorType").notNull(),
actor: text("actor").notNull(),
actorId: text("actorId").notNull(),
action: text("action").notNull(),
metadata: text("metadata")
},
(table) => [
index("idx_actionAuditLog_timestamp").on(table.timestamp),
index("idx_actionAuditLog_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export const accessAuditLog = sqliteTable("accessAuditLog", {
id: integer("id").primaryKey({ autoIncrement: true }),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: text("actorType"),
actor: text("actor"),
actorId: text("actorId"),
resourceId: integer("resourceId"),
ip: text("ip"),
location: text("location"),
type: text("type").notNull(),
action: integer("action", { mode: "boolean" }).notNull(),
userAgent: text("userAgent"),
metadata: text("metadata")
}, (table) => ([
index("idx_identityAuditLog_timestamp").on(table.timestamp),
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
]));
export const accessAuditLog = sqliteTable(
"accessAuditLog",
{
id: integer("id").primaryKey({ autoIncrement: true }),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
actorType: text("actorType"),
actor: text("actor"),
actorId: text("actorId"),
resourceId: integer("resourceId"),
ip: text("ip"),
location: text("location"),
type: text("type").notNull(),
action: integer("action", { mode: "boolean" }).notNull(),
userAgent: text("userAgent"),
metadata: text("metadata")
},
(table) => [
index("idx_identityAuditLog_timestamp").on(table.timestamp),
index("idx_identityAuditLog_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
@@ -264,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel<
>;
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
export type LoginPage = InferSelectModel<typeof loginPage>;
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;

View File

@@ -1,27 +1,33 @@
import { randomUUID } from "crypto";
import { InferSelectModel } from "drizzle-orm";
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import {
sqliteTable,
text,
integer,
index,
uniqueIndex
} from "drizzle-orm/sqlite-core";
import { no } from "zod/v4/locales";
export const domains = sqliteTable("domains", {
domainId: text("domainId").primaryKey(),
baseDomain: text("baseDomain").notNull(),
configManaged: integer("configManaged", { mode: "boolean" })
configManaged: integer("configManaged", {mode: "boolean"})
.notNull()
.default(false),
type: text("type"), // "ns", "cname", "wildcard"
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
verified: integer("verified", {mode: "boolean"}).notNull().default(false),
failed: integer("failed", {mode: "boolean"}).notNull().default(false),
tries: integer("tries").notNull().default(0),
certResolver: text("certResolver"),
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
preferWildcardCert: integer("preferWildcardCert", {mode: "boolean"})
});
export const dnsRecords = sqliteTable("dnsRecords", {
id: integer("id").primaryKey({ autoIncrement: true }),
id: integer("id").primaryKey({autoIncrement: true}),
domainId: text("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" }),
.references(() => domains.domainId, {onDelete: "cascade"}),
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
baseDomain: text("baseDomain"),
@@ -35,16 +41,16 @@ export const orgs = sqliteTable("orgs", {
subnet: text("subnet"),
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
createdAt: text("createdAt"),
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
requireTwoFactor: integer("requireTwoFactor", {mode: "boolean"}),
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
passwordExpiryDays: integer("passwordExpiryDays"), // days
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(7),
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0)
});
@@ -52,23 +58,23 @@ export const orgs = sqliteTable("orgs", {
export const userDomains = sqliteTable("userDomains", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
domainId: text("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" })
.references(() => domains.domainId, {onDelete: "cascade"})
});
export const orgDomains = sqliteTable("orgDomains", {
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
.references(() => orgs.orgId, {onDelete: "cascade"}),
domainId: text("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" })
.references(() => domains.domainId, {onDelete: "cascade"})
});
export const sites = sqliteTable("sites", {
siteId: integer("siteId").primaryKey({ autoIncrement: true }),
siteId: integer("siteId").primaryKey({autoIncrement: true}),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
@@ -85,7 +91,7 @@ export const sites = sqliteTable("sites", {
megabytesOut: integer("bytesOut").default(0),
lastBandwidthUpdate: text("lastBandwidthUpdate"),
type: text("type").notNull(), // "newt" or "wireguard"
online: integer("online", { mode: "boolean" }).notNull().default(false),
online: integer("online", {mode: "boolean"}).notNull().default(false),
// exit node stuff that is how to connect to the site when it has a wg server
address: text("address"), // this is the address of the wireguard interface in newt
@@ -93,14 +99,14 @@ export const sites = sqliteTable("sites", {
publicKey: text("publicKey"), // TODO: Fix typo in publicKey
lastHolePunch: integer("lastHolePunch"),
listenPort: integer("listenPort"),
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
dockerSocketEnabled: integer("dockerSocketEnabled", {mode: "boolean"})
.notNull()
.default(true)
});
export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
resourceGuid: text("resourceGuid", { length: 36 })
resourceId: integer("resourceId").primaryKey({autoIncrement: true}),
resourceGuid: text("resourceGuid", {length: 36})
.unique()
.notNull()
.$defaultFn(() => randomUUID()),
@@ -116,39 +122,48 @@ export const resources = sqliteTable("resources", {
domainId: text("domainId").references(() => domains.domainId, {
onDelete: "set null"
}),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
blockAccess: integer("blockAccess", { mode: "boolean" })
ssl: integer("ssl", {mode: "boolean"}).notNull().default(false),
blockAccess: integer("blockAccess", {mode: "boolean"})
.notNull()
.default(false),
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
http: integer("http", { mode: "boolean" }).notNull().default(true),
sso: integer("sso", {mode: "boolean"}).notNull().default(true),
http: integer("http", {mode: "boolean"}).notNull().default(true),
protocol: text("protocol").notNull(),
proxyPort: integer("proxyPort"),
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
emailWhitelistEnabled: integer("emailWhitelistEnabled", {mode: "boolean"})
.notNull()
.default(false),
applyRules: integer("applyRules", { mode: "boolean" })
applyRules: integer("applyRules", {mode: "boolean"})
.notNull()
.default(false),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
stickySession: integer("stickySession", { mode: "boolean" })
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
stickySession: integer("stickySession", {mode: "boolean"})
.notNull()
.default(false),
tlsServerName: text("tlsServerName"),
setHostHeader: text("setHostHeader"),
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
enableProxy: integer("enableProxy", {mode: "boolean"}).default(true),
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
onDelete: "set null"
}),
headers: text("headers"), // comma-separated list of headers to add to the request
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: integer("maintenanceModeEnabled", { mode: "boolean" })
.notNull()
.default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
maintenanceModeType: text("maintenanceModeType", {
enum: ["forced", "automatic"]
}).default("forced"), // "forced" = always show, "automatic" = only when down
maintenanceTitle: text("maintenanceTitle"),
maintenanceMessage: text("maintenanceMessage"),
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
});
export const targets = sqliteTable("targets", {
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
targetId: integer("targetId").primaryKey({autoIncrement: true}),
resourceId: integer("resourceId")
.references(() => resources.resourceId, {
onDelete: "cascade"
@@ -163,7 +178,7 @@ export const targets = sqliteTable("targets", {
method: text("method"),
port: integer("port").notNull(),
internalPort: integer("internalPort"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
path: text("path"),
pathMatchType: text("pathMatchType"), // exact, prefix, regex
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
@@ -177,8 +192,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
}),
targetId: integer("targetId")
.notNull()
.references(() => targets.targetId, { onDelete: "cascade" }),
hcEnabled: integer("hcEnabled", { mode: "boolean" })
.references(() => targets.targetId, {onDelete: "cascade"}),
hcEnabled: integer("hcEnabled", {mode: "boolean"})
.notNull()
.default(false),
hcPath: text("hcPath"),
@@ -196,11 +211,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
hcMethod: text("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName"),
hcTlsServerName: text("hcTlsServerName")
});
export const exitNodes = sqliteTable("exitNodes", {
exitNodeId: integer("exitNodeId").primaryKey({ autoIncrement: true }),
exitNodeId: integer("exitNodeId").primaryKey({autoIncrement: true}),
name: text("name").notNull(),
address: text("address").notNull(), // this is the address of the wireguard interface in gerbil
endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config
@@ -208,7 +223,7 @@ export const exitNodes = sqliteTable("exitNodes", {
listenPort: integer("listenPort").notNull(),
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
maxConnections: integer("maxConnections"),
online: integer("online", { mode: "boolean" }).notNull().default(false),
online: integer("online", {mode: "boolean"}).notNull().default(false),
lastPing: integer("lastPing"),
type: text("type").default("gerbil"), // gerbil, remoteExitNode
region: text("region")
@@ -221,10 +236,10 @@ export const siteResources = sqliteTable("siteResources", {
}),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
.references(() => sites.siteId, {onDelete: "cascade"}),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
.references(() => orgs.orgId, {onDelete: "cascade"}),
niceId: text("niceId").notNull(),
name: text("name").notNull(),
mode: text("mode").notNull(), // "host" | "cidr" | "port"
@@ -234,7 +249,10 @@ export const siteResources = sqliteTable("siteResources", {
destination: text("destination").notNull(), // ip, cidr, hostname
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
alias: text("alias"),
aliasAddress: text("aliasAddress")
aliasAddress: text("aliasAddress"),
tcpPortRangeString: text("tcpPortRangeString"),
udpPortRangeString: text("udpPortRangeString"),
disableIcmp: integer("disableIcmp", { mode: "boolean" })
});
export const clientSiteResources = sqliteTable("clientSiteResources", {
@@ -274,20 +292,20 @@ export const users = sqliteTable("user", {
onDelete: "cascade"
}),
passwordHash: text("passwordHash"),
twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" })
twoFactorEnabled: integer("twoFactorEnabled", {mode: "boolean"})
.notNull()
.default(false),
twoFactorSetupRequested: integer("twoFactorSetupRequested", {
mode: "boolean"
}).default(false),
twoFactorSecret: text("twoFactorSecret"),
emailVerified: integer("emailVerified", { mode: "boolean" })
emailVerified: integer("emailVerified", {mode: "boolean"})
.notNull()
.default(false),
dateCreated: text("dateCreated").notNull(),
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
termsVersion: text("termsVersion"),
serverAdmin: integer("serverAdmin", { mode: "boolean" })
serverAdmin: integer("serverAdmin", {mode: "boolean"})
.notNull()
.default(false),
lastPasswordChange: integer("lastPasswordChange")
@@ -321,7 +339,7 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", {
export const setupTokens = sqliteTable("setupTokens", {
tokenId: text("tokenId").primaryKey(),
token: text("token").notNull(),
used: integer("used", { mode: "boolean" }).notNull().default(false),
used: integer("used", {mode: "boolean"}).notNull().default(false),
dateCreated: text("dateCreated").notNull(),
dateUsed: text("dateUsed")
});
@@ -350,7 +368,7 @@ export const clients = sqliteTable("clients", {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
}),
niceId: text("niceId").notNull(),
name: text("name").notNull(),
pubKey: text("pubKey"),
olmId: text("olmId"), // to lock it to a specific olm optionally
@@ -360,7 +378,7 @@ export const clients = sqliteTable("clients", {
lastBandwidthUpdate: text("lastBandwidthUpdate"),
lastPing: integer("lastPing"),
type: text("type").notNull(), // "olm"
online: integer("online", { mode: "boolean" }).notNull().default(false),
online: integer("online", {mode: "boolean"}).notNull().default(false),
// endpoint: text("endpoint"),
lastHolePunch: integer("lastHolePunch")
});
@@ -406,10 +424,10 @@ export const olms = sqliteTable("olms", {
});
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
codeId: integer("id").primaryKey({ autoIncrement: true }),
codeId: integer("id").primaryKey({autoIncrement: true}),
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
codeHash: text("codeHash").notNull()
});
@@ -417,7 +435,7 @@ export const sessions = sqliteTable("session", {
sessionId: text("id").primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull(),
issuedAt: integer("issuedAt"),
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
@@ -429,7 +447,7 @@ export const newtSessions = sqliteTable("newtSession", {
sessionId: text("id").primaryKey(),
newtId: text("newtId")
.notNull()
.references(() => newts.newtId, { onDelete: "cascade" }),
.references(() => newts.newtId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull()
});
@@ -437,14 +455,14 @@ export const olmSessions = sqliteTable("clientSession", {
sessionId: text("id").primaryKey(),
olmId: text("olmId")
.notNull()
.references(() => olms.olmId, { onDelete: "cascade" }),
.references(() => olms.olmId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull()
});
export const userOrgs = sqliteTable("userOrgs", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
@@ -453,28 +471,28 @@ export const userOrgs = sqliteTable("userOrgs", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId),
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
isOwner: integer("isOwner", {mode: "boolean"}).notNull().default(false),
autoProvisioned: integer("autoProvisioned", {
mode: "boolean"
}).default(false)
});
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
codeId: integer("id").primaryKey({ autoIncrement: true }),
codeId: integer("id").primaryKey({autoIncrement: true}),
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
email: text("email").notNull(),
code: text("code").notNull(),
expiresAt: integer("expiresAt").notNull()
});
export const passwordResetTokens = sqliteTable("passwordResetTokens", {
tokenId: integer("id").primaryKey({ autoIncrement: true }),
tokenId: integer("id").primaryKey({autoIncrement: true}),
email: text("email").notNull(),
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
tokenHash: text("tokenHash").notNull(),
expiresAt: integer("expiresAt").notNull()
});
@@ -486,13 +504,13 @@ export const actions = sqliteTable("actions", {
});
export const roles = sqliteTable("roles", {
roleId: integer("roleId").primaryKey({ autoIncrement: true }),
roleId: integer("roleId").primaryKey({autoIncrement: true}),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
isAdmin: integer("isAdmin", { mode: "boolean" }),
isAdmin: integer("isAdmin", {mode: "boolean"}),
name: text("name").notNull(),
description: text("description")
});
@@ -500,92 +518,92 @@ export const roles = sqliteTable("roles", {
export const roleActions = sqliteTable("roleActions", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
.references(() => roles.roleId, {onDelete: "cascade"}),
actionId: text("actionId")
.notNull()
.references(() => actions.actionId, { onDelete: "cascade" }),
.references(() => actions.actionId, {onDelete: "cascade"}),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
.references(() => orgs.orgId, {onDelete: "cascade"})
});
export const userActions = sqliteTable("userActions", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
actionId: text("actionId")
.notNull()
.references(() => actions.actionId, { onDelete: "cascade" }),
.references(() => actions.actionId, {onDelete: "cascade"}),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
.references(() => orgs.orgId, {onDelete: "cascade"})
});
export const roleSites = sqliteTable("roleSites", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
.references(() => roles.roleId, {onDelete: "cascade"}),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" })
.references(() => sites.siteId, {onDelete: "cascade"})
});
export const userSites = sqliteTable("userSites", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" })
.references(() => sites.siteId, {onDelete: "cascade"})
});
export const userClients = sqliteTable("userClients", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" })
.references(() => clients.clientId, {onDelete: "cascade"})
});
export const roleClients = sqliteTable("roleClients", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
.references(() => roles.roleId, {onDelete: "cascade"}),
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" })
.references(() => clients.clientId, {onDelete: "cascade"})
});
export const roleResources = sqliteTable("roleResources", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
.references(() => roles.roleId, {onDelete: "cascade"}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
.references(() => resources.resourceId, {onDelete: "cascade"})
});
export const userResources = sqliteTable("userResources", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
.references(() => users.userId, {onDelete: "cascade"}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
.references(() => resources.resourceId, {onDelete: "cascade"})
});
export const userInvites = sqliteTable("userInvites", {
inviteId: text("inviteId").primaryKey(),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
.references(() => orgs.orgId, {onDelete: "cascade"}),
email: text("email").notNull(),
expiresAt: integer("expiresAt").notNull(),
tokenHash: text("token").notNull(),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
.references(() => roles.roleId, {onDelete: "cascade"})
});
export const resourcePincode = sqliteTable("resourcePincode", {
@@ -594,7 +612,7 @@ export const resourcePincode = sqliteTable("resourcePincode", {
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
.references(() => resources.resourceId, {onDelete: "cascade"}),
pincodeHash: text("pincodeHash").notNull(),
digitLength: integer("digitLength").notNull()
});
@@ -605,7 +623,7 @@ export const resourcePassword = sqliteTable("resourcePassword", {
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
.references(() => resources.resourceId, {onDelete: "cascade"}),
passwordHash: text("passwordHash").notNull()
});
@@ -615,18 +633,28 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
.references(() => resources.resourceId, {onDelete: "cascade"}),
headerAuthHash: text("headerAuthHash").notNull()
});
export const resourceHeaderAuthExtendedCompatibility = sqliteTable("resourceHeaderAuthExtendedCompatibility", {
headerAuthExtendedCompatibilityId: integer("headerAuthExtendedCompatibilityId").primaryKey({
autoIncrement: true
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, {onDelete: "cascade"}),
extendedCompatibilityIsActivated: integer("extendedCompatibilityIsActivated", {mode: "boolean"}).notNull().default(true)
});
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
accessTokenId: text("accessTokenId").primaryKey(),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
.references(() => orgs.orgId, {onDelete: "cascade"}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
.references(() => resources.resourceId, {onDelete: "cascade"}),
tokenHash: text("tokenHash").notNull(),
sessionLength: integer("sessionLength").notNull(),
expiresAt: integer("expiresAt"),
@@ -639,13 +667,13 @@ export const resourceSessions = sqliteTable("resourceSessions", {
sessionId: text("id").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
.references(() => resources.resourceId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull(),
sessionLength: integer("sessionLength").notNull(),
doNotExtend: integer("doNotExtend", { mode: "boolean" })
doNotExtend: integer("doNotExtend", {mode: "boolean"})
.notNull()
.default(false),
isRequestToken: integer("isRequestToken", { mode: "boolean" }),
isRequestToken: integer("isRequestToken", {mode: "boolean"}),
userSessionId: text("userSessionId").references(() => sessions.sessionId, {
onDelete: "cascade"
}),
@@ -677,11 +705,11 @@ export const resourceSessions = sqliteTable("resourceSessions", {
});
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
whitelistId: integer("id").primaryKey({ autoIncrement: true }),
whitelistId: integer("id").primaryKey({autoIncrement: true}),
email: text("email").notNull(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
.references(() => resources.resourceId, {onDelete: "cascade"})
});
export const resourceOtp = sqliteTable("resourceOtp", {
@@ -690,7 +718,7 @@ export const resourceOtp = sqliteTable("resourceOtp", {
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
.references(() => resources.resourceId, {onDelete: "cascade"}),
email: text("email").notNull(),
otpHash: text("otpHash").notNull(),
expiresAt: integer("expiresAt").notNull()
@@ -702,11 +730,11 @@ export const versionMigrations = sqliteTable("versionMigrations", {
});
export const resourceRules = sqliteTable("resourceRules", {
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
ruleId: integer("ruleId").primaryKey({autoIncrement: true}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
.references(() => resources.resourceId, {onDelete: "cascade"}),
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
priority: integer("priority").notNull(),
action: text("action").notNull(), // ACCEPT, DROP, PASS
match: text("match").notNull(), // CIDR, PATH, IP
@@ -714,17 +742,17 @@ export const resourceRules = sqliteTable("resourceRules", {
});
export const supporterKey = sqliteTable("supporterKey", {
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
keyId: integer("keyId").primaryKey({autoIncrement: true}),
key: text("key").notNull(),
githubUsername: text("githubUsername").notNull(),
phrase: text("phrase"),
tier: text("tier"),
valid: integer("valid", { mode: "boolean" }).notNull().default(false)
valid: integer("valid", {mode: "boolean"}).notNull().default(false)
});
// Identity Providers
export const idp = sqliteTable("idp", {
idpId: integer("idpId").primaryKey({ autoIncrement: true }),
idpId: integer("idpId").primaryKey({autoIncrement: true}),
name: text("name").notNull(),
type: text("type").notNull(),
defaultRoleMapping: text("defaultRoleMapping"),
@@ -744,7 +772,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
variant: text("variant").notNull().default("oidc"),
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
.references(() => idp.idpId, {onDelete: "cascade"}),
clientId: text("clientId").notNull(),
clientSecret: text("clientSecret").notNull(),
authUrl: text("authUrl").notNull(),
@@ -772,22 +800,22 @@ export const apiKeys = sqliteTable("apiKeys", {
apiKeyHash: text("apiKeyHash").notNull(),
lastChars: text("lastChars").notNull(),
createdAt: text("dateCreated").notNull(),
isRoot: integer("isRoot", { mode: "boolean" }).notNull().default(false)
isRoot: integer("isRoot", {mode: "boolean"}).notNull().default(false)
});
export const apiKeyActions = sqliteTable("apiKeyActions", {
apiKeyId: text("apiKeyId")
.notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
.references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
actionId: text("actionId")
.notNull()
.references(() => actions.actionId, { onDelete: "cascade" })
.references(() => actions.actionId, {onDelete: "cascade"})
});
export const apiKeyOrg = sqliteTable("apiKeyOrg", {
apiKeyId: text("apiKeyId")
.notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
.references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
@@ -798,10 +826,10 @@ export const apiKeyOrg = sqliteTable("apiKeyOrg", {
export const idpOrg = sqliteTable("idpOrg", {
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
.references(() => idp.idpId, {onDelete: "cascade"}),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
.references(() => orgs.orgId, {onDelete: "cascade"}),
roleMapping: text("roleMapping"),
orgMapping: text("orgMapping")
});
@@ -819,19 +847,19 @@ export const blueprints = sqliteTable("blueprints", {
name: text("name").notNull(),
source: text("source").notNull(),
createdAt: integer("createdAt").notNull(),
succeeded: integer("succeeded", { mode: "boolean" }).notNull(),
succeeded: integer("succeeded", {mode: "boolean"}).notNull(),
contents: text("contents").notNull(),
message: text("message")
});
export const requestAuditLog = sqliteTable(
"requestAuditLog",
{
id: integer("id").primaryKey({ autoIncrement: true }),
id: integer("id").primaryKey({autoIncrement: true}),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: text("orgId").references(() => orgs.orgId, {
onDelete: "cascade"
}),
action: integer("action", { mode: "boolean" }).notNull(),
action: integer("action", {mode: "boolean"}).notNull(),
reason: integer("reason").notNull(),
actorType: text("actorType"),
actor: text("actor"),
@@ -848,7 +876,7 @@ export const requestAuditLog = sqliteTable(
host: text("host"),
path: text("path"),
method: text("method"),
tls: integer("tls", { mode: "boolean" })
tls: integer("tls", {mode: "boolean"})
},
(table) => [
index("idx_requestAuditLog_timestamp").on(table.timestamp),
@@ -904,6 +932,7 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;

View File

@@ -18,10 +18,13 @@ function createEmailClient() {
host: emailConfig.smtp_host,
port: emailConfig.smtp_port,
secure: emailConfig.smtp_secure || false,
auth: (emailConfig.smtp_user && emailConfig.smtp_pass) ? {
user: emailConfig.smtp_user,
pass: emailConfig.smtp_pass
} : null
auth:
emailConfig.smtp_user && emailConfig.smtp_pass
? {
user: emailConfig.smtp_user,
pass: emailConfig.smtp_pass
}
: null
} as SMTPTransport.Options;
if (emailConfig.smtp_tls_reject_unauthorized !== undefined) {

View File

@@ -10,6 +10,7 @@ export async function sendEmail(
from: string | undefined;
to: string | undefined;
subject: string;
replyTo?: string;
}
) {
if (!emailClient) {
@@ -32,6 +33,7 @@ export async function sendEmail(
address: opts.from
},
to: opts.to,
replyTo: opts.replyTo,
subject: opts.subject,
html: emailHtml
});

View File

@@ -19,7 +19,13 @@ interface Props {
billingLink: string; // Link to billing page
}
export const NotifyUsageLimitApproaching = ({ email, limitName, currentUsage, usageLimit, billingLink }: Props) => {
export const NotifyUsageLimitApproaching = ({
email,
limitName,
currentUsage,
usageLimit,
billingLink
}: Props) => {
const previewText = `Your usage for ${limitName} is approaching the limit.`;
const usagePercentage = Math.round((currentUsage / usageLimit) * 100);
@@ -37,23 +43,32 @@ export const NotifyUsageLimitApproaching = ({ email, limitName, currentUsage, us
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
We wanted to let you know that your usage for <strong>{limitName}</strong> is approaching your plan limit.
We wanted to let you know that your usage for{" "}
<strong>{limitName}</strong> is approaching your
plan limit.
</EmailText>
<EmailText>
<strong>Current Usage:</strong> {currentUsage} of {usageLimit} ({usagePercentage}%)
<strong>Current Usage:</strong> {currentUsage} of{" "}
{usageLimit} ({usagePercentage}%)
</EmailText>
<EmailText>
Once you reach your limit, some functionality may be restricted or your sites may disconnect until you upgrade your plan or your usage resets.
Once you reach your limit, some functionality may be
restricted or your sites may disconnect until you
upgrade your plan or your usage resets.
</EmailText>
<EmailText>
To avoid any interruption to your service, we recommend upgrading your plan or monitoring your usage closely. You can <a href={billingLink}>upgrade your plan here</a>.
To avoid any interruption to your service, we
recommend upgrading your plan or monitoring your
usage closely. You can{" "}
<a href={billingLink}>upgrade your plan here</a>.
</EmailText>
<EmailText>
If you have any questions or need assistance, please don't hesitate to reach out to our support team.
If you have any questions or need assistance, please
don't hesitate to reach out to our support team.
</EmailText>
<EmailFooter>

View File

@@ -19,7 +19,13 @@ interface Props {
billingLink: string; // Link to billing page
}
export const NotifyUsageLimitReached = ({ email, limitName, currentUsage, usageLimit, billingLink }: Props) => {
export const NotifyUsageLimitReached = ({
email,
limitName,
currentUsage,
usageLimit,
billingLink
}: Props) => {
const previewText = `You've reached your ${limitName} usage limit - Action required`;
const usagePercentage = Math.round((currentUsage / usageLimit) * 100);
@@ -32,30 +38,48 @@ export const NotifyUsageLimitReached = ({ email, limitName, currentUsage, usageL
<EmailContainer>
<EmailLetterHead />
<EmailHeading>Usage Limit Reached - Action Required</EmailHeading>
<EmailHeading>
Usage Limit Reached - Action Required
</EmailHeading>
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
You have reached your usage limit for <strong>{limitName}</strong>.
You have reached your usage limit for{" "}
<strong>{limitName}</strong>.
</EmailText>
<EmailText>
<strong>Current Usage:</strong> {currentUsage} of {usageLimit} ({usagePercentage}%)
<strong>Current Usage:</strong> {currentUsage} of{" "}
{usageLimit} ({usagePercentage}%)
</EmailText>
<EmailText>
<strong>Important:</strong> Your functionality may now be restricted and your sites may disconnect until you either upgrade your plan or your usage resets. To prevent any service interruption, immediate action is recommended.
<strong>Important:</strong> Your functionality may
now be restricted and your sites may disconnect
until you either upgrade your plan or your usage
resets. To prevent any service interruption,
immediate action is recommended.
</EmailText>
<EmailText>
<strong>What you can do:</strong>
<br /> <a href={billingLink} style={{ color: '#2563eb', fontWeight: 'bold' }}>Upgrade your plan immediately</a> to restore full functionality
<br /> Monitor your usage to stay within limits in the future
<br />{" "}
<a
href={billingLink}
style={{ color: "#2563eb", fontWeight: "bold" }}
>
Upgrade your plan immediately
</a>{" "}
to restore full functionality
<br /> Monitor your usage to stay within limits in
the future
</EmailText>
<EmailText>
If you have any questions or need immediate assistance, please contact our support team right away.
If you have any questions or need immediate
assistance, please contact our support team right
away.
</EmailText>
<EmailFooter>

View File

@@ -5,7 +5,7 @@ import config from "@server/lib/config";
import logger from "@server/logger";
import {
errorHandlerMiddleware,
notFoundMiddleware,
notFoundMiddleware
} from "@server/middlewares";
import { authenticated, unauthenticated } from "#dynamic/routers/integration";
import { logIncomingMiddleware } from "./middlewares/logIncoming";

29
server/lib/asn.ts Normal file
View File

@@ -0,0 +1,29 @@
import logger from "@server/logger";
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
export async function getAsnForIp(ip: string): Promise<number | undefined> {
try {
if (!maxmindAsnLookup) {
logger.debug(
"MaxMind ASN DB path not configured, cannot perform ASN lookup"
);
return;
}
const result = maxmindAsnLookup.get(ip);
if (!result || !result.autonomous_system_number) {
return;
}
logger.debug(
`ASN lookup successful for IP ${ip}: AS${result.autonomous_system_number}`
);
return result.autonomous_system_number;
} catch (error) {
logger.error("Error performing ASN lookup:", error);
}
return;
}

View File

@@ -25,16 +25,22 @@ export const FeatureMeterIdsSandbox: Record<FeatureId, string> = {
};
export function getFeatureMeterId(featureId: FeatureId): string {
if (process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
) {
return FeatureMeterIds[featureId];
} else {
return FeatureMeterIdsSandbox[featureId];
}
}
export function getFeatureIdByMetricId(metricId: string): FeatureId | undefined {
return (Object.entries(FeatureMeterIds) as [FeatureId, string][])
.find(([_, v]) => v === metricId)?.[0];
export function getFeatureIdByMetricId(
metricId: string
): FeatureId | undefined {
return (Object.entries(FeatureMeterIds) as [FeatureId, string][]).find(
([_, v]) => v === metricId
)?.[0];
}
export type FeaturePriceSet = {
@@ -43,7 +49,8 @@ export type FeaturePriceSet = {
[FeatureId.DOMAINS]?: string; // Optional since domains are not billed
};
export const standardFeaturePriceSet: FeaturePriceSet = { // Free tier matches the freeLimitSet
export const standardFeaturePriceSet: FeaturePriceSet = {
// Free tier matches the freeLimitSet
[FeatureId.SITE_UPTIME]: "price_1RrQc4D3Ee2Ir7WmaJGZ3MtF",
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea",
[FeatureId.EGRESS_DATA_MB]: "price_1RrQXFD3Ee2Ir7WmvGDlgxQk",
@@ -51,7 +58,8 @@ export const standardFeaturePriceSet: FeaturePriceSet = { // Free tier matches t
[FeatureId.REMOTE_EXIT_NODES]: "price_1S46weD3Ee2Ir7Wm94KEHI4h"
};
export const standardFeaturePriceSetSandbox: FeaturePriceSet = { // Free tier matches the freeLimitSet
export const standardFeaturePriceSetSandbox: FeaturePriceSet = {
// Free tier matches the freeLimitSet
[FeatureId.SITE_UPTIME]: "price_1RefFBDCpkOb237BPrKZ8IEU",
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF",
[FeatureId.EGRESS_DATA_MB]: "price_1Rfp9LDCpkOb237BwuN5Oiu0",
@@ -60,15 +68,20 @@ export const standardFeaturePriceSetSandbox: FeaturePriceSet = { // Free tier ma
};
export function getStandardFeaturePriceSet(): FeaturePriceSet {
if (process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
) {
return standardFeaturePriceSet;
} else {
return standardFeaturePriceSetSandbox;
}
}
export function getLineItems(featurePriceSet: FeaturePriceSet): Stripe.Checkout.SessionCreateParams.LineItem[] {
export function getLineItems(
featurePriceSet: FeaturePriceSet
): Stripe.Checkout.SessionCreateParams.LineItem[] {
return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({
price: priceId,
price: priceId
}));
}
}

View File

@@ -2,4 +2,4 @@ export * from "./limitSet";
export * from "./features";
export * from "./limitsService";
export * from "./getOrgTierData";
export * from "./createCustomer";
export * from "./createCustomer";

View File

@@ -12,7 +12,7 @@ export const sandboxLimitSet: LimitSet = {
[FeatureId.USERS]: { value: 1, description: "Sandbox limit" },
[FeatureId.EGRESS_DATA_MB]: { value: 1000, description: "Sandbox limit" }, // 1 GB
[FeatureId.DOMAINS]: { value: 0, description: "Sandbox limit" },
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" },
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" }
};
export const freeLimitSet: LimitSet = {
@@ -29,7 +29,7 @@ export const freeLimitSet: LimitSet = {
export const subscribedLimitSet: LimitSet = {
[FeatureId.SITE_UPTIME]: {
value: 2232000,
description: "Contact us to increase soft limit.",
description: "Contact us to increase soft limit."
}, // 50 sites up for 31 days
[FeatureId.USERS]: {
value: 150,
@@ -38,7 +38,7 @@ export const subscribedLimitSet: LimitSet = {
[FeatureId.EGRESS_DATA_MB]: {
value: 12000000,
description: "Contact us to increase soft limit."
}, // 12000 GB
}, // 12000 GB
[FeatureId.DOMAINS]: {
value: 25,
description: "Contact us to increase soft limit."

View File

@@ -1,22 +1,32 @@
export enum TierId {
STANDARD = "standard",
STANDARD = "standard"
}
export type TierPriceSet = {
[key in TierId]: string;
};
export const tierPriceSet: TierPriceSet = { // Free tier matches the freeLimitSet
[TierId.STANDARD]: "price_1RrQ9cD3Ee2Ir7Wmqdy3KBa0",
export const tierPriceSet: TierPriceSet = {
// Free tier matches the freeLimitSet
[TierId.STANDARD]: "price_1RrQ9cD3Ee2Ir7Wmqdy3KBa0"
};
export const tierPriceSetSandbox: TierPriceSet = { // Free tier matches the freeLimitSet
export const tierPriceSetSandbox: TierPriceSet = {
// Free tier matches the freeLimitSet
// when matching tier the keys closer to 0 index are matched first so list the tiers in descending order of value
[TierId.STANDARD]: "price_1RrAYJDCpkOb237By2s1P32m",
[TierId.STANDARD]: "price_1RrAYJDCpkOb237By2s1P32m"
};
export function getTierPriceSet(environment?: string, sandbox_mode?: boolean): TierPriceSet {
if ((process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") || (environment === "prod" && sandbox_mode !== true)) { // THIS GETS LOADED CLIENT SIDE AND SERVER SIDE
export function getTierPriceSet(
environment?: string,
sandbox_mode?: boolean
): TierPriceSet {
if (
(process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true") ||
(environment === "prod" && sandbox_mode !== true)
) {
// THIS GETS LOADED CLIENT SIDE AND SERVER SIDE
return tierPriceSet;
} else {
return tierPriceSetSandbox;

View File

@@ -19,7 +19,7 @@ import logger from "@server/logger";
import { sendToClient } from "#dynamic/routers/ws";
import { build } from "@server/build";
import { s3Client } from "@server/lib/s3";
import cache from "@server/lib/cache";
import cache from "@server/lib/cache";
interface StripeEvent {
identifier?: string;

View File

@@ -1,4 +1,4 @@
import { db, newts, blueprints, Blueprint } from "@server/db";
import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db";
import { Config, ConfigSchema } from "./types";
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
import { fromError } from "zod-validation-error";
@@ -14,6 +14,8 @@ import {
import { BlueprintSource } from "@server/routers/blueprints/types";
import { stringify as stringifyYaml } from "yaml";
import { faker } from "@faker-js/faker";
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";
type ApplyBlueprintArgs = {
orgId: string;
@@ -57,83 +59,195 @@ export async function applyBlueprint({
trx,
siteId
);
});
logger.debug(
`Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
);
// We need to update the targets on the newts from the successfully updated information
for (const result of proxyResourcesResults) {
for (const target of result.targetsToUpdate) {
const [site] = await db
.select()
.from(sites)
.innerJoin(newts, eq(sites.siteId, newts.siteId))
.where(
and(
eq(sites.siteId, target.siteId),
eq(sites.orgId, orgId),
eq(sites.type, "newt"),
isNotNull(sites.pubKey)
)
)
.limit(1);
if (site) {
logger.debug(
`Updating target ${target.targetId} on site ${site.sites.siteId}`
);
// see if you can find a matching target health check from the healthchecksToUpdate array
const matchingHealthcheck =
result.healthchecksToUpdate.find(
(hc) => hc.targetId === target.targetId
);
await addProxyTargets(
site.newt.newtId,
[target],
matchingHealthcheck ? [matchingHealthcheck] : [],
result.proxyResource.protocol,
result.proxyResource.proxyPort
);
}
}
}
logger.debug(
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
);
// We need to update the targets on the newts from the successfully updated information
for (const result of clientResourcesResults) {
const [site] = await db
.select()
.from(sites)
.innerJoin(newts, eq(sites.siteId, newts.siteId))
.where(
and(
eq(sites.siteId, result.resource.siteId),
eq(sites.orgId, orgId),
eq(sites.type, "newt"),
isNotNull(sites.pubKey)
)
)
.limit(1);
logger.debug(
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
`Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
);
// await addClientTargets(
// site.newt.newtId,
// result.resource.destination,
// result.resource.destinationPort,
// result.resource.protocol,
// result.resource.proxyPort
// );
}
// We need to update the targets on the newts from the successfully updated information
for (const result of proxyResourcesResults) {
for (const target of result.targetsToUpdate) {
const [site] = await trx
.select()
.from(sites)
.innerJoin(newts, eq(sites.siteId, newts.siteId))
.where(
and(
eq(sites.siteId, target.siteId),
eq(sites.orgId, orgId),
eq(sites.type, "newt"),
isNotNull(sites.pubKey)
)
)
.limit(1);
if (site) {
logger.debug(
`Updating target ${target.targetId} on site ${site.sites.siteId}`
);
// see if you can find a matching target health check from the healthchecksToUpdate array
const matchingHealthcheck =
result.healthchecksToUpdate.find(
(hc) => hc.targetId === target.targetId
);
await addProxyTargets(
site.newt.newtId,
[target],
matchingHealthcheck ? [matchingHealthcheck] : [],
result.proxyResource.protocol,
result.proxyResource.proxyPort
);
}
}
}
logger.debug(
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
);
// We need to update the targets on the newts from the successfully updated information
for (const result of clientResourcesResults) {
if (
result.oldSiteResource &&
result.oldSiteResource.siteId !=
result.newSiteResource.siteId
) {
// query existing associations
const existingRoleIds = await trx
.select()
.from(roleSiteResources)
.where(
eq(
roleSiteResources.siteResourceId,
result.oldSiteResource.siteResourceId
)
)
.then((rows) => rows.map((row) => row.roleId));
const existingUserIds= await trx
.select()
.from(userSiteResources)
.where(
eq(
userSiteResources.siteResourceId,
result.oldSiteResource.siteResourceId
)
).then((rows) => rows.map((row) => row.userId));
const existingClientIds = await trx
.select()
.from(clientSiteResources)
.where(
eq(
clientSiteResources.siteResourceId,
result.oldSiteResource.siteResourceId
)
).then((rows) => rows.map((row) => row.clientId));
// delete the existing site resource
await trx
.delete(siteResources)
.where(
and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId))
);
await rebuildClientAssociationsFromSiteResource(
result.oldSiteResource,
trx
);
const [insertedSiteResource] = await trx
.insert(siteResources)
.values({
...result.newSiteResource,
})
.returning();
// wait some time to allow for messages to be handled
await new Promise((resolve) => setTimeout(resolve, 750));
//////////////////// update the associations ////////////////////
if (existingRoleIds.length > 0) {
await trx.insert(roleSiteResources).values(
existingRoleIds.map((roleId) => ({
roleId,
siteResourceId: insertedSiteResource!.siteResourceId
}))
);
}
if (existingUserIds.length > 0) {
await trx.insert(userSiteResources).values(
existingUserIds.map((userId) => ({
userId,
siteResourceId: insertedSiteResource!.siteResourceId
}))
);
}
if (existingClientIds.length > 0) {
await trx.insert(clientSiteResources).values(
existingClientIds.map((clientId) => ({
clientId,
siteResourceId: insertedSiteResource!.siteResourceId
}))
);
}
await rebuildClientAssociationsFromSiteResource(
insertedSiteResource,
trx
);
} else {
const [newSite] = await trx
.select()
.from(sites)
.innerJoin(newts, eq(sites.siteId, newts.siteId))
.where(
and(
eq(sites.siteId, result.newSiteResource.siteId),
eq(sites.orgId, orgId),
eq(sites.type, "newt"),
isNotNull(sites.pubKey)
)
)
.limit(1);
if (!newSite) {
logger.debug(
`No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
);
continue;
}
logger.debug(
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.sites.siteId}`
);
await handleMessagingForUpdatedSiteResource(
result.oldSiteResource,
result.newSiteResource,
{
siteId: newSite.sites.siteId,
orgId: newSite.sites.orgId
},
trx
);
}
// await addClientTargets(
// site.newt.newtId,
// result.resource.destination,
// result.resource.destinationPort,
// result.resource.protocol,
// result.resource.proxyPort
// );
}
});
blueprintSucceeded = true;
blueprintMessage = "Blueprint applied successfully";
@@ -171,53 +285,3 @@ export async function applyBlueprint({
return blueprint;
}
// await updateDatabaseFromConfig("org_i21aifypnlyxur2", {
// resources: {
// "resource-nice-id": {
// name: "this is my resource",
// protocol: "http",
// "full-domain": "level1.test.example.com",
// "host-header": "example.com",
// "tls-server-name": "example.com",
// auth: {
// pincode: 123456,
// password: "sadfasdfadsf",
// "sso-enabled": true,
// "sso-roles": ["Member"],
// "sso-users": ["owen@pangolin.net"],
// "whitelist-users": ["owen@pangolin.net"]
// },
// targets: [
// {
// site: "glossy-plains-viscacha-rat",
// hostname: "localhost",
// method: "http",
// port: 8000,
// healthcheck: {
// port: 8000,
// hostname: "localhost"
// }
// },
// {
// site: "glossy-plains-viscacha-rat",
// hostname: "localhost",
// method: "http",
// port: 8001
// }
// ]
// },
// "resource-nice-id2": {
// name: "http server",
// protocol: "tcp",
// "proxy-port": 3000,
// targets: [
// {
// site: "glossy-plains-viscacha-rat",
// hostname: "localhost",
// port: 3000,
// }
// ]
// }
// }
// });

View File

@@ -34,7 +34,12 @@ export async function applyNewtDockerBlueprint(
return;
}
if (isEmptyObject(blueprint["proxy-resources"]) && isEmptyObject(blueprint["client-resources"])) {
if (
isEmptyObject(blueprint["proxy-resources"]) &&
isEmptyObject(blueprint["client-resources"]) &&
isEmptyObject(blueprint["public-resources"]) &&
isEmptyObject(blueprint["private-resources"])
) {
return;
}

View File

@@ -1,17 +1,24 @@
import {
clients,
clientSiteResources,
roles,
roleSiteResources,
SiteResource,
siteResources,
Transaction,
userOrgs,
users,
userSiteResources
} from "@server/db";
import { sites } from "@server/db";
import { eq, and } from "drizzle-orm";
import {
Config,
} from "./types";
import { eq, and, ne, inArray } from "drizzle-orm";
import { Config } from "./types";
import logger from "@server/logger";
import { getNextAvailableAliasAddress } from "../ip";
export type ClientResourcesResults = {
resource: SiteResource;
newSiteResource: SiteResource;
oldSiteResource?: SiteResource;
}[];
export async function updateClientResources(
@@ -75,11 +82,14 @@ export async function updateClientResources(
.set({
name: resourceData.name || resourceNiceId,
siteId: site.siteId,
mode: "port",
proxyPort: resourceData["proxy-port"]!,
destination: resourceData.hostname,
destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol
mode: resourceData.mode,
destination: resourceData.destination,
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
alias: resourceData.alias || null,
disableIcmp: resourceData["disable-icmp"],
tcpPortRangeString: resourceData["tcp-ports"],
udpPortRangeString: resourceData["udp-ports"]
})
.where(
eq(
@@ -89,8 +99,117 @@ export async function updateClientResources(
)
.returning();
results.push({ resource: updatedResource });
const siteResourceId = existingResource.siteResourceId;
const orgId = existingResource.orgId;
await trx
.delete(clientSiteResources)
.where(eq(clientSiteResources.siteResourceId, siteResourceId));
if (resourceData.machines.length > 0) {
// get clientIds from niceIds
const clientsToUpdate = await trx
.select()
.from(clients)
.where(
and(
inArray(clients.niceId, resourceData.machines),
eq(clients.orgId, orgId)
)
);
const clientIds = clientsToUpdate.map(
(client) => client.clientId
);
await trx.insert(clientSiteResources).values(
clientIds.map((clientId) => ({
clientId,
siteResourceId
}))
);
}
await trx
.delete(userSiteResources)
.where(eq(userSiteResources.siteResourceId, siteResourceId));
if (resourceData.users.length > 0) {
// get userIds from username
const usersToUpdate = await trx
.select()
.from(users)
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
.where(
and(
inArray(users.username, resourceData.users),
eq(userOrgs.orgId, orgId)
)
);
const userIds = usersToUpdate.map((user) => user.user.userId);
await trx
.insert(userSiteResources)
.values(
userIds.map((userId) => ({ userId, siteResourceId }))
);
}
// Get all admin role IDs for this org to exclude from deletion
const adminRoles = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)));
const adminRoleIds = adminRoles.map((role) => role.roleId);
if (adminRoleIds.length > 0) {
await trx.delete(roleSiteResources).where(
and(
eq(roleSiteResources.siteResourceId, siteResourceId),
ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role
)
);
} else {
await trx
.delete(roleSiteResources)
.where(
eq(roleSiteResources.siteResourceId, siteResourceId)
);
}
if (resourceData.roles.length > 0) {
// Re-add specified roles but we need to get the roleIds from the role name in the array
const rolesToUpdate = await trx
.select()
.from(roles)
.where(
and(
eq(roles.orgId, orgId),
inArray(roles.name, resourceData.roles)
)
);
const roleIds = rolesToUpdate.map((role) => role.roleId);
await trx
.insert(roleSiteResources)
.values(
roleIds.map((roleId) => ({ roleId, siteResourceId }))
);
}
results.push({
newSiteResource: updatedResource,
oldSiteResource: existingResource
});
} else {
let aliasAddress: string | null = null;
if (resourceData.mode == "host") {
// we can only have an alias on a host
aliasAddress = await getNextAvailableAliasAddress(orgId);
}
// Create new resource
const [newResource] = await trx
.insert(siteResources)
@@ -99,19 +218,107 @@ export async function updateClientResources(
siteId: site.siteId,
niceId: resourceNiceId,
name: resourceData.name || resourceNiceId,
mode: "port",
proxyPort: resourceData["proxy-port"]!,
destination: resourceData.hostname,
destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol
mode: resourceData.mode,
destination: resourceData.destination,
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
alias: resourceData.alias || null,
aliasAddress: aliasAddress,
disableIcmp: resourceData["disable-icmp"],
tcpPortRangeString: resourceData["tcp-ports"],
udpPortRangeString: resourceData["udp-ports"]
})
.returning();
const siteResourceId = newResource.siteResourceId;
const [adminRole] = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (!adminRole) {
throw new Error(`Admin role not found for org ${orgId}`);
}
await trx.insert(roleSiteResources).values({
roleId: adminRole.roleId,
siteResourceId: siteResourceId
});
if (resourceData.roles.length > 0) {
// get roleIds from role names
const rolesToUpdate = await trx
.select()
.from(roles)
.where(
and(
eq(roles.orgId, orgId),
inArray(roles.name, resourceData.roles)
)
);
const roleIds = rolesToUpdate.map((role) => role.roleId);
await trx
.insert(roleSiteResources)
.values(
roleIds.map((roleId) => ({ roleId, siteResourceId }))
);
}
if (resourceData.users.length > 0) {
// get userIds from username
const usersToUpdate = await trx
.select()
.from(users)
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
.where(
and(
inArray(users.username, resourceData.users),
eq(userOrgs.orgId, orgId)
)
);
const userIds = usersToUpdate.map((user) => user.user.userId);
await trx
.insert(userSiteResources)
.values(
userIds.map((userId) => ({ userId, siteResourceId }))
);
}
if (resourceData.machines.length > 0) {
// get clientIds from niceIds
const clientsToUpdate = await trx
.select()
.from(clients)
.where(
and(
inArray(clients.niceId, resourceData.machines),
eq(clients.orgId, orgId)
)
);
const clientIds = clientsToUpdate.map(
(client) => client.clientId
);
await trx.insert(clientSiteResources).values(
clientIds.map((clientId) => ({
clientId,
siteResourceId
}))
);
}
logger.info(
`Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}`
);
results.push({ resource: newResource });
results.push({ newSiteResource: newResource });
}
}

View File

@@ -54,10 +54,14 @@ function getContainerPort(container: Container): number | null {
export function processContainerLabels(containers: Container[]): {
"proxy-resources": { [key: string]: ResourceConfig };
"client-resources": { [key: string]: ResourceConfig };
"public-resources": { [key: string]: ResourceConfig };
"private-resources": { [key: string]: ResourceConfig };
} {
const result = {
"proxy-resources": {} as { [key: string]: ResourceConfig },
"client-resources": {} as { [key: string]: ResourceConfig }
"client-resources": {} as { [key: string]: ResourceConfig },
"public-resources": {} as { [key: string]: ResourceConfig },
"private-resources": {} as { [key: string]: ResourceConfig }
};
// Process each container
@@ -68,8 +72,10 @@ export function processContainerLabels(containers: Container[]): {
const proxyResourceLabels: DockerLabels = {};
const clientResourceLabels: DockerLabels = {};
const publicResourceLabels: DockerLabels = {};
const privateResourceLabels: DockerLabels = {};
// Filter and separate proxy-resources and client-resources labels
// Filter and separate proxy-resources, client-resources, public-resources, and private-resources labels
Object.entries(container.labels).forEach(([key, value]) => {
if (key.startsWith("pangolin.proxy-resources.")) {
// remove the pangolin.proxy- prefix to get "resources.xxx"
@@ -79,17 +85,51 @@ export function processContainerLabels(containers: Container[]): {
// remove the pangolin.client- prefix to get "resources.xxx"
const strippedKey = key.replace("pangolin.client-", "");
clientResourceLabels[strippedKey] = value;
} else if (key.startsWith("pangolin.public-resources.")) {
// remove the pangolin.public- prefix to get "resources.xxx"
const strippedKey = key.replace("pangolin.public-", "");
publicResourceLabels[strippedKey] = value;
} else if (key.startsWith("pangolin.private-resources.")) {
// remove the pangolin.private- prefix to get "resources.xxx"
const strippedKey = key.replace("pangolin.private-", "");
privateResourceLabels[strippedKey] = value;
}
});
// Process proxy resources
if (Object.keys(proxyResourceLabels).length > 0) {
processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]);
processResourceLabels(
proxyResourceLabels,
container,
result["proxy-resources"]
);
}
// Process client resources
if (Object.keys(clientResourceLabels).length > 0) {
processResourceLabels(clientResourceLabels, container, result["client-resources"]);
processResourceLabels(
clientResourceLabels,
container,
result["client-resources"]
);
}
// Process public resources (alias for proxy resources)
if (Object.keys(publicResourceLabels).length > 0) {
processResourceLabels(
publicResourceLabels,
container,
result["public-resources"]
);
}
// Process private resources (alias for client resources)
if (Object.keys(privateResourceLabels).length > 0) {
processResourceLabels(
privateResourceLabels,
container,
result["private-resources"]
);
}
});
@@ -161,8 +201,7 @@ function processResourceLabels(
const finalTarget = { ...target };
if (!finalTarget.hostname) {
finalTarget.hostname =
container.name ||
container.hostname;
container.name || container.hostname;
}
if (!finalTarget.port) {
const containerPort =

View File

@@ -2,7 +2,7 @@ import {
domains,
orgDomains,
Resource,
resourceHeaderAuth,
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
resourcePincode,
resourceRules,
resourceWhitelist,
@@ -16,8 +16,8 @@ import {
userResources,
users
} from "@server/db";
import { resources, targets, sites } from "@server/db";
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
import {resources, targets, sites} from "@server/db";
import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm";
import {
Config,
ConfigSchema,
@@ -25,12 +25,12 @@ import {
TargetData
} from "./types";
import logger from "@server/logger";
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";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { get } from "http";
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";
import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators";
import {get} from "http";
export type ProxyResourcesResults = {
proxyResource: Resource;
@@ -63,7 +63,7 @@ export async function updateProxyResources(
if (targetSiteId) {
// Look up site by niceId
[site] = await trx
.select({ siteId: sites.siteId })
.select({siteId: sites.siteId})
.from(sites)
.where(
and(
@@ -75,7 +75,7 @@ export async function updateProxyResources(
} else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
[site] = await trx
.select({ siteId: sites.siteId })
.select({siteId: sites.siteId})
.from(sites)
.where(
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
@@ -93,7 +93,7 @@ export async function updateProxyResources(
let internalPortToCreate;
if (!targetData["internal-port"]) {
const { internalPort, targetIps } = await pickPort(
const {internalPort, targetIps} = await pickPort(
site.siteId!,
trx
);
@@ -221,13 +221,14 @@ export async function updateProxyResources(
domainId: domain ? domain.domainId : null,
enabled: resourceEnabled,
sso: resourceData.auth?.["sso-enabled"] || false,
skipToIdpId: resourceData.auth?.["auto-login-idp"] || null,
skipToIdpId:
resourceData.auth?.["auto-login-idp"] || null,
ssl: resourceSsl,
setHostHeader: resourceData["host-header"] || null,
tlsServerName: resourceData["tls-server-name"] || null,
emailWhitelistEnabled: resourceData.auth?.[
"whitelist-users"
]
]
? resourceData.auth["whitelist-users"].length > 0
: false,
headers: headers || null,
@@ -286,21 +287,39 @@ export async function updateProxyResources(
existingResource.resourceId
)
);
await trx
.delete(resourceHeaderAuthExtendedCompatibility)
.where(
eq(
resourceHeaderAuthExtendedCompatibility.resourceId,
existingResource.resourceId
)
);
if (resourceData.auth?.["basic-auth"]) {
const headerAuthUser =
resourceData.auth?.["basic-auth"]?.user;
const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password;
if (headerAuthUser && headerAuthPassword) {
const headerAuthExtendedCompatibility =
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
const headerAuthHash = await hashPassword(
Buffer.from(
`${headerAuthUser}:${headerAuthPassword}`
).toString("base64")
);
await trx.insert(resourceHeaderAuth).values({
resourceId: existingResource.resourceId,
headerAuthHash
});
await Promise.all([
trx.insert(resourceHeaderAuth).values({
resourceId: existingResource.resourceId,
headerAuthHash
}),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
resourceId: existingResource.resourceId,
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
})
]);
}
}
@@ -361,7 +380,7 @@ export async function updateProxyResources(
if (targetSiteId) {
// Look up site by niceId
[site] = await trx
.select({ siteId: sites.siteId })
.select({siteId: sites.siteId})
.from(sites)
.where(
and(
@@ -373,7 +392,7 @@ export async function updateProxyResources(
} else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
[site] = await trx
.select({ siteId: sites.siteId })
.select({siteId: sites.siteId})
.from(sites)
.where(
and(
@@ -418,7 +437,7 @@ export async function updateProxyResources(
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
let internalPortToUpdate;
if (!targetData["internal-port"]) {
const { internalPort, targetIps } = await pickPort(
const {internalPort, targetIps} = await pickPort(
site.siteId!,
trx
);
@@ -546,7 +565,8 @@ export async function updateProxyResources(
if (
existingRule.action !== getRuleAction(rule.action) ||
existingRule.match !== rule.match.toUpperCase() ||
existingRule.value !== getRuleValue(rule.match.toUpperCase(), rule.value)
existingRule.value !==
getRuleValue(rule.match.toUpperCase(), rule.value)
) {
validateRule(rule);
await trx
@@ -554,7 +574,10 @@ export async function updateProxyResources(
.set({
action: getRuleAction(rule.action),
match: rule.match.toUpperCase(),
value: getRuleValue(rule.match.toUpperCase(), rule.value),
value: getRuleValue(
rule.match.toUpperCase(),
rule.value
)
})
.where(
eq(resourceRules.ruleId, existingRule.ruleId)
@@ -566,7 +589,10 @@ export async function updateProxyResources(
resourceId: existingResource.resourceId,
action: getRuleAction(rule.action),
match: rule.match.toUpperCase(),
value: getRuleValue(rule.match.toUpperCase(), rule.value),
value: getRuleValue(
rule.match.toUpperCase(),
rule.value
),
priority: index + 1 // start priorities at 1
});
}
@@ -648,18 +674,25 @@ export async function updateProxyResources(
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password;
const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility;
if (headerAuthUser && headerAuthPassword) {
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
const headerAuthHash = await hashPassword(
Buffer.from(
`${headerAuthUser}:${headerAuthPassword}`
).toString("base64")
);
await trx.insert(resourceHeaderAuth).values({
resourceId: newResource.resourceId,
headerAuthHash
});
await Promise.all([
trx.insert(resourceHeaderAuth).values({
resourceId: newResource.resourceId,
headerAuthHash
}),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
resourceId: newResource.resourceId,
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
}),
]);
}
}
@@ -852,16 +885,16 @@ async function syncUserResources(
.from(userResources)
.where(eq(userResources.resourceId, resourceId));
for (const email of ssoUsers) {
for (const username of ssoUsers) {
const [user] = await trx
.select()
.from(users)
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
.where(and(eq(users.email, email), eq(userOrgs.orgId, orgId)))
.where(and(eq(users.username, username), eq(userOrgs.orgId, orgId)))
.limit(1);
if (!user) {
throw new Error(`User not found: ${email} in org ${orgId}`);
throw new Error(`User not found: ${username} in org ${orgId}`);
}
const existingUserResource = existingUserResources.find(
@@ -889,7 +922,11 @@ async function syncUserResources(
)
.limit(1);
if (user && user.user.email && !ssoUsers.includes(user.user.email)) {
if (
user &&
user.user.username &&
!ssoUsers.includes(user.user.username)
) {
await trx
.delete(userResources)
.where(
@@ -1006,7 +1043,7 @@ async function getDomain(
trx: Transaction
) {
const [fullDomainExists] = await trx
.select({ resourceId: resources.resourceId })
.select({resourceId: resources.resourceId})
.from(resources)
.where(
and(
@@ -1074,10 +1111,8 @@ async function getDomainId(
// remove the base domain of the domain
let subdomain = null;
if (domainSelection.type == "ns") {
if (fullDomain != baseDomain) {
subdomain = fullDomain.replace(`.${baseDomain}`, "");
}
if (fullDomain != baseDomain) {
subdomain = fullDomain.replace(`.${baseDomain}`, "");
}
// Return the first valid domain

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { portRangeStringSchema } from "@server/lib/ip";
export const SiteSchema = z.object({
name: z.string().min(1).max(100),
@@ -9,14 +10,18 @@ export const TargetHealthCheckSchema = z.object({
hostname: z.string(),
port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
path: z.string().optional(),
path: z.string().optional().default("/"),
scheme: z.string().optional(),
mode: z.string().default("http"),
interval: z.int().default(30),
"unhealthy-interval": z.int().default(30),
unhealthyInterval: z.int().optional(), // deprecated alias
timeout: z.int().default(5),
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null),
headers: z
.array(z.object({ name: z.string(), value: z.string() }))
.nullable()
.optional()
.default(null),
"follow-redirects": z.boolean().default(true),
followRedirects: z.boolean().optional(), // deprecated alias
method: z.string().default("GET"),
@@ -36,7 +41,10 @@ export const TargetSchema = z.object({
healthcheck: TargetHealthCheckSchema.optional(),
rewritePath: z.string().optional(), // deprecated alias
"rewrite-path": z.string().optional(),
"rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
"rewrite-match": z
.enum(["exact", "prefix", "regex", "stripPrefix"])
.optional()
.nullable(),
priority: z.int().min(1).max(1000).optional().default(100)
});
export type TargetData = z.infer<typeof TargetSchema>;
@@ -47,7 +55,8 @@ export const AuthSchema = z.object({
password: z.string().min(1).optional(),
"basic-auth": z.object({
user: z.string().min(1),
password: z.string().min(1)
password: z.string().min(1),
extendedCompatibility: z.boolean().default(true)
}).optional(),
"sso-enabled": z.boolean().optional().default(false),
"sso-roles": z
@@ -59,14 +68,74 @@ export const AuthSchema = z.object({
}),
"sso-users": z.array(z.email()).optional().default([]),
"whitelist-users": z.array(z.email()).optional().default([]),
"auto-login-idp": z.int().positive().optional(),
"auto-login-idp": z.int().positive().optional()
});
export const RuleSchema = z.object({
action: z.enum(["allow", "deny", "pass"]),
match: z.enum(["cidr", "path", "ip", "country"]),
value: z.string()
});
export const RuleSchema = z
.object({
action: z.enum(["allow", "deny", "pass"]),
match: z.enum(["cidr", "path", "ip", "country", "asn"]),
value: z.string()
})
.refine(
(rule) => {
if (rule.match === "ip") {
// Check if it's a valid IP address (v4 or v6)
return z.union([z.ipv4(), z.ipv6()]).safeParse(rule.value)
.success;
}
return true;
},
{
path: ["value"],
message: "Value must be a valid IP address when match is 'ip'"
}
)
.refine(
(rule) => {
if (rule.match === "cidr") {
// Check if it's a valid CIDR (v4 or v6)
return z.union([z.cidrv4(), z.cidrv6()]).safeParse(rule.value)
.success;
}
return true;
},
{
path: ["value"],
message: "Value must be a valid CIDR notation when match is 'cidr'"
}
)
.refine(
(rule) => {
if (rule.match === "country") {
// Check if it's a valid 2-letter country code
return /^[A-Z]{2}$/.test(rule.value);
}
return true;
},
{
path: ["value"],
message:
"Value must be a 2-letter country code when match is 'country'"
}
)
.refine(
(rule) => {
if (rule.match === "asn") {
// Check if it's either AS<number> format or just a number
const asNumberPattern = /^AS\d+$/i;
const isASFormat = asNumberPattern.test(rule.value);
const isNumeric = /^\d+$/.test(rule.value);
return isASFormat || isNumeric;
}
return true;
},
{
path: ["value"],
message:
"Value must be either 'AS<number>' format or a number when match is 'asn'"
}
);
export const HeaderSchema = z.object({
name: z.string().min(1),
@@ -122,7 +191,6 @@ export const ResourceSchema = z
{
path: ["targets"],
error: "When protocol is 'http', all targets must have a 'method' field"
}
)
.refine(
@@ -204,130 +272,225 @@ export function isTargetsOnlyResource(resource: any): boolean {
return Object.keys(resource).length === 1 && resource.targets;
}
export const ClientResourceSchema = z.object({
name: z.string().min(2).max(100),
site: z.string().min(2).max(100).optional(),
protocol: z.enum(["tcp", "udp"]),
"proxy-port": z.number().min(1).max(65535),
"hostname": z.string().min(1).max(255),
"internal-port": z.number().min(1).max(65535),
enabled: z.boolean().optional().default(true)
});
export const ClientResourceSchema = z
.object({
name: z.string().min(1).max(255),
mode: z.enum(["host", "cidr"]),
site: z.string(),
// protocol: z.enum(["tcp", "udp"]).optional(),
// proxyPort: z.int().positive().optional(),
// destinationPort: z.int().positive().optional(),
destination: z.string().min(1),
// enabled: z.boolean().default(true),
"tcp-ports": portRangeStringSchema.optional().default("*"),
"udp-ports": portRangeStringSchema.optional().default("*"),
"disable-icmp": z.boolean().optional().default(false),
alias: z
.string()
.regex(
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/,
"Alias must be a fully qualified domain name (e.g., example.com)"
)
.optional(),
roles: z
.array(z.string())
.optional()
.default([])
.refine((roles) => !roles.includes("Admin"), {
error: "Admin role cannot be included in roles"
}),
users: z.array(z.email()).optional().default([]),
machines: z.array(z.string()).optional().default([])
})
.refine(
(data) => {
if (data.mode === "host") {
// Check if it's a valid IP address using zod (v4 or v6)
const isValidIP = z
.union([z.ipv4(), z.ipv6()])
.safeParse(data.destination).success;
if (isValidIP) {
return true;
}
// Check if it's a valid domain (hostname pattern, TLD not required)
const domainRegex =
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
const isValidDomain = domainRegex.test(data.destination);
const isValidAlias = data.alias && domainRegex.test(data.alias);
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
}
return true;
},
{
message:
"Destination must be a valid IP address or valid domain AND alias is required"
}
)
.refine(
(data) => {
if (data.mode === "cidr") {
// Check if it's a valid CIDR (v4 or v6)
const isValidCIDR = z
.union([z.cidrv4(), z.cidrv6()])
.safeParse(data.destination).success;
return isValidCIDR;
}
return true;
},
{
message: "Destination must be a valid CIDR notation for cidr mode"
}
);
// Schema for the entire configuration object
export const ConfigSchema = z
.object({
"proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}),
"client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}),
"proxy-resources": z
.record(z.string(), ResourceSchema)
.optional()
.prefault({}),
"public-resources": z
.record(z.string(), ResourceSchema)
.optional()
.prefault({}),
"client-resources": z
.record(z.string(), ClientResourceSchema)
.optional()
.prefault({}),
"private-resources": z
.record(z.string(), ClientResourceSchema)
.optional()
.prefault({}),
sites: z.record(z.string(), SiteSchema).optional().prefault({})
})
.refine(
.transform((data) => {
// Merge public-resources into proxy-resources
if (data["public-resources"]) {
data["proxy-resources"] = {
...data["proxy-resources"],
...data["public-resources"]
};
delete (data as any)["public-resources"];
}
// Merge private-resources into client-resources
if (data["private-resources"]) {
data["client-resources"] = {
...data["client-resources"],
...data["private-resources"]
};
delete (data as any)["private-resources"];
}
return data as {
"proxy-resources": Record<string, z.infer<typeof ResourceSchema>>;
"client-resources": Record<
string,
z.infer<typeof ClientResourceSchema>
>;
sites: Record<string, z.infer<typeof SiteSchema>>;
};
})
.superRefine((config, ctx) => {
// Enforce the full-domain uniqueness across resources in the same stack
(config) => {
// Extract duplicates for error message
const fullDomainMap = new Map<string, string[]>();
const fullDomainMap = new Map<string, string[]>();
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const fullDomain = resource["full-domain"];
if (fullDomain) {
// Only process if full-domain is defined
if (!fullDomainMap.has(fullDomain)) {
fullDomainMap.set(fullDomain, []);
}
fullDomainMap.get(fullDomain)!.push(resourceKey);
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const fullDomain = resource["full-domain"];
if (fullDomain) {
// Only process if full-domain is defined
if (!fullDomainMap.has(fullDomain)) {
fullDomainMap.set(fullDomain, []);
}
fullDomainMap.get(fullDomain)!.push(resourceKey);
}
);
const duplicates = Array.from(fullDomainMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(
([fullDomain, resourceKeys]) =>
`'${fullDomain}' used by resources: ${resourceKeys.join(", ")}`
)
.join("; ");
if (duplicates.length !== 0) {
return {
path: ["resources"],
error: `Duplicate 'full-domain' values found: ${duplicates}`
};
}
);
const fullDomainDuplicates = Array.from(fullDomainMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(
([fullDomain, resourceKeys]) =>
`'${fullDomain}' used by resources: ${resourceKeys.join(", ")}`
)
.join("; ");
if (fullDomainDuplicates.length !== 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["proxy-resources"],
message: `Duplicate 'full-domain' values found: ${fullDomainDuplicates}`
});
}
)
.refine(
// Enforce proxy-port uniqueness within proxy-resources per protocol
(config) => {
// Extract duplicates for error message
const protocolPortMap = new Map<string, string[]>();
const protocolPortMap = new Map<string, string[]>();
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
const protocol = resource.protocol;
if (proxyPort !== undefined && protocol !== undefined) {
const key = `${protocol}:${proxyPort}`;
if (!protocolPortMap.has(key)) {
protocolPortMap.set(key, []);
}
protocolPortMap.get(key)!.push(resourceKey);
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
const protocol = resource.protocol;
if (proxyPort !== undefined && protocol !== undefined) {
const key = `${protocol}:${proxyPort}`;
if (!protocolPortMap.has(key)) {
protocolPortMap.set(key, []);
}
protocolPortMap.get(key)!.push(resourceKey);
}
);
const duplicates = Array.from(protocolPortMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(
([protocolPort, resourceKeys]) => {
const [protocol, port] = protocolPort.split(':');
return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`;
}
)
.join("; ");
if (duplicates.length !== 0) {
return {
path: ["proxy-resources"],
error: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`
};
}
}
)
.refine(
// Enforce proxy-port uniqueness within client-resources
(config) => {
// Extract duplicates for error message
const proxyPortMap = new Map<number, string[]>();
);
Object.entries(config["client-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
if (proxyPort !== undefined) {
if (!proxyPortMap.has(proxyPort)) {
proxyPortMap.set(proxyPort, []);
}
proxyPortMap.get(proxyPort)!.push(resourceKey);
const portDuplicates = Array.from(protocolPortMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(([protocolPort, resourceKeys]) => {
const [protocol, port] = protocolPort.split(":");
return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`;
})
.join("; ");
if (portDuplicates.length !== 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["proxy-resources"],
message: `Duplicate 'proxy-port' values found in proxy-resources: ${portDuplicates}`
});
}
// Enforce alias uniqueness within client-resources
const aliasMap = new Map<string, string[]>();
Object.entries(config["client-resources"]).forEach(
([resourceKey, resource]) => {
const alias = resource.alias;
if (alias !== undefined) {
if (!aliasMap.has(alias)) {
aliasMap.set(alias, []);
}
aliasMap.get(alias)!.push(resourceKey);
}
);
const duplicates = Array.from(proxyPortMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(
([proxyPort, resourceKeys]) =>
`port ${proxyPort} used by client-resources: ${resourceKeys.join(", ")}`
)
.join("; ");
if (duplicates.length !== 0) {
return {
path: ["client-resources"],
error: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`
};
}
);
const aliasDuplicates = Array.from(aliasMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(
([alias, resourceKeys]) =>
`alias '${alias}' used by client-resources: ${resourceKeys.join(", ")}`
)
.join("; ");
if (aliasDuplicates.length !== 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["client-resources"],
message: `Duplicate 'alias' values found in client-resources: ${aliasDuplicates}`
});
}
);
});
// Type inference from the schema
export type Site = z.infer<typeof SiteSchema>;

View File

@@ -1,5 +1,20 @@
import NodeCache from "node-cache";
import logger from "@server/logger";
export const cache = new NodeCache({ stdTTL: 3600, checkperiod: 120 });
// Create cache with maxKeys limit to prevent memory leaks
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
export const cache = new NodeCache({
stdTTL: 3600,
checkperiod: 120,
maxKeys: 10000
});
export default cache;
// Log cache statistics periodically for monitoring
setInterval(() => {
const stats = cache.getStats();
logger.debug(
`Cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%`
);
}, 300000); // Every 5 minutes
export default cache;

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