Compare commits

..

336 Commits

Author SHA1 Message Date
Owen Schwartz
362981ad19 Merge pull request #2498 from fosrl/crowdin_dev
New Crowdin updates
2026-02-17 21:57:14 -08:00
Owen Schwartz
fa4f7e4ac2 New translations en-us.json (Norwegian Bokmal) 2026-02-17 21:56:24 -08:00
Owen Schwartz
c6bca4e2ab New translations en-us.json (Chinese Simplified) 2026-02-17 21:56:23 -08:00
Owen Schwartz
e28b361e05 New translations en-us.json (Turkish) 2026-02-17 21:56:22 -08:00
Owen Schwartz
a18691011b New translations en-us.json (Russian) 2026-02-17 21:56:20 -08:00
Owen Schwartz
c4a6403cba New translations en-us.json (Portuguese) 2026-02-17 21:56:19 -08:00
Owen Schwartz
1851bf941a New translations en-us.json (Polish) 2026-02-17 21:56:18 -08:00
Owen Schwartz
b7ab3c2e92 New translations en-us.json (Dutch) 2026-02-17 21:56:16 -08:00
Owen Schwartz
ce1ad032ba New translations en-us.json (Korean) 2026-02-17 21:56:15 -08:00
Owen Schwartz
8446c68e1b New translations en-us.json (Italian) 2026-02-17 21:56:14 -08:00
Owen Schwartz
40ed388b0f New translations en-us.json (German) 2026-02-17 21:56:12 -08:00
Owen Schwartz
ce1693aa2f New translations en-us.json (Czech) 2026-02-17 21:56:11 -08:00
Owen Schwartz
11d16a1552 New translations en-us.json (Bulgarian) 2026-02-17 21:56:10 -08:00
Owen Schwartz
0ac54a2c88 New translations en-us.json (Spanish) 2026-02-17 21:56:08 -08:00
Owen Schwartz
b7d8b32123 New translations en-us.json (French) 2026-02-17 21:56:07 -08:00
miloschwartz
7ad76f5683 allow type hyphen in orgID 2026-02-17 21:54:55 -08:00
Owen
e2f78ba476 Merge branch 'main' of github.com:fosrl/pangolin into dev 2026-02-17 21:06:16 -08:00
Owen
5d92190d50 Merge branch 'cloud-multi-org' into dev 2026-02-17 21:01:44 -08:00
Owen
2b0d6de986 Handle feature lifecycle for multiple orgs 2026-02-17 21:00:48 -08:00
Owen
057f82a561 Fix some cosmetics 2026-02-17 20:46:02 -08:00
Owen
719d2a5ffe Count everything when deleting the org 2026-02-17 20:39:47 -08:00
miloschwartz
d4bff9d5cb clean orgId and fix primary badge 2026-02-17 20:35:36 -08:00
Owen
19fcc1f93b Set org limit 2026-02-17 20:18:50 -08:00
miloschwartz
d45ea127c2 use billing org id in get subscription status 2026-02-17 20:07:29 -08:00
Owen
f591cf8601 Look to the right org to test is subscribed 2026-02-17 20:06:58 -08:00
Owen
6661a76aa8 Update member resources page and testing new org counts 2026-02-17 20:01:43 -08:00
miloschwartz
a2ed22bfcc use add/remove helper functions in auto (de)provision 2026-02-17 17:50:41 -08:00
Owen
e370f8891a Also update in the assign 2026-02-17 17:34:57 -08:00
miloschwartz
8a83e32c42 add send email verification opt out 2026-02-17 17:33:35 -08:00
Owen
831eb6325c Centralize user functions 2026-02-17 17:31:41 -08:00
Owen
4d6240c987 Handle new usage tracking with multi org 2026-02-17 17:10:05 -08:00
miloschwartz
79cf7c84dc support delete org and preserve path on switch 2026-02-17 16:45:15 -08:00
Owen
b71f582329 Use the billing org id when updating and checking usage 2026-02-17 15:09:42 -08:00
Owen
8315d4b6ae Dont create ca certs quite yet 2026-02-17 14:48:13 -08:00
miloschwartz
b8c3cc751a support creating multiple orgs in saas 2026-02-17 14:37:46 -08:00
Owen
d00262dc31 Send the right port and cert 2026-02-17 11:43:38 -08:00
Owen
3debc6c8d3 Add round trip tracking for any message 2026-02-16 20:29:55 -08:00
Owen
5092eb58fb Ssh host should be the destination 2026-02-16 15:31:09 -08:00
Owen
f0b9240575 Accept resource as either niceId or alias 2026-02-16 15:29:23 -08:00
Owen
9cf59c409e Initial sign endpoint working 2026-02-16 15:19:29 -08:00
Owen
bfd5aa30a7 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-02-15 11:09:11 -08:00
Owen
9737170665 Merge branch 'Lokowitz-update-packages' into dev 2026-02-15 11:08:12 -08:00
Owen
922a040466 Merge branch 'update-packages' of github.com:Lokowitz/pangolin into Lokowitz-update-packages 2026-02-15 11:08:02 -08:00
miloschwartz
9eacefb155 support delete account 2026-02-14 22:44:30 -08:00
miloschwartz
33f0782f3a support delete account 2026-02-14 18:01:37 -08:00
Milo Schwartz
e6a5cef945 Merge pull request #2371 from Fredkiss3/refactor/paginated-tables
feat: server side filtered, ordered & paginated tables
2026-02-14 11:43:01 -08:00
miloschwartz
4c8edb80b3 dont show table footer in client-side data-table 2026-02-14 11:40:59 -08:00
miloschwartz
d4668fae99 add openapi types 2026-02-14 11:25:00 -08:00
Fred KISSIE
ddfe55e3ae ♻️ add niceId to query filtering on most tables 2026-02-14 04:19:30 +01:00
Fred KISSIE
761a5f1d4c ♻️ use like & LOWER(column) for searching with query 2026-02-14 04:11:27 +01:00
Fred KISSIE
1fbcad8787 ♻️ refactor 2026-02-14 04:06:11 +01:00
miloschwartz
aba586e605 change translation 2026-02-13 17:35:54 -08:00
Milo Schwartz
27b21b5ad4 Merge pull request #2359 from Fredkiss3/feat/logo-path-in-enterprise
feat: Support file path in branding logo URL for enterprise
2026-02-13 17:16:33 -08:00
Milo Schwartz
b6e54dab17 Merge branch 'dev' into feat/logo-path-in-enterprise 2026-02-13 17:16:25 -08:00
miloschwartz
1f8e89772d disable global idp routes if idp mode is org 2026-02-13 15:46:13 -08:00
Owen
843b13ed57 Try to fix cicd 2026-02-13 15:00:17 -08:00
Owen
be89e5ca55 Fix issue with auto provisioning being overriden 2026-02-13 14:56:56 -08:00
Lokowitz
5f3657fd56 update packages 2026-02-13 06:15:26 +00:00
Lokowitz
494162400e Merge remote-tracking branch 'origin/dev' into update-packages 2026-02-13 06:12:24 +00:00
Fred KISSIE
ab65bb6a8a Merge branch 'dev' into refactor/paginated-tables 2026-02-13 06:03:09 +01:00
miloschwartz
333625f199 rename starter in cloud to basic 2026-02-12 20:24:23 -08:00
Owen
dbfd715381 Fix windows formatting 2026-02-12 16:27:51 -08:00
Owen Schwartz
f1d989964e Merge pull request #2471 from fosrl/dev
Update translations
2026-02-12 16:14:41 -08:00
Owen Schwartz
b701629498 Merge pull request #2465 from fosrl/crowdin_dev
New Crowdin updates
2026-02-12 16:14:17 -08:00
Owen Schwartz
8250946325 New translations en-us.json (Norwegian Bokmal) 2026-02-12 16:14:04 -08:00
Owen Schwartz
71f63d8e6f New translations en-us.json (Chinese Simplified) 2026-02-12 16:14:03 -08:00
Owen Schwartz
dd5e834db0 New translations en-us.json (Turkish) 2026-02-12 16:14:01 -08:00
Owen Schwartz
970ecb52f0 New translations en-us.json (Russian) 2026-02-12 16:14:00 -08:00
Owen Schwartz
62ea1b40e1 New translations en-us.json (Portuguese) 2026-02-12 16:13:58 -08:00
Owen Schwartz
3b0fd5c592 New translations en-us.json (Polish) 2026-02-12 16:13:57 -08:00
Owen Schwartz
b7616026dd New translations en-us.json (Dutch) 2026-02-12 16:13:55 -08:00
Owen Schwartz
16ad60b89a New translations en-us.json (Korean) 2026-02-12 16:13:54 -08:00
Owen Schwartz
db7971d2f7 New translations en-us.json (Italian) 2026-02-12 16:13:53 -08:00
Owen Schwartz
f3f8bd3125 New translations en-us.json (German) 2026-02-12 16:13:51 -08:00
Owen Schwartz
516fd0ee8f New translations en-us.json (Czech) 2026-02-12 16:13:50 -08:00
Owen Schwartz
8d6700d493 New translations en-us.json (Bulgarian) 2026-02-12 16:13:49 -08:00
Owen Schwartz
9d4ace9b3e New translations en-us.json (Spanish) 2026-02-12 16:13:47 -08:00
Owen Schwartz
2800655e33 New translations en-us.json (French) 2026-02-12 16:13:45 -08:00
Owen Schwartz
91eecee11d Merge pull request #2469 from fosrl/dev
1.15.4
2026-02-12 16:10:44 -08:00
Owen Schwartz
899e5aa395 New translations en-us.json (Norwegian Bokmal) 2026-02-12 15:41:25 -08:00
Owen Schwartz
d5820c4902 New translations en-us.json (Chinese Simplified) 2026-02-12 15:41:24 -08:00
Owen Schwartz
a91c002274 New translations en-us.json (Turkish) 2026-02-12 15:41:22 -08:00
Owen Schwartz
4d142b93dd New translations en-us.json (Russian) 2026-02-12 15:41:21 -08:00
Owen Schwartz
04dcf57ff3 New translations en-us.json (Portuguese) 2026-02-12 15:41:20 -08:00
Owen Schwartz
975550c755 New translations en-us.json (Polish) 2026-02-12 15:41:18 -08:00
Owen Schwartz
a964a80d85 New translations en-us.json (Dutch) 2026-02-12 15:41:17 -08:00
Owen Schwartz
22c3b8f116 New translations en-us.json (Korean) 2026-02-12 15:41:15 -08:00
Owen Schwartz
c4b1831cfe New translations en-us.json (Italian) 2026-02-12 15:41:14 -08:00
Owen Schwartz
cdb6813384 New translations en-us.json (German) 2026-02-12 15:41:13 -08:00
Owen Schwartz
b14b68d83c New translations en-us.json (Czech) 2026-02-12 15:41:11 -08:00
Owen Schwartz
3c2f930e6b New translations en-us.json (Bulgarian) 2026-02-12 15:41:10 -08:00
Owen Schwartz
ca9c7ce555 New translations en-us.json (Spanish) 2026-02-12 15:41:08 -08:00
Owen Schwartz
c2e95a0607 New translations en-us.json (French) 2026-02-12 15:41:06 -08:00
miloschwartz
2767ee9e80 update pangolin cli links 2026-02-12 15:29:08 -08:00
miloschwartz
d998a8087f fix pg migration 2026-02-12 15:06:24 -08:00
miloschwartz
fdce016921 add 1.15.4 migration 2026-02-12 15:00:12 -08:00
miloschwartz
c73d70933b bump version 2026-02-12 14:52:29 -08:00
miloschwartz
e9d0ad6e37 use pangolin cli for container 2026-02-12 14:51:25 -08:00
Owen
a35586f762 Add sudo 2026-02-12 14:47:55 -08:00
miloschwartz
f527c30923 add post auth url 2026-02-12 14:21:50 -08:00
Owen
94e70219cf Make install sudo because run is sudo 2026-02-12 14:12:45 -08:00
Owen
6496763aae Cap retention days 2026-02-12 12:18:42 -08:00
Owen
a409ec269b Change back to lokowitz db method 2026-02-12 12:13:13 -08:00
Owen
bc7bc8da66 Stop tracking files that should be ignored 2026-02-12 12:07:57 -08:00
Owen
52484c774e Setting up drizzle and fix site not showing in private resource 2026-02-12 12:05:15 -08:00
Lokowitz
4e1e0cade1 upgrade package 2026-02-12 15:51:19 +00:00
Lokowitz
fda5904dac Merge remote-tracking branch 'origin/dev' into update-packages 2026-02-12 15:47:29 +00:00
Owen Schwartz
69ecc22318 New translations en-us.json (German) 2026-02-12 01:39:38 -08:00
Owen
bff9d33ee6 Move back to db:sqlite:generate 2026-02-11 21:47:10 -08:00
Owen Schwartz
edf506953b Merge pull request #2463 from fosrl/dev
1.15.3
2026-02-11 21:42:04 -08:00
Owen Schwartz
5e11746549 New translations en-us.json (Norwegian Bokmal) 2026-02-11 21:40:26 -08:00
Owen Schwartz
1ae315e303 New translations en-us.json (Chinese Simplified) 2026-02-11 21:40:26 -08:00
Owen Schwartz
758b03ab25 New translations en-us.json (Turkish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
e756fad573 New translations en-us.json (Russian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
3547450b03 New translations en-us.json (Portuguese) 2026-02-11 21:40:26 -08:00
Owen Schwartz
733f6692c6 New translations en-us.json (Polish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
2d83160b16 New translations en-us.json (Dutch) 2026-02-11 21:40:26 -08:00
Owen Schwartz
256fa880dd New translations en-us.json (Korean) 2026-02-11 21:40:26 -08:00
Owen Schwartz
b08c5f5c67 New translations en-us.json (Italian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
d0862a2d26 New translations en-us.json (German) 2026-02-11 21:40:26 -08:00
Owen Schwartz
e97340ed52 New translations en-us.json (Czech) 2026-02-11 21:40:26 -08:00
Owen Schwartz
e27c81eea6 New translations en-us.json (Bulgarian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
7f7f3d43b2 New translations en-us.json (Spanish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
4b1b772098 New translations en-us.json (French) 2026-02-11 21:40:26 -08:00
Owen Schwartz
f66b88490f New translations en-us.json (Norwegian Bokmal) 2026-02-11 21:40:26 -08:00
Owen Schwartz
18f9157169 New translations en-us.json (Chinese Simplified) 2026-02-11 21:40:26 -08:00
Owen Schwartz
6eb82a807b New translations en-us.json (Turkish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
bf57a97833 New translations en-us.json (Russian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
e9e2093220 New translations en-us.json (Portuguese) 2026-02-11 21:40:26 -08:00
Owen Schwartz
c3540da2e3 New translations en-us.json (Polish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
d228cf56dd New translations en-us.json (Dutch) 2026-02-11 21:40:26 -08:00
Owen Schwartz
8f4cecd963 New translations en-us.json (Korean) 2026-02-11 21:40:26 -08:00
Owen Schwartz
66adff44bb New translations en-us.json (Italian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
be41c094dc New translations en-us.json (German) 2026-02-11 21:40:26 -08:00
Owen Schwartz
273848ca18 New translations en-us.json (Czech) 2026-02-11 21:40:26 -08:00
Owen Schwartz
1e9dbead3b New translations en-us.json (Bulgarian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
aeaa8ba133 New translations en-us.json (Spanish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
24654af635 New translations en-us.json (French) 2026-02-11 21:40:26 -08:00
Owen Schwartz
e88a21d6db New translations en-us.json (Norwegian Bokmal) 2026-02-11 21:40:26 -08:00
Owen Schwartz
bcd01badaf New translations en-us.json (Chinese Simplified) 2026-02-11 21:40:26 -08:00
Owen Schwartz
8e063506e0 New translations en-us.json (Turkish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
84f5d6137a New translations en-us.json (Russian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
0a8565f5e8 New translations en-us.json (Portuguese) 2026-02-11 21:40:26 -08:00
Owen Schwartz
bd8da25a46 New translations en-us.json (Polish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
a841f588dd New translations en-us.json (Dutch) 2026-02-11 21:40:26 -08:00
Owen Schwartz
75a4362ce3 New translations en-us.json (Korean) 2026-02-11 21:40:26 -08:00
Owen Schwartz
e763e001e5 New translations en-us.json (Italian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
69475a0ae7 New translations en-us.json (German) 2026-02-11 21:40:26 -08:00
Owen Schwartz
53e14c2ad7 New translations en-us.json (Czech) 2026-02-11 21:40:26 -08:00
Owen Schwartz
1edc33148a New translations en-us.json (Bulgarian) 2026-02-11 21:40:26 -08:00
Owen Schwartz
a4cbfc74e4 New translations en-us.json (Spanish) 2026-02-11 21:40:26 -08:00
Owen Schwartz
c0d25aeb02 New translations en-us.json (French) 2026-02-11 21:40:26 -08:00
miloschwartz
40f49bf6da add pangolin cloud link 2026-02-11 19:46:19 -08:00
Owen
0bfce87dc6 Ignore migrations 2026-02-11 19:25:00 -08:00
Owen
2a0655e9de Bump version 2026-02-11 19:08:11 -08:00
Owen
a86cfa5934 Add missing col in migration 2026-02-11 19:03:23 -08:00
miloschwartz
54b77523c5 remove console.log 2026-02-11 19:02:22 -08:00
Owen
ba06c8928d Dont restrict numbers outside of the cloud 2026-02-11 19:01:47 -08:00
miloschwartz
c8a4ac1ed4 add global/org idp banner 2026-02-11 19:01:06 -08:00
miloschwartz
143acbae48 add identity provider mode setting 2026-02-11 18:05:06 -08:00
Owen
937f6fdae8 Add better error message 2026-02-11 17:56:58 -08:00
Owen
ba7239ac08 Verify everything we can 2026-02-11 17:30:21 -08:00
Owen
2e748274c0 Merge branch 'main' into dev 2026-02-11 17:22:19 -08:00
Owen
eab2750953 Add migrations for 1.15.3 2026-02-11 17:21:15 -08:00
miloschwartz
17b6cb0c73 Merge branch 'new-pricing' into dev 2026-02-11 17:11:21 -08:00
Owen
98a4c453c1 Fix anouther subscribed logic issue 2026-02-11 12:41:22 -08:00
Owen
6475dceab9 Rename tiers in features and fix subscribed logic issue 2026-02-11 12:38:18 -08:00
Owen
040a945774 Fix dynamic -> private import violations 2026-02-11 10:30:25 -08:00
Owen
47743a5fa8 Fix private import -> dynamic 2026-02-11 10:26:50 -08:00
Owen Schwartz
d47d6de985 New translations en-us.json (Norwegian Bokmal) 2026-02-11 10:23:00 -08:00
Owen Schwartz
37818b8594 New translations en-us.json (Chinese Simplified) 2026-02-11 10:23:00 -08:00
Owen Schwartz
3b184acddd New translations en-us.json (Turkish) 2026-02-11 10:23:00 -08:00
Owen Schwartz
9c80404d17 New translations en-us.json (Russian) 2026-02-11 10:23:00 -08:00
Owen Schwartz
aaa7082f9d New translations en-us.json (Portuguese) 2026-02-11 10:23:00 -08:00
Owen Schwartz
a45b45b2ce New translations en-us.json (Polish) 2026-02-11 10:23:00 -08:00
Owen Schwartz
e4bfbd267e New translations en-us.json (Dutch) 2026-02-11 10:23:00 -08:00
Owen Schwartz
65b4dcc672 New translations en-us.json (Korean) 2026-02-11 10:23:00 -08:00
Owen Schwartz
36fc30b524 New translations en-us.json (Italian) 2026-02-11 10:23:00 -08:00
Owen Schwartz
e724ed9137 New translations en-us.json (German) 2026-02-11 10:23:00 -08:00
Owen Schwartz
7ca992af05 New translations en-us.json (Czech) 2026-02-11 10:23:00 -08:00
Owen Schwartz
37f1c714ac New translations en-us.json (Bulgarian) 2026-02-11 10:23:00 -08:00
Owen Schwartz
397a43fb60 New translations en-us.json (Spanish) 2026-02-11 10:23:00 -08:00
Owen Schwartz
45e0a648c6 New translations en-us.json (French) 2026-02-11 10:23:00 -08:00
Owen
7336aa81d9 Update database build in dockerfile 2026-02-11 10:15:07 -08:00
miloschwartz
d727c10d98 link to billing page 2026-02-11 10:06:57 -08:00
Owen
321d77a317 Fix setting limits 2026-02-11 10:06:57 -08:00
miloschwartz
19b8a6b737 show required tier in paid features alert 2026-02-11 10:06:56 -08:00
Owen
f2e69dfb96 Add override for limits 2026-02-11 10:06:56 -08:00
miloschwartz
8207e49317 use purple banner for all paid features alert 2026-02-11 10:06:56 -08:00
miloschwartz
b75600b9ea add subscription violation banner 2026-02-11 10:06:56 -08:00
Owen
7b01f1bef6 Remove s3 bucket 2026-02-11 10:06:56 -08:00
Owen
e7bd2c0001 Allow cupons 2026-02-11 10:06:56 -08:00
Owen
a26076e9db Add final prices and fix logs 2026-02-11 10:06:56 -08:00
Owen
9711a0fb8e Continue to clean things up 2026-02-11 10:06:56 -08:00
Owen
accc670411 Communication improvements 2026-02-11 10:06:56 -08:00
Owen
071c41a54f Show warnings and specifics when downgrading 2026-02-11 10:06:56 -08:00
Owen
35ba6c19c3 Disable features when downgrading 2026-02-11 10:06:56 -08:00
miloschwartz
14c8348166 set default org mapping on create global idp 2026-02-11 10:06:56 -08:00
Owen
7d6ee72025 Finish adding limits checks to all put and post 2026-02-11 10:06:56 -08:00
Owen
ea0e770b57 Fix error response 2026-02-11 10:06:56 -08:00
Owen
193b7ff21e Adding limit checks 2026-02-11 10:06:55 -08:00
miloschwartz
d814ad9f3e add tier matrix to branding page 2026-02-11 10:06:55 -08:00
miloschwartz
da8b620c75 refactor and add tiers 2026-02-11 10:06:55 -08:00
miloschwartz
911b5e6814 refactor front end hooks 2026-02-11 10:06:55 -08:00
Owen
f991fd9c71 Fix to use the limits file 2026-02-11 10:06:55 -08:00
Owen
652e4c922d Comment out stripe usage reporting 2026-02-11 10:06:55 -08:00
miloschwartz
4364e3fbc1 fix some errors 2026-02-11 10:06:55 -08:00
Owen
a783fdecbc Fix errors 2026-02-11 10:06:55 -08:00
Owen
16f67455a2 Handle auto provisioning 2026-02-11 10:06:55 -08:00
Owen
0850a28d20 Add more tier matrix checks 2026-02-11 10:06:55 -08:00
miloschwartz
5ca598139e use pricing matrix in existing usePaidStatus funcitons 2026-02-11 10:06:55 -08:00
Owen
df1bf09163 Add pricing matrix 2026-02-11 10:06:55 -08:00
miloschwartz
50bc8d3e9c add rest of tier types 2026-02-11 10:06:55 -08:00
miloschwartz
86d089024e add tier type 2026-02-11 10:06:55 -08:00
Owen
d5c1cf594d Remove site kick 2026-02-11 10:06:54 -08:00
Owen
a0b5731e69 Getting swtiching tiers to work 2026-02-11 10:06:54 -08:00
miloschwartz
ceb359d614 refactor is licensed and subscribed util functions 2026-02-11 10:06:54 -08:00
miloschwartz
a49a9f8e3b dont fingerprint machine clients 2026-02-11 10:06:54 -08:00
miloschwartz
766606b08d use pangolin cli in machine client commands 2026-02-11 10:06:54 -08:00
miloschwartz
fed56c1959 show features in ce 2026-02-11 10:06:54 -08:00
miloschwartz
ae6ed8ad97 prefill username in login 2026-02-11 10:06:54 -08:00
Owen
c1ca0b8e2c Set version when creating sub 2026-02-11 10:06:54 -08:00
Owen
569dc735ce Rename tiers and get working 2026-02-11 10:06:54 -08:00
Owen
dd11c2c871 Dont accept invite if over the limits 2026-02-11 10:06:54 -08:00
Owen
8def4a2b68 Dont log to stripe 2026-02-11 10:06:54 -08:00
Owen
13a5f24b07 Dont write stripe to files anymore 2026-02-11 10:06:52 -08:00
Owen
0989d6353e Further billing 2026-02-11 10:06:03 -08:00
Owen
4139a7b73f Basic billing page is working 2026-02-11 10:06:03 -08:00
Owen
be60d66ce3 Switch to the new tier system and clean up checks 2026-02-11 10:06:03 -08:00
Owen
0a33043874 Switching to new pricing - remove old feature tracking 2026-02-11 10:06:03 -08:00
Owen
96d1d983e5 Wrap insert in transaction
Ref #2222
2026-02-11 10:06:03 -08:00
Owen
7ffb260d7c Change features, remove site uptime 2026-02-11 10:05:53 -08:00
miloschwartz
ce74489df5 link to billing page 2026-02-10 22:07:52 -08:00
Owen
342b188fae Fix setting limits 2026-02-10 21:54:26 -08:00
miloschwartz
fa6fee7b55 show required tier in paid features alert 2026-02-10 21:33:05 -08:00
Owen
c53d5a4d7d Add override for limits 2026-02-10 21:29:19 -08:00
miloschwartz
521e905724 use purple banner for all paid features alert 2026-02-10 21:21:20 -08:00
miloschwartz
4623090050 add subscription violation banner 2026-02-10 21:19:14 -08:00
Owen
dd9e5cc541 Remove s3 bucket 2026-02-10 21:14:14 -08:00
Owen
626be6a347 Allow cupons 2026-02-10 21:08:11 -08:00
Owen
56327ed503 Add final prices and fix logs 2026-02-10 20:42:34 -08:00
Fred KISSIE
6d1665004b 🏷️ fix type errors 2026-02-11 04:34:53 +01:00
Fred KISSIE
59b8119fbd Merge branch 'dev' into refactor/paginated-tables 2026-02-11 04:12:40 +01:00
Owen
9ff863db5e Continue to clean things up 2026-02-10 18:30:01 -08:00
Owen
e2ac6e6d4d Communication improvements 2026-02-10 17:04:22 -08:00
Owen
df4101875a Show warnings and specifics when downgrading 2026-02-10 16:35:09 -08:00
Owen
3f5c788d48 Disable features when downgrading 2026-02-10 16:11:19 -08:00
Fred KISSIE
45cd4df6e5 ♻️ agent 2026-02-11 00:37:42 +01:00
miloschwartz
94ac3ec76e set default org mapping on create global idp 2026-02-10 10:58:01 -08:00
Owen
af7263a0b1 Finish adding limits checks to all put and post 2026-02-10 10:53:02 -08:00
Owen
035396f95c Fix error response 2026-02-10 10:53:02 -08:00
Owen
f318f6304b Adding limit checks 2026-02-10 10:53:02 -08:00
miloschwartz
9d0ff472e5 add tier matrix to branding page 2026-02-10 10:42:39 -08:00
miloschwartz
d27482e812 refactor and add tiers 2026-02-10 10:27:10 -08:00
Lokowitz
d5b6de70da update package and update dockerfile 2026-02-10 11:49:53 +00:00
miloschwartz
69c2212ea0 refactor front end hooks 2026-02-09 20:50:44 -08:00
Owen
10be9bcd56 Fix to use the limits file 2026-02-09 20:39:26 -08:00
Owen
f531def0d2 Comment out stripe usage reporting 2026-02-09 20:30:44 -08:00
miloschwartz
ed40eae655 fix some errors 2026-02-09 20:23:55 -08:00
Owen
ba5ae6ed04 Fix errors 2026-02-09 20:17:14 -08:00
Fred KISSIE
d6ade102dc filter & paginate on machine clients table 2026-02-10 05:14:37 +01:00
Owen
0a6301697e Handle auto provisioning 2026-02-09 20:11:24 -08:00
Owen
13b4fc6725 Add more tier matrix checks 2026-02-09 19:52:44 -08:00
Fred KISSIE
c94d246c24 ♻️ list machine query 2026-02-10 04:00:45 +01:00
Fred KISSIE
5b779ba9fe ♻️ refactor 2026-02-10 03:21:12 +01:00
Fred KISSIE
3ba2cb19a9 approval feed 2026-02-10 03:20:49 +01:00
miloschwartz
a095dddd01 use pricing matrix in existing usePaidStatus funcitons 2026-02-09 18:17:18 -08:00
Owen
1b5cfaa49b Add pricing matrix 2026-02-09 18:04:37 -08:00
miloschwartz
66f3fabbae add rest of tier types 2026-02-09 17:52:28 -08:00
miloschwartz
0be8fb7931 add tier type 2026-02-09 17:42:45 -08:00
Owen
431e6ffaae Remove site kick 2026-02-09 17:23:48 -08:00
Owen
7d8185e0ee Getting swtiching tiers to work 2026-02-09 17:05:14 -08:00
miloschwartz
dff45748bd refactor is licensed and subscribed util functions 2026-02-09 16:57:41 -08:00
Fred KISSIE
da514ef314 ♻️ refactor 2026-02-10 00:45:34 +01:00
Fred KISSIE
7f73cde794 ♻️ refetch approval count every 30s 2026-02-10 00:45:20 +01:00
Fred KISSIE
b0af0d9cd5 ♻️ keep previous data 2026-02-10 00:31:21 +01:00
miloschwartz
e6464929ff Merge branch 'dev' into new-pricing 2026-02-09 15:05:13 -08:00
miloschwartz
122053939d dont fingerprint machine clients 2026-02-09 14:41:40 -08:00
Lokowitz
8429197b07 fix .gitignore 2026-02-09 19:56:32 +00:00
Lokowitz
44f2081882 update packages 2026-02-09 17:08:59 +00:00
Owen
300b4a3706 Set version when creating sub 2026-02-08 17:56:50 -08:00
Owen
81ef2db7f8 Rename tiers and get working 2026-02-08 17:56:36 -08:00
Owen
c41e8be3e8 Dont accept invite if over the limits 2026-02-08 11:55:24 -08:00
Owen
41bab0ce0b Dont log to stripe 2026-02-08 11:13:09 -08:00
Owen
5f26b9eeea Merge branch 'k8s' into new-pricing 2026-02-08 11:08:51 -08:00
Owen
1cca69ad23 Further billing 2026-02-08 11:08:23 -08:00
miloschwartz
410ed3949b use pangolin cli in machine client commands 2026-02-07 17:13:55 -08:00
miloschwartz
efc6ef3075 show features in ce 2026-02-07 17:00:44 -08:00
Lokowitz
63f7dd1d20 fix lint error 2026-02-07 08:48:58 +00:00
Lokowitz
57b8c69983 remove date-fns 2026-02-07 08:34:56 +00:00
Lokowitz
aad060810a update package and move eslint to dev 2026-02-07 08:26:05 +00:00
Lokowitz
9222b00a6f Merge remote-tracking branch 'origin/dev' into update-packages 2026-02-07 08:14:16 +00:00
Fred KISSIE
ff61b22e7e ♻️do not set default values 2026-02-07 05:37:52 +01:00
Fred KISSIE
577cb91343 whole table filter 2026-02-07 05:37:01 +01:00
Fred KISSIE
1889386f64 🚧 wip: table filters 2026-02-07 04:51:37 +01:00
Fred KISSIE
5d7f082ebf sort user device table & refactor sort into common functino 2026-02-07 04:41:42 +01:00
Fred KISSIE
db6327c4ff 🔇 remove console.logs in API 2026-02-07 02:52:23 +01:00
Fred KISSIE
fd7f6b2b99 filter user devices API finished 2026-02-07 02:51:32 +01:00
Fred KISSIE
49435398a8 🔥 cleanup imports 2026-02-07 02:50:59 +01:00
Owen
e101ac341b Basic billing page is working 2026-02-06 17:41:20 -08:00
Owen
6cfc7b7c69 Switch to the new tier system and clean up checks 2026-02-06 16:27:31 -08:00
Owen
313acabc86 Wrap insert in transaction
Ref #2222
2026-02-06 10:48:18 -08:00
Owen
34cced872f Switching to new pricing - remove old feature tracking 2026-02-06 10:47:43 -08:00
Owen
ac09e3aaf9 Wrap insert in transaction
Ref #2222
2026-02-06 10:47:19 -08:00
Fred KISSIE
9f2fd34e99 🚧 wip: user devices endpoint 2026-02-06 05:37:44 +01:00
Fred KISSIE
67b63d3084 ♻️ make code cleanrer 2026-02-06 04:52:21 +01:00
Fred KISSIE
4a31a7b84b 🚨 fix lint error 2026-02-06 03:55:11 +01:00
Fred KISSIE
538b601b1e Merge branch 'dev' into refactor/paginated-tables 2026-02-06 03:54:50 +01:00
Fred KISSIE
588f064c25 🚸 make resource enabled switch optimistic 2026-02-06 03:53:14 +01:00
Fred KISSIE
d521e79662 🏷️ fix types 2026-02-06 03:21:00 +01:00
Fred KISSIE
ccddb9244d 🏷️ add types on mode in sqlite 2026-02-06 03:14:03 +01:00
Fred KISSIE
0547396213 ♻️ do not sort client resources 2026-02-06 02:44:23 +01:00
Fred KISSIE
6c85171091 serverside filter+paginate client resources table 2026-02-06 02:42:15 +01:00
miloschwartz
a8f6b6c1da prefill username in login 2026-02-05 16:55:00 -08:00
Owen
f899326189 Change features, remove site uptime 2026-02-05 14:56:07 -08:00
Lokowitz
0f4d1d2a74 add preview server 2026-02-05 19:46:57 +00:00
Lokowitz
941d5c08e3 upgrade packages 2026-02-05 19:26:36 +00:00
Lokowitz
db9f74158b Merge remote-tracking branch 'origin/dev' into update-packages 2026-02-05 19:24:12 +00:00
Fred KISSIE
609ffccd67 🏷️ fix typescript error 2026-02-05 05:35:59 +01:00
Fred KISSIE
748af1d8cb ♻️ cleanup code for searching & filtering 2026-02-05 05:21:25 +01:00
Fred KISSIE
d309ec249e filter resources by status 2026-02-05 03:15:18 +01:00
Fred KISSIE
67949b4968 🚧 wip: healthStatus 2026-02-04 04:10:08 +01:00
Fred KISSIE
1fc40b3017 filter by auth state 2026-02-04 03:42:05 +01:00
Fred KISSIE
bb1a375484 paginate, search & filter resources by enabled 2026-02-04 02:20:28 +01:00
Lokowitz
13c011895d update packages and node 2026-02-02 19:17:40 +00:00
Lokowitz
bd8d0e3392 update packages 2026-02-02 18:48:35 +00:00
Fred KISSIE
cda6b67bef search, filter & paginate sites table 2026-01-31 03:02:39 +01:00
Fred KISSIE
066305b095 toggle column sorting & pagination 2026-01-31 00:45:14 +01:00
Owen
f2ba4b270f Dont write stripe to files anymore 2026-01-29 20:56:46 -08:00
Fred KISSIE
89695df012 🚧 wip: pagination and search work 2026-01-30 05:39:01 +01:00
Fred KISSIE
b04385a340 🚧 search on table 2026-01-29 05:48:41 +01:00
Fred KISSIE
d374ea6ea6 🚧wip 2026-01-29 05:07:41 +01:00
Fred KISSIE
01a2820390 🚧 POC: pagination in sites table 2026-01-29 05:07:27 +01:00
Fred KISSIE
c89c1a03da 🎨 use prettier for formatting typescript 2026-01-29 05:05:34 +01:00
Fred KISSIE
38ac4c5980 🚧 wip: paginated tables 2026-01-28 04:46:54 +01:00
Fred KISSIE
ed3ee64e4b support pathname in logo URL in branding page 2026-01-28 03:04:12 +01:00
275 changed files with 15415 additions and 8213 deletions

View File

@@ -32,3 +32,5 @@ migrations/
config/ config/
build.ts build.ts
tsconfig.json tsconfig.json
Dockerfile*
migrations/

View File

@@ -1,4 +1,4 @@
name: CI/CD Pipeline name: Public CICD Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries. # CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events. # Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
@@ -440,6 +440,10 @@ jobs:
issuer="https://token.actions.githubusercontent.com" issuer="https://token.actions.githubusercontent.com"
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs) id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
# Track failures
FAILED_TAGS=()
SUCCESSFUL_TAGS=()
# Determine if this is an RC release # Determine if this is an RC release
IS_RC="false" IS_RC="false"
if [[ "$TAG" == *"-rc."* ]]; then if [[ "$TAG" == *"-rc."* ]]; then
@@ -471,7 +475,11 @@ jobs:
for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}" echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}"
TAG_FAILED=false
# Wrap the entire tag processing in error handling
(
set -e
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')" DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
REF="${BASE_IMAGE}@${DIGEST}" REF="${BASE_IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}" echo "Resolved digest: ${REF}"
@@ -517,48 +525,42 @@ jobs:
VERIFIED_INDEX_KEYLESS=false VERIFIED_INDEX_KEYLESS=false
fi fi
# If index verification fails, attempt to verify child platform manifests # Check if verification succeeded
if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}" echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
CHILD_VERIFIED=false echo "This may be due to registry propagation delays. Continuing anyway."
fi
) || TAG_FAILED=true
for ARCH in arm64 amd64; do if [ "$TAG_FAILED" = "true" ]; then
CHILD_TAG="${IMAGE_TAG}-${ARCH}" echo "⚠️ WARNING: Failed to sign/verify ${BASE_IMAGE}:${IMAGE_TAG}"
echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}" FAILED_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
echo "==> cosign verify (public key) child ${CHILD_REF}"
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
CHILD_VERIFIED=true
echo "Public key verification succeeded for child ${CHILD_REF}"
else else
echo "Public key verification failed for child ${CHILD_REF}"
fi
echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
CHILD_VERIFIED=true
echo "Keyless verification succeeded for child ${CHILD_REF}"
else
echo "Keyless verification failed for child ${CHILD_REF}"
fi
else
echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
fi
done
if [ "${CHILD_VERIFIED}" != "true" ]; then
echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
exit 10
fi
fi
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}" echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
SUCCESSFUL_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
fi
done done
done done
echo "All images signed and verified successfully!" # Report summary
echo ""
echo "=========================================="
echo "Sign and Verify Summary"
echo "=========================================="
echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
echo "Failed: ${#FAILED_TAGS[@]}"
echo ""
if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
echo "Failed tags:"
for tag in "${FAILED_TAGS[@]}"; do
echo " - $tag"
done
echo ""
echo "⚠️ WARNING: Some tags failed to sign/verify, but continuing anyway"
else
echo "✓ All images signed and verified successfully!"
fi
shell: bash shell: bash
post-run: post-run:

View File

@@ -1,426 +0,0 @@
name: CI/CD Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
permissions:
contents: read
packages: write # for GHCR push
id-token: write # for Cosign Keyless (OIDC) Signing
# Required secrets:
# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN: push to Docker Hub
# - GITHUB_TOKEN: used for GHCR login and OIDC keyless signing
# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY: for key-based signing
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
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:
# 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 - 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@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version: 1.24
- 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: Pull latest Gerbil version
id: get-gerbil-tag
run: |
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
echo "LATEST_GERBIL_TAG=$LATEST_TAG" >> $GITHUB_ENV
shell: bash
- name: Pull latest Badger version
id: get-badger-tag
run: |
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
shell: bash
- name: Update install/main.go
run: |
PANGOLIN_VERSION=${{ env.TAG }}
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }}
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_VERSION\"/" install/main.go
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
cat install/main.go
shell: bash
- name: Build installer
working-directory: install
run: |
make go-build-release
- name: Upload artifacts from /install/bin
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: install-bin
path: install/bin/
- name: Install skopeo + jq
# skopeo: copy/inspect images between registries
# jq: JSON parsing tool used to extract digest values
run: |
sudo apt-get update -y
sudo apt-get install -y skopeo jq
skopeo --version
shell: bash
- name: Login to GHCR
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: |
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
shell: bash
- 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
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
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
- name: Dual-sign and verify (GHCR & Docker Hub)
# Sign each image by digest using keyless (OIDC) and key-based signing,
# then verify both the public key signature and the keyless OIDC signature.
env:
TAG: ${{ env.TAG }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
COSIGN_YES: "true"
run: |
set -euo pipefail
issuer="https://token.actions.githubusercontent.com"
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
echo "Processing ${IMAGE}:${TAG}"
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
REF="${IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}"
echo "==> cosign sign (keyless) --recursive ${REF}"
cosign sign --recursive "${REF}"
echo "==> cosign sign (key) --recursive ${REF}"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
echo "==> cosign verify (public key) ${REF}"
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
echo "==> cosign verify (keyless policy) ${REF}"
cosign verify \
--certificate-oidc-issuer "${issuer}" \
--certificate-identity-regexp "${id_regex}" \
"${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

@@ -1,4 +1,4 @@
name: CI/CD Pipeline name: SAAS Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries. # CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events. # Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.

View File

@@ -37,7 +37,7 @@ jobs:
run: npm run db:generate run: npm run db:generate
- name: Apply database migrations - name: Apply database migrations
run: npm run db:sqlite:push run: npm run db:push
- name: Test with tsc - name: Test with tsc
run: npx tsc --noEmit run: npx tsc --noEmit

2
.gitignore vendored
View File

@@ -52,3 +52,5 @@ scratch/
tsconfig.json tsconfig.json
hydrateSaas.ts hydrateSaas.ts
CLAUDE.md CLAUDE.md
drizzle.config.ts
server/setup/migrations.ts

View File

@@ -10,7 +10,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"

View File

@@ -1,33 +1,54 @@
FROM node:24-alpine AS builder FROM node:24-alpine AS base
WORKDIR /app WORKDIR /app
ARG BUILD=oss
ARG DATABASE=sqlite
RUN apk add --no-cache python3 make g++ RUN apk add --no-cache python3 make g++
# COPY package.json package-lock.json ./
COPY package*.json ./ COPY package*.json ./
FROM base AS builder-dev
RUN npm ci RUN npm ci
COPY . . COPY . .
ARG BUILD=oss
ARG DATABASE=sqlite
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \ RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
npm run set:$DATABASE && \ npm run set:$DATABASE && \
npm run set:$BUILD && \ npm run set:$BUILD && \
npm run db:generate && \ npm run db:generate && \
npm run build && \ npm run build && \
npm run build:cli npm run build:cli && \
test -f dist/server.mjs
# test to make sure the build output is there and error if not FROM base AS builder
RUN test -f dist/server.mjs
# Prune dev dependencies and clean up to prepare for copy to runner RUN npm ci --omit=dev
RUN npm prune --omit=dev && npm cache clean --force
FROM node:24-alpine AS runner FROM node:24-alpine AS runner
WORKDIR /app
RUN apk add --no-cache curl tzdata
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder-dev /app/.next/standalone ./
COPY --from=builder-dev /app/.next/static ./.next/static
COPY --from=builder-dev /app/dist ./dist
COPY --from=builder-dev /app/server/migrations ./dist/init
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
COPY server/db/names.json ./dist/names.json
COPY server/db/ios_models.json ./dist/ios_models.json
COPY server/db/mac_models.json ./dist/mac_models.json
COPY public ./public
# OCI Image Labels - Build Args for dynamic values # OCI Image Labels - Build Args for dynamic values
ARG VERSION="dev" ARG VERSION="dev"
ARG REVISION="" ARG REVISION=""
@@ -38,28 +59,6 @@ ARG LICENSE="AGPL-3.0"
ARG IMAGE_TITLE="Pangolin" ARG IMAGE_TITLE="Pangolin"
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
WORKDIR /app
# Only curl and tzdata needed at runtime - no build tools!
RUN apk add --no-cache curl tzdata
# 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/server/migrations ./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
COPY server/db/names.json ./dist/names.json
COPY server/db/ios_models.json ./dist/ios_models.json
COPY server/db/mac_models.json ./dist/mac_models.json
COPY public ./public
# OCI Image Labels # OCI Image Labels
# https://github.com/opencontainers/image-spec/blob/main/annotations.md # https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \ LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \

View File

@@ -1,7 +1,9 @@
FROM node:22-alpine FROM node:24-alpine
WORKDIR /app WORKDIR /app
RUN apk add --no-cache python3 make g++
COPY package*.json ./ COPY package*.json ./
# Install dependencies # Install dependencies

View File

@@ -7,8 +7,8 @@ services:
POSTGRES_DB: postgres # Default database name POSTGRES_DB: postgres # Default database name
POSTGRES_USER: postgres # Default user POSTGRES_USER: postgres # Default user
POSTGRES_PASSWORD: password # Default password (change for production!) POSTGRES_PASSWORD: password # Default password (change for production!)
volumes: # volumes:
- ./config/postgres:/var/lib/postgresql/data # - ./config/postgres:/var/lib/postgresql/data
ports: ports:
- "5432:5432" # Map host port 5432 to container port 5432 - "5432:5432" # Map host port 5432 to container port 5432
restart: no restart: no

View File

@@ -281,7 +281,7 @@ esbuild
}) })
], ],
sourcemap: "inline", sourcemap: "inline",
target: "node22" target: "node24"
}) })
.then((result) => { .then((result) => {
// Check if there were any errors in the build result // Check if there were any errors in the build result

View File

@@ -18,6 +18,8 @@
"componentsMember": "Вие сте част от {count, plural, =0 {нула организации} one {една организация} other {# организации}}.", "componentsMember": "Вие сте част от {count, plural, =0 {нула организации} one {една организация} other {# организации}}.",
"componentsInvalidKey": "Засечен е невалиден или изтекъл лиценз. Проверете лицензионните условия, за да се възползвате от всички функционалности.", "componentsInvalidKey": "Засечен е невалиден или изтекъл лиценз. Проверете лицензионните условия, за да се възползвате от всички функционалности.",
"dismiss": "Отхвърляне", "dismiss": "Отхвърляне",
"subscriptionViolationMessage": "Превишихте ограничението на текущия си план. Коригирайте проблема, като премахнете сайтове, потребители или други ресурси, за да оставате в рамките на плана си.",
"subscriptionViolationViewBilling": "Преглед на фактурирането",
"componentsLicenseViolation": "Нарушение на лиценза: Сървърът използва {usedSites} сайта, което надвишава лицензионния лимит от {maxSites} сайта. Проверете лицензионните условия, за да се възползвате от всички функционалности.", "componentsLicenseViolation": "Нарушение на лиценза: Сървърът използва {usedSites} сайта, което надвишава лицензионния лимит от {maxSites} сайта. Проверете лицензионните условия, за да се възползвате от всички функционалности.",
"componentsSupporterMessage": "Благодарим ви, че подкрепяте Pangolin като {tier}!", "componentsSupporterMessage": "Благодарим ви, че подкрепяте Pangolin като {tier}!",
"inviteErrorNotValid": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е приета или вече не е валидна.", "inviteErrorNotValid": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е приета или вече не е валидна.",
@@ -199,6 +201,7 @@
"protocolSelect": "Изберете протокол", "protocolSelect": "Изберете протокол",
"resourcePortNumber": "Номер на порт", "resourcePortNumber": "Номер на порт",
"resourcePortNumberDescription": "Външен номер на порт за прокси заявки.", "resourcePortNumberDescription": "Външен номер на порт за прокси заявки.",
"back": "Назад",
"cancel": "Отмяна", "cancel": "Отмяна",
"resourceConfig": "Конфигурационни фрагменти", "resourceConfig": "Конфигурационни фрагменти",
"resourceConfigDescription": "Копирайте и поставете тези конфигурационни отрязъци, за да настроите TCP/UDP ресурса", "resourceConfigDescription": "Копирайте и поставете тези конфигурационни отрязъци, за да настроите TCP/UDP ресурса",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Възникна грешка при изтриването на организацията.", "orgErrorDeleteMessage": "Възникна грешка при изтриването на организацията.",
"orgDeleted": "Организацията е изтрита", "orgDeleted": "Организацията е изтрита",
"orgDeletedMessage": "Организацията и нейните данни са изтрити.", "orgDeletedMessage": "Организацията и нейните данни са изтрити.",
"deleteAccount": "Изтриване на профил",
"deleteAccountDescription": "Перманентно изтрийте своя профил, всички организации, които притежавате, и всички данни в тези организации. Това не може да бъде отменено.",
"deleteAccountButton": "Изтриване на профил",
"deleteAccountConfirmTitle": "Изтрий профила",
"deleteAccountConfirmMessage": "Това ще изтрие перманентно вашия профил, всички организации, които притежавате, и всички данни в тези организации. Това не може да бъде отменено.",
"deleteAccountConfirmString": "изтриване на профил",
"deleteAccountSuccess": "Профилът е изтрит",
"deleteAccountSuccessMessage": "Вашият профил е изтрит.",
"deleteAccountError": "Неуспешно изтриване на профил",
"deleteAccountPreviewAccount": "Вашият профил",
"deleteAccountPreviewOrgs": "Организации, които притежавате (и всички техни данни)",
"orgMissing": "Липсва идентификатор на организация", "orgMissing": "Липсва идентификатор на организация",
"orgMissingMessage": "Невъзможност за регенериране на покана без идентификатор на организация.", "orgMissingMessage": "Невъзможност за регенериране на покана без идентификатор на организация.",
"accessUsersManage": "Управление на потребители", "accessUsersManage": "Управление на потребители",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Филтрирайте по състояние на одобрение", "filterByApprovalState": "Филтрирайте по състояние на одобрение",
"approvalListEmpty": "Няма одобрения", "approvalListEmpty": "Няма одобрения",
"approvalState": "Състояние на одобрение", "approvalState": "Състояние на одобрение",
"approvalLoadMore": "Заредете още",
"loadingApprovals": "Зарежда се одобрение",
"approve": "Одобряване", "approve": "Одобряване",
"approved": "Одобрен", "approved": "Одобрен",
"denied": "Отказан", "denied": "Отказан",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Увеличаване на броя на сайтовете", "sitestCountIncrease": "Увеличаване на броя на сайтовете",
"idpManage": "Управление на доставчици на идентичност", "idpManage": "Управление на доставчици на идентичност",
"idpManageDescription": "Прегледайте и управлявайте доставчици на идентичност в системата", "idpManageDescription": "Прегледайте и управлявайте доставчици на идентичност в системата",
"idpGlobalModeBanner": "Доставчиците на идентичност (IdPs) за всяка организация са деактивирани на този сървър. Използват се глобални IdPs (споделени между всички организации). Управлявайте глобалните IdPs в <adminPanelLink>администраторския панел</adminPanelLink>. За да активирате IdPs за всяка организация, редактирайте конфигурацията на сървъра и задайте режима на IdP към org. <configDocsLink>Вижте документацията</configDocsLink>. Ако желаете да продължите да използвате глобалните IdPs и да премахнете това от настройките на организацията, изрично задайте режима на global в конфигурацията.",
"idpGlobalModeBannerUpgradeRequired": "Доставчиците на идентичност (IdPs) за всяка организация са деактивирани на този сървър. Използват се глобални IdPs (споделени между всички организации). Управлявайте глобалните IdPs в <adminPanelLink>администраторския панел</adminPanelLink>. За да използвате доставчици на идентичност за всяка организация, трябва да надстроите до изданието Enterprise.",
"idpGlobalModeBannerLicenseRequired": "Доставчиците на идентичност (IdPs) за всяка организация са деактивирани на този сървър. Използват се глобални IdPs (споделени между всички организации). Управлявайте глобалните IdPs в <adminPanelLink>администраторския панел</adminPanelLink>. За да използвате доставчици на идентичност за всяка организация, е необходим лиценз за изданието Enterprise.",
"idpDeletedDescription": "Доставчик на идентичност успешно изтрит", "idpDeletedDescription": "Доставчик на идентичност успешно изтрит",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Сигурни ли сте, че искате да изтриете доставчика за идентичност?", "idpQuestionRemove": "Сигурни ли сте, че искате да изтриете доставчика за идентичност?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Настройка - Pangolin", "pangolinSetup": "Настройка - Pangolin",
"orgNameRequired": "Името на организацията е задължително", "orgNameRequired": "Името на организацията е задължително",
"orgIdRequired": "ID на организацията е задължително", "orgIdRequired": "ID на организацията е задължително",
"orgIdMaxLength": "ID на организация трябва да бъде най-много 32 символа",
"orgErrorCreate": "Възникна грешка при създаване на организация", "orgErrorCreate": "Възникна грешка при създаване на организация",
"pageNotFound": "Страницата не е намерена", "pageNotFound": "Страницата не е намерена",
"pageNotFoundDescription": "О, не! Страницата, която търсите, не съществува.", "pageNotFoundDescription": "О, не! Страницата, която търсите, не съществува.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Преглед на дневници", "actionViewLogs": "Преглед на дневници",
"noneSelected": "Нищо не е избрано", "noneSelected": "Нищо не е избрано",
"orgNotFound2": "Няма намерени организации.", "orgNotFound2": "Няма намерени организации.",
"searchProgress": "Търсене...", "searchPlaceholder": "Търсене...",
"emptySearchOptions": "Няма намерени опции",
"create": "Създаване", "create": "Създаване",
"orgs": "Организации", "orgs": "Организации",
"loginError": "Възникна неочаквана грешка. Моля, опитайте отново.", "loginError": "Възникна неочаквана грешка. Моля, опитайте отново.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Лог & Анализи", "sidebarLogAndAnalytics": "Лог & Анализи",
"sidebarBluePrints": "Чертежи", "sidebarBluePrints": "Чертежи",
"sidebarOrganization": "Организация", "sidebarOrganization": "Организация",
"sidebarBillingAndLicenses": "Фактуриране & Лицензи",
"sidebarLogsAnalytics": "Анализи", "sidebarLogsAnalytics": "Анализи",
"blueprints": "Чертежи", "blueprints": "Чертежи",
"blueprintsDescription": "Прилагайте декларативни конфигурации и преглеждайте предишни изпълнения", "blueprintsDescription": "Прилагайте декларативни конфигурации и преглеждайте предишни изпълнения",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Преглед на лимитите за използване", "billingUsageLimitsOverview": "Преглед на лимитите за използване",
"billingMonitorUsage": "Следете своята употреба спрямо конфигурираните лимити. Ако имате нужда от увеличаване на лимитите, моля свържете се с нас support@pangolin.net.", "billingMonitorUsage": "Следете своята употреба спрямо конфигурираните лимити. Ако имате нужда от увеличаване на лимитите, моля свържете се с нас support@pangolin.net.",
"billingDataUsage": "Използване на данни", "billingDataUsage": "Използване на данни",
"billingOnlineTime": "Време на работа на сайта", "billingSites": "Сайтове",
"billingUsers": "Активни потребители", "billingUsers": "Потребители",
"billingDomains": "Активни домейни", "billingDomains": "Домейни",
"billingRemoteExitNodes": "Активни самостоятелно хоствани възли", "billingOrganizations": "Организации",
"billingRemoteExitNodes": "Дистанционни възли",
"billingNoLimitConfigured": "Няма конфигуриран лимит", "billingNoLimitConfigured": "Няма конфигуриран лимит",
"billingEstimatedPeriod": "Очакван период на фактуриране", "billingEstimatedPeriod": "Очакван период на фактуриране",
"billingIncludedUsage": "Включено използване", "billingIncludedUsage": "Включено използване",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Неуспех при получаване на URL на портала", "billingFailedToGetPortalUrl": "Неуспех при получаване на URL на портала",
"billingPortalError": "Грешка в портала", "billingPortalError": "Грешка в портала",
"billingDataUsageInfo": "Таксува се за всички данни, прехвърляни през вашите защитени тунели, когато сте свързани към облака. Това включва както входящия, така и изходящия трафик за всички ваши сайтове. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Данните не се таксуват при използване на възли.", "billingDataUsageInfo": "Таксува се за всички данни, прехвърляни през вашите защитени тунели, когато сте свързани към облака. Това включва както входящия, така и изходящия трафик за всички ваши сайтове. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Данните не се таксуват при използване на възли.",
"billingOnlineTimeInfo": "Таксува се на база колко време вашите сайтове остават свързани с облака. Пример: 44,640 минути се равняват на един сайт работещ 24/7 за цял месец. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Времето не се таксува при използване на възли.", "billingSInfo": "Колко сайта можете да използвате",
"billingUsersInfo": "Таксува се всеки потребител в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните потребителски акаунти във вашата организация.", "billingUsersInfo": "Колко потребители можете да използвате",
"billingDomainInfo": "Таксува се всеки домейн в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните домейн акаунти във вашата организация.", "billingDomainInfo": "Колко домейни можете да използвате",
"billingRemoteExitNodesInfo": "Таксува се всеки управляван възел в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните управлявани възли във вашата организация.", "billingRemoteExitNodesInfo": "Колко дистанционни възли можете да използвате",
"billingLicenseKeys": "Лицензионни ключове",
"billingLicenseKeysDescription": "Управлявайте вашите абонаменти за лицензионни ключове",
"billingLicenseSubscription": "Абонамент за лиценз",
"billingInactive": "Неактивен",
"billingLicenseItem": "Лицензионен елемент",
"billingQuantity": "Количество",
"billingTotal": "общо",
"billingModifyLicenses": "Промяна на абонамента за лиценз",
"domainNotFound": "Домейнът не е намерен", "domainNotFound": "Домейнът не е намерен",
"domainNotFoundDescription": "Този ресурс е деактивиран, защото домейнът вече не съществува в нашата система. Моля, задайте нов домейн за този ресурс.", "domainNotFoundDescription": "Този ресурс е деактивиран, защото домейнът вече не съществува в нашата система. Моля, задайте нов домейн за този ресурс.",
"failed": "Неуспешно", "failed": "Неуспешно",
"createNewOrgDescription": "Създайте нова организация", "createNewOrgDescription": "Създайте нова организация",
"organization": "Организация", "organization": "Организация",
"primary": "Основно",
"port": "Порт", "port": "Порт",
"securityKeyManage": "Управление на ключове за защита", "securityKeyManage": "Управление на ключове за защита",
"securityKeyDescription": "Добавяне или премахване на ключове за защита за удостоверяване без парола", "securityKeyDescription": "Добавяне или премахване на ключове за защита за удостоверяване без парола",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Номерът на порта е задължителен за не-HTTP ресурси", "resourcePortRequired": "Номерът на порта е задължителен за не-HTTP ресурси",
"resourcePortNotAllowed": "Номерът на порта не трябва да бъде задаван за HTTP ресурси", "resourcePortNotAllowed": "Номерът на порта не трябва да бъде задаван за HTTP ресурси",
"billingPricingCalculatorLink": "Калкулатор на цените", "billingPricingCalculatorLink": "Калкулатор на цените",
"billingYourPlan": "Вашият план",
"billingViewOrModifyPlan": "Преглед или промяна на текущия ви план",
"billingViewPlanDetails": "Преглед на подробности за плана",
"billingUsageAndLimits": "Използване и граници",
"billingViewUsageAndLimits": "Преглед на ограниченията на плана и текущото използване",
"billingCurrentUsage": "Текущо използване",
"billingMaximumLimits": "Максимални граници",
"billingRemoteNodes": "Дистанционни възли",
"billingUnlimited": "Неограничено",
"billingPaidLicenseKeys": "Платени лицензионни ключове",
"billingManageLicenseSubscription": "Управлявайте абонамента си за платени самостоятелно хоствани лицензионни ключове",
"billingCurrentKeys": "Текущи ключове",
"billingModifyCurrentPlan": "Промяна на текущия план",
"billingConfirmUpgrade": "Потвърдете повишаването",
"billingConfirmDowngrade": "Потвърдете понижението",
"billingConfirmUpgradeDescription": "Предстои ви да повишите плана си. Прегледайте новите ограничения и цени по-долу.",
"billingConfirmDowngradeDescription": "Предстои ви да понижите плана си. Прегледайте новите ограничения и цени по-долу.",
"billingPlanIncludes": "Планът включва",
"billingProcessing": "Процесиране...",
"billingConfirmUpgradeButton": "Потвърдете повишаването",
"billingConfirmDowngradeButton": "Потвърдете понижението",
"billingLimitViolationWarning": "Използването надвишава новите планови ограничения",
"billingLimitViolationDescription": "Текущото ви използване надвишава ограниченията на този план. След понижаване, всички действия ще бъдат деактивирани, докато не намалите използването в рамките на новите ограничения. Моля, прегледайте по-долу функциите, които в момента са извън ограниченията. Ограничения в нарушение:",
"billingFeatureLossWarning": "Уведомление за наличност на функциите",
"billingFeatureLossDescription": "Чрез понижението на плана, функциите, недостъпни в новия план, ще бъдат автоматично деактивирани. Някои настройки и конфигурации може да бъдат загубени. Моля, прегледайте ценовата матрица, за да разберете кои функции вече няма да са на разположение.",
"billingUsageExceedsLimit": "Текущото използване ({current}) надвишава ограничението ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Съгласен съм с", "IAgreeToThe": "Съгласен съм с",
"termsOfService": "условията за ползване", "termsOfService": "условията за ползване",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Сигурни ли сте, че искате да премахнете брандинга за страниците за автентификация?", "authPageBrandingQuestionRemove": "Сигурни ли сте, че искате да премахнете брандинга за страниците за автентификация?",
"authPageBrandingDeleteConfirm": "Потвърждение на изтриване на брандинга.", "authPageBrandingDeleteConfirm": "Потвърждение на изтриване на брандинга.",
"brandingLogoURL": "URL адрес на логото.", "brandingLogoURL": "URL адрес на логото.",
"brandingLogoURLOrPath": "URL или Път към лого",
"brandingLogoPathDescription": "Въведете URL или локален път.",
"brandingLogoURLDescription": "Въведете публично достъпен URL към вашето лого изображение.",
"brandingPrimaryColor": "Основен цвят.", "brandingPrimaryColor": "Основен цвят.",
"brandingLogoWidth": "Ширина (px).", "brandingLogoWidth": "Ширина (px).",
"brandingLogoHeight": "Височина (px).", "brandingLogoHeight": "Височина (px).",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Назад към стандартния вход.", "orgAuthBackToSignIn": "Назад към стандартния вход.",
"orgAuthNoAccount": "Нямате профил?", "orgAuthNoAccount": "Нямате профил?",
"subscriptionRequiredToUse": "Необходим е абонамент, за да използвате тази функция.", "subscriptionRequiredToUse": "Необходим е абонамент, за да използвате тази функция.",
"mustUpgradeToUse": "Трябва да повишите своя абонамент, за да използвате тази функция.",
"subscriptionRequiredTierToUse": "Тази функция изисква <tierLink>{tier}</tierLink> или по-висок план.",
"upgradeToTierToUse": "Повишете до <tierLink>{tier}</tierLink> или по-висок план, за да използвате тази функция.",
"subscriptionTierTier1": "Домашен",
"subscriptionTierTier2": "Екип",
"subscriptionTierTier3": "Бизнес",
"subscriptionTierEnterprise": "Предприятие",
"idpDisabled": "Доставчиците на идентичност са деактивирани.", "idpDisabled": "Доставчиците на идентичност са деактивирани.",
"orgAuthPageDisabled": "Страницата за удостоверяване на организацията е деактивирана.", "orgAuthPageDisabled": "Страницата за удостоверяване на организацията е деактивирана.",
"domainRestartedDescription": "Проверка на домейна е рестартирана успешно", "domainRestartedDescription": "Проверка на домейна е рестартирана успешно",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Получаване на лиценз",
"description": "Изберете план и ни кажете как планирате да използвате Pangolin.",
"chooseTier": "Изберете вашия план",
"viewPricingLink": "Вижте цените, функциите и ограниченията",
"tiers": {
"starter": {
"title": "Стартов",
"description": "Предприятие, 25 потребители, 25 сайта и общностна поддръжка."
},
"scale": {
"title": "Скала",
"description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка."
}
},
"personalUseOnly": "Само за лична употреба (безплатен лиценз — без плащане)",
"buttons": {
"continueToCheckout": "Продължете към плащане"
},
"toasts": {
"checkoutError": {
"title": "Грешка при плащането",
"description": "Не можа да се започне плащането. Моля, опитайте отново."
}
}
},
"priority": "Приоритет", "priority": "Приоритет",
"priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.", "priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.",
"instanceName": "Име на инстанция", "instanceName": "Име на инстанция",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Край на следващата година", "logRetentionEndOfFollowingYear": "Край на следващата година",
"actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация", "actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация",
"accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация", "accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация",
"licenseRequiredToUse": "Необходим е лиценз Enterprise, за да се използва тази функция.", "licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink>, за да използвате тази функция. Тази функция е също достъпна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "Необходимо е <enterpriseEditionLink>изданието Enterprise</enterpriseEditionLink>, за да използвате тази функция. Тази функция е също достъпна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Решавач на сертификати", "certResolver": "Решавач на сертификати",
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.", "certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
"selectCertResolver": "Изберете решавач на сертификати", "selectCertResolver": "Изберете решавач на сертификати",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Активирана защитна стена.", "firewallEnabled": "Активирана защитна стена.",
"autoUpdatesEnabled": "Активирани автоматични актуализации.", "autoUpdatesEnabled": "Активирани автоматични актуализации.",
"tpmAvailable": "TPM е на разположение.", "tpmAvailable": "TPM е на разположение.",
"windowsAntivirusEnabled": "Активирана антивирусна програма",
"macosSipEnabled": "Protection на системната цялост (SIP).", "macosSipEnabled": "Protection на системната цялост (SIP).",
"macosGatekeeperEnabled": "Gatekeeper.", "macosGatekeeperEnabled": "Gatekeeper.",
"macosFirewallStealthMode": "Скрит режим на защитната стена.", "macosFirewallStealthMode": "Скрит режим на защитната стена.",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Jste členem {count, plural, =0 {0 organizací} one {1 organizace} other {# organizací}}.", "componentsMember": "Jste členem {count, plural, =0 {0 organizací} one {1 organizace} other {# organizací}}.",
"componentsInvalidKey": "Byly nalezeny neplatné nebo propadlé licenční klíče. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.", "componentsInvalidKey": "Byly nalezeny neplatné nebo propadlé licenční klíče. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.",
"dismiss": "Zavřít", "dismiss": "Zavřít",
"subscriptionViolationMessage": "Jste za hranicemi vašeho aktuálního plánu. Opravte problém odstraněním webů, uživatelů nebo jiných zdrojů, abyste zůstali ve vašem tarifu.",
"subscriptionViolationViewBilling": "Zobrazit fakturaci",
"componentsLicenseViolation": "Porušení licenčních podmínek: Tento server používá {usedSites} stránek, což překračuje limit {maxSites} licencovaných stránek. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.", "componentsLicenseViolation": "Porušení licenčních podmínek: Tento server používá {usedSites} stránek, což překračuje limit {maxSites} licencovaných stránek. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.",
"componentsSupporterMessage": "Děkujeme, že podporujete Pangolin jako {tier}!", "componentsSupporterMessage": "Děkujeme, že podporujete Pangolin jako {tier}!",
"inviteErrorNotValid": "Je nám líto, ale vypadá to, že pozvánka, ke které se snažíte získat přístup, nebyla přijata nebo již není platná.", "inviteErrorNotValid": "Je nám líto, ale vypadá to, že pozvánka, ke které se snažíte získat přístup, nebyla přijata nebo již není platná.",
@@ -199,6 +201,7 @@
"protocolSelect": "Vybrat protokol", "protocolSelect": "Vybrat protokol",
"resourcePortNumber": "Číslo portu", "resourcePortNumber": "Číslo portu",
"resourcePortNumberDescription": "Externí port k požadavkům proxy serveru.", "resourcePortNumberDescription": "Externí port k požadavkům proxy serveru.",
"back": "Zpět",
"cancel": "Zrušit", "cancel": "Zrušit",
"resourceConfig": "Konfigurační snippety", "resourceConfig": "Konfigurační snippety",
"resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační textové bloky pro nastavení TCP/UDP zdroje", "resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační textové bloky pro nastavení TCP/UDP zdroje",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Došlo k chybě při odstraňování organizace.", "orgErrorDeleteMessage": "Došlo k chybě při odstraňování organizace.",
"orgDeleted": "Organizace odstraněna", "orgDeleted": "Organizace odstraněna",
"orgDeletedMessage": "Organizace a její data byla smazána.", "orgDeletedMessage": "Organizace a její data byla smazána.",
"deleteAccount": "Odstranit účet",
"deleteAccountDescription": "Trvale smazat svůj účet, všechny organizace, které vlastníte, a všechna data těchto organizací. Tuto akci nelze vrátit zpět.",
"deleteAccountButton": "Odstranit účet",
"deleteAccountConfirmTitle": "Odstranit účet",
"deleteAccountConfirmMessage": "Toto trvale vymaže váš účet, všechny organizace, které vlastníte, a všechna data v rámci těchto organizací. Tuto akci nelze vrátit zpět.",
"deleteAccountConfirmString": "smazat účet",
"deleteAccountSuccess": "Účet odstraněn",
"deleteAccountSuccessMessage": "Váš účet byl odstraněn.",
"deleteAccountError": "Nepodařilo se odstranit účet",
"deleteAccountPreviewAccount": "Váš účet",
"deleteAccountPreviewOrgs": "Organizace, které vlastníte (a všechny jejich údaje)",
"orgMissing": "Chybí ID organizace", "orgMissing": "Chybí ID organizace",
"orgMissingMessage": "Nelze obnovit pozvánku bez ID organizace.", "orgMissingMessage": "Nelze obnovit pozvánku bez ID organizace.",
"accessUsersManage": "Spravovat uživatele", "accessUsersManage": "Spravovat uživatele",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtrovat podle státu schválení", "filterByApprovalState": "Filtrovat podle státu schválení",
"approvalListEmpty": "Žádná schválení", "approvalListEmpty": "Žádná schválení",
"approvalState": "Země schválení", "approvalState": "Země schválení",
"approvalLoadMore": "Načíst více",
"loadingApprovals": "Načítání schválení",
"approve": "Schválit", "approve": "Schválit",
"approved": "Schváleno", "approved": "Schváleno",
"denied": "Zamítnuto", "denied": "Zamítnuto",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Zvýšit počet stránek", "sitestCountIncrease": "Zvýšit počet stránek",
"idpManage": "Spravovat poskytovatele identity", "idpManage": "Spravovat poskytovatele identity",
"idpManageDescription": "Zobrazit a spravovat poskytovatele identity v systému", "idpManageDescription": "Zobrazit a spravovat poskytovatele identity v systému",
"idpGlobalModeBanner": "Poskytovatelé identity (IdP) pro každou organizaci jsou na tomto serveru zakázáni. Používá globální IdP (sdílené napříč všemi organizacemi). Správa globálních IdP v <adminPanelLink>admin panelu</adminPanelLink>. Chcete-li povolit IdP pro každou organizaci, upravte konfiguraci serveru a nastavte IdP režim na org. <configDocsLink>Viz dokumentace</configDocsLink>. Pokud chcete pokračovat v používání globálních IdP a zmizet z nastavení organizace, explicitně nastavte režim na globální v konfiguraci.",
"idpGlobalModeBannerUpgradeRequired": "Poskytovatelé identity (IdP) pro každou organizaci jsou na tomto serveru zakázáni. Používá globální IdP (sdílené napříč všemi organizacemi). Spravujte globální IdP v <adminPanelLink>admin panelu</adminPanelLink>. Chcete-li použít poskytovatele identity pro každou organizaci, musíte přejít na Enterprise vydání.",
"idpGlobalModeBannerLicenseRequired": "Poskytovatelé identity (IdP) pro každou organizaci jsou na tomto serveru zakázáni. Používá globální IdP (sdílené napříč všemi organizacemi). Správa globálních IdP v <adminPanelLink>admin panelu</adminPanelLink>. Chcete-li použít poskytovatele identity pro každou organizaci, je vyžadována Enterprise licence.",
"idpDeletedDescription": "Poskytovatel identity byl úspěšně odstraněn", "idpDeletedDescription": "Poskytovatel identity byl úspěšně odstraněn",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Jste si jisti, že chcete trvale odstranit poskytovatele identity?", "idpQuestionRemove": "Jste si jisti, že chcete trvale odstranit poskytovatele identity?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Setup - Pangolin", "pangolinSetup": "Setup - Pangolin",
"orgNameRequired": "Je vyžadován název organizace", "orgNameRequired": "Je vyžadován název organizace",
"orgIdRequired": "Je vyžadováno ID organizace", "orgIdRequired": "Je vyžadováno ID organizace",
"orgIdMaxLength": "ID organizace musí mít nejvýše 32 znaků",
"orgErrorCreate": "Při vytváření org došlo k chybě", "orgErrorCreate": "Při vytváření org došlo k chybě",
"pageNotFound": "Stránka nenalezena", "pageNotFound": "Stránka nenalezena",
"pageNotFoundDescription": "Jejda! Stránka, kterou hledáte, neexistuje.", "pageNotFoundDescription": "Jejda! Stránka, kterou hledáte, neexistuje.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Zobrazit logy", "actionViewLogs": "Zobrazit logy",
"noneSelected": "Není vybráno", "noneSelected": "Není vybráno",
"orgNotFound2": "Nebyly nalezeny žádné organizace.", "orgNotFound2": "Nebyly nalezeny žádné organizace.",
"searchProgress": "Hledat...", "searchPlaceholder": "Hledat...",
"emptySearchOptions": "Nebyly nalezeny žádné možnosti",
"create": "Vytvořit", "create": "Vytvořit",
"orgs": "Organizace", "orgs": "Organizace",
"loginError": "Došlo k neočekávané chybě. Zkuste to prosím znovu.", "loginError": "Došlo k neočekávané chybě. Zkuste to prosím znovu.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Log & Analytics", "sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Plány", "sidebarBluePrints": "Plány",
"sidebarOrganization": "Organizace", "sidebarOrganization": "Organizace",
"sidebarBillingAndLicenses": "Fakturace a licence",
"sidebarLogsAnalytics": "Analytici", "sidebarLogsAnalytics": "Analytici",
"blueprints": "Plány", "blueprints": "Plány",
"blueprintsDescription": "Použít deklarativní konfigurace a zobrazit předchozí běhy", "blueprintsDescription": "Použít deklarativní konfigurace a zobrazit předchozí běhy",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Přehled omezení použití", "billingUsageLimitsOverview": "Přehled omezení použití",
"billingMonitorUsage": "Sledujte vaše využití pomocí nastavených limitů. Pokud potřebujete zvýšit limity, kontaktujte nás prosím support@pangolin.net.", "billingMonitorUsage": "Sledujte vaše využití pomocí nastavených limitů. Pokud potřebujete zvýšit limity, kontaktujte nás prosím support@pangolin.net.",
"billingDataUsage": "Využití dat", "billingDataUsage": "Využití dat",
"billingOnlineTime": "Stránka online čas", "billingSites": "Stránky",
"billingUsers": "Aktivní uživatelé", "billingUsers": "Uživatelé",
"billingDomains": "Aktivní domény", "billingDomains": "Domény",
"billingRemoteExitNodes": "Aktivní Samostatně hostované uzly", "billingOrganizations": "Tělo",
"billingRemoteExitNodes": "Vzdálené uzly",
"billingNoLimitConfigured": "Žádný limit nenastaven", "billingNoLimitConfigured": "Žádný limit nenastaven",
"billingEstimatedPeriod": "Odhadované období fakturace", "billingEstimatedPeriod": "Odhadované období fakturace",
"billingIncludedUsage": "Zahrnuto využití", "billingIncludedUsage": "Zahrnuto využití",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Nepodařilo se získat URL portálu", "billingFailedToGetPortalUrl": "Nepodařilo se získat URL portálu",
"billingPortalError": "Chyba portálu", "billingPortalError": "Chyba portálu",
"billingDataUsageInfo": "Pokud jste připojeni k cloudu, jsou vám účtována všechna data přenášená prostřednictvím zabezpečených tunelů. To zahrnuje příchozí i odchozí provoz na všech vašich stránkách. Jakmile dosáhnete svého limitu, vaše stránky se odpojí, dokud neaktualizujete svůj tarif nebo nezmenšíte jeho používání. Data nejsou nabírána při používání uzlů.", "billingDataUsageInfo": "Pokud jste připojeni k cloudu, jsou vám účtována všechna data přenášená prostřednictvím zabezpečených tunelů. To zahrnuje příchozí i odchozí provoz na všech vašich stránkách. Jakmile dosáhnete svého limitu, vaše stránky se odpojí, dokud neaktualizujete svůj tarif nebo nezmenšíte jeho používání. Data nejsou nabírána při používání uzlů.",
"billingOnlineTimeInfo": "Platíte na základě toho, jak dlouho budou vaše stránky připojeny k cloudu. Například, 44,640 minut se rovná jedné stránce 24/7 po celý měsíc. Jakmile dosáhnete svého limitu, vaše stránky se odpojí, dokud neaktualizujete svůj tarif nebo nezkrátíte jeho používání. Čas není vybírán při používání uzlů.", "billingSInfo": "Kolik stránek můžete použít",
"billingUsersInfo": "Každý uživatel v organizaci je účtován denně. Fakturace je počítána na základě počtu aktivních uživatelských účtů na Vašem org.", "billingUsersInfo": "Kolik uživatelů můžete použít",
"billingDomainInfo": "Objednávka je účtována za každou doménu v organizaci. Fakturace je počítána denně na základě počtu aktivních doménových účtů na Vašem org.", "billingDomainInfo": "Kolik domén můžete použít",
"billingRemoteExitNodesInfo": "Za každý spravovaný uzel v organizaci se vám účtuje denně. Fakturace je vypočítávána na základě počtu aktivních spravovaných uzlů ve vašem org.", "billingRemoteExitNodesInfo": "Kolik vzdálených uzlů můžete použít",
"billingLicenseKeys": "Licenční klíče",
"billingLicenseKeysDescription": "Spravovat předplatné licenčního klíče",
"billingLicenseSubscription": "Předplatné licence",
"billingInactive": "Neaktivní",
"billingLicenseItem": "Položka licence",
"billingQuantity": "Množství",
"billingTotal": "celkem",
"billingModifyLicenses": "Upravit předplatné licence",
"domainNotFound": "Doména nenalezena", "domainNotFound": "Doména nenalezena",
"domainNotFoundDescription": "Tento dokument je zakázán, protože doména již neexistuje náš systém. Nastavte prosím novou doménu pro tento dokument.", "domainNotFoundDescription": "Tento dokument je zakázán, protože doména již neexistuje náš systém. Nastavte prosím novou doménu pro tento dokument.",
"failed": "Selhalo", "failed": "Selhalo",
"createNewOrgDescription": "Vytvořit novou organizaci", "createNewOrgDescription": "Vytvořit novou organizaci",
"organization": "Organizace", "organization": "Organizace",
"primary": "Primární",
"port": "Přístav", "port": "Přístav",
"securityKeyManage": "Správa bezpečnostních klíčů", "securityKeyManage": "Správa bezpečnostních klíčů",
"securityKeyDescription": "Přidat nebo odebrat bezpečnostní klíče pro bezheslou autentizaci", "securityKeyDescription": "Přidat nebo odebrat bezpečnostní klíče pro bezheslou autentizaci",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Pro neHTTP zdroje je vyžadováno číslo portu", "resourcePortRequired": "Pro neHTTP zdroje je vyžadováno číslo portu",
"resourcePortNotAllowed": "Číslo portu by nemělo být nastaveno pro HTTP zdroje", "resourcePortNotAllowed": "Číslo portu by nemělo být nastaveno pro HTTP zdroje",
"billingPricingCalculatorLink": "Cenová kalkulačka", "billingPricingCalculatorLink": "Cenová kalkulačka",
"billingYourPlan": "Váš plán",
"billingViewOrModifyPlan": "Zobrazit nebo upravit aktuální tarif",
"billingViewPlanDetails": "Zobrazit detaily plánu",
"billingUsageAndLimits": "Limity a použití",
"billingViewUsageAndLimits": "Zobrazit limity vašeho plánu a aktuální využití",
"billingCurrentUsage": "Aktuální využití",
"billingMaximumLimits": "Maximální limity",
"billingRemoteNodes": "Vzdálené uzly",
"billingUnlimited": "Bez omezení",
"billingPaidLicenseKeys": "Placené licenční klíče",
"billingManageLicenseSubscription": "Spravujte své předplatné za placené samohostované licenční klíče",
"billingCurrentKeys": "Aktuální klíče",
"billingModifyCurrentPlan": "Upravit aktuální tarif",
"billingConfirmUpgrade": "Potvrdit aktualizaci",
"billingConfirmDowngrade": "Potvrdit downgrade",
"billingConfirmUpgradeDescription": "Chystáte se povýšit svůj tarif. Přečtěte si nové limity a ceny.",
"billingConfirmDowngradeDescription": "Chystáte se snížit svůj tarif. Přečtěte si nové limity a ceny níže.",
"billingPlanIncludes": "Plán zahrnuje",
"billingProcessing": "Zpracovávám...",
"billingConfirmUpgradeButton": "Potvrdit aktualizaci",
"billingConfirmDowngradeButton": "Potvrdit downgrade",
"billingLimitViolationWarning": "Využití překročilo limity nového plánu",
"billingLimitViolationDescription": "Vaše současné využití překračuje meze tohoto plánu. Po ponížení budou všechny akce zakázány, dokud nesnížíte využití v rámci nových limitů. Přečtěte si prosím níže uvedené funkce překračující limity. Limity při porušení:",
"billingFeatureLossWarning": "Upozornění na dostupnost funkce",
"billingFeatureLossDescription": "Po pomenutí budou funkce v novém plánu automaticky zakázány. Některá nastavení a konfigurace mohou být ztraceny. Zkontrolujte cenovou matrici, abyste pochopili, které funkce již nebudou k dispozici.",
"billingUsageExceedsLimit": "Aktuální využití ({current}) překračuje limit ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Souhlasím s", "IAgreeToThe": "Souhlasím s",
"termsOfService": "podmínky služby", "termsOfService": "podmínky služby",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Jste si jisti, že chcete odstranit branding autentizačních stránek?", "authPageBrandingQuestionRemove": "Jste si jisti, že chcete odstranit branding autentizačních stránek?",
"authPageBrandingDeleteConfirm": "Potvrzení odstranění brandingu", "authPageBrandingDeleteConfirm": "Potvrzení odstranění brandingu",
"brandingLogoURL": "URL loga", "brandingLogoURL": "URL loga",
"brandingLogoURLOrPath": "URL nebo cesta k logu",
"brandingLogoPathDescription": "Zadejte URL nebo místní cestu.",
"brandingLogoURLDescription": "Zadejte veřejně přístupnou adresu URL vašeho loga.",
"brandingPrimaryColor": "Primární barva", "brandingPrimaryColor": "Primární barva",
"brandingLogoWidth": "Šířka (px)", "brandingLogoWidth": "Šířka (px)",
"brandingLogoHeight": "Výška (px)", "brandingLogoHeight": "Výška (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Zpět ke standardnímu přihlášení", "orgAuthBackToSignIn": "Zpět ke standardnímu přihlášení",
"orgAuthNoAccount": "Nemáte účet?", "orgAuthNoAccount": "Nemáte účet?",
"subscriptionRequiredToUse": "Pro použití této funkce je vyžadováno předplatné.", "subscriptionRequiredToUse": "Pro použití této funkce je vyžadováno předplatné.",
"mustUpgradeToUse": "Pro použití této funkce musíte aktualizovat své předplatné.",
"subscriptionRequiredTierToUse": "Tato funkce vyžaduje <tierLink>{tier}</tierLink> nebo vyšší.",
"upgradeToTierToUse": "Pro použití této funkce upgradujte na <tierLink>{tier}</tierLink> nebo vyšší.",
"subscriptionTierTier1": "Domů",
"subscriptionTierTier2": "Tým",
"subscriptionTierTier3": "Podniky",
"subscriptionTierEnterprise": "Podniky",
"idpDisabled": "Poskytovatelé identit jsou zakázáni.", "idpDisabled": "Poskytovatelé identit jsou zakázáni.",
"orgAuthPageDisabled": "Ověřovací stránka organizace je zakázána.", "orgAuthPageDisabled": "Ověřovací stránka organizace je zakázána.",
"domainRestartedDescription": "Ověření domény bylo úspěšně restartováno", "domainRestartedDescription": "Ověření domény bylo úspěšně restartováno",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Získat licenci",
"description": "Vyberte si plán a řekněte nám, jak plánujete používat Pangolin.",
"chooseTier": "Vyberte si svůj plán",
"viewPricingLink": "Zobrazit ceny, funkce a limity",
"tiers": {
"starter": {
"title": "Počáteční",
"description": "Firemní funkce, 25 uživatelů, 25 stránek a komunitní podpory."
},
"scale": {
"title": "Měřítko",
"description": "Podnikové funkce, 50 uživatelů, 50 míst a prioritní podpory."
}
},
"personalUseOnly": "Pouze osobní použití (bezplatná licence bez platby)",
"buttons": {
"continueToCheckout": "Pokračovat do pokladny"
},
"toasts": {
"checkoutError": {
"title": "Chyba při objednávce",
"description": "Nelze spustit objednávku. Zkuste to prosím znovu."
}
}
},
"priority": "Priorita", "priority": "Priorita",
"priorityDescription": "Vyšší priorita je vyhodnocena jako první. Priorita = 100 znamená automatické řazení (rozhodnutí systému). Pro vynucení manuální priority použijte jiné číslo.", "priorityDescription": "Vyšší priorita je vyhodnocena jako první. Priorita = 100 znamená automatické řazení (rozhodnutí systému). Pro vynucení manuální priority použijte jiné číslo.",
"instanceName": "Název instance", "instanceName": "Název instance",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Konec následujícího roku", "logRetentionEndOfFollowingYear": "Konec následujícího roku",
"actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci", "actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci",
"accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci", "accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci",
"licenseRequiredToUse": "Pro použití této funkce je vyžadována licence pro podnikání.", "licenseRequiredToUse": "Pro použití této funkce je vyžadována licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> . Tato funkce je také dostupná v <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> je vyžadována pro použití této funkce. Tato funkce je také k dispozici v <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Oddělovač certifikátů", "certResolver": "Oddělovač certifikátů",
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.", "certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
"selectCertResolver": "Vyberte řešič certifikátů", "selectCertResolver": "Vyberte řešič certifikátů",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Firewall povolen", "firewallEnabled": "Firewall povolen",
"autoUpdatesEnabled": "Automatické aktualizace povoleny", "autoUpdatesEnabled": "Automatické aktualizace povoleny",
"tpmAvailable": "TPM k dispozici", "tpmAvailable": "TPM k dispozici",
"windowsAntivirusEnabled": "Antivirus povolen",
"macosSipEnabled": "Ochrana systémové integrity (SIP)", "macosSipEnabled": "Ochrana systémové integrity (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Režim neviditelnosti firewallu", "macosFirewallStealthMode": "Režim neviditelnosti firewallu",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.", "componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.",
"componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", "componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
"dismiss": "Verwerfen", "dismiss": "Verwerfen",
"subscriptionViolationMessage": "Sie überschreiten Ihre Grenzen für Ihr aktuelles Paket. Korrigieren Sie das Problem, indem Sie Webseiten, Benutzer oder andere Ressourcen entfernen, um in Ihrem Paket zu bleiben.",
"subscriptionViolationViewBilling": "Rechnung anzeigen",
"componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", "componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
"componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!", "componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!",
"inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.", "inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.",
@@ -199,6 +201,7 @@
"protocolSelect": "Wählen Sie ein Protokoll", "protocolSelect": "Wählen Sie ein Protokoll",
"resourcePortNumber": "Portnummer", "resourcePortNumber": "Portnummer",
"resourcePortNumberDescription": "Die externe Portnummer für Proxy-Anfragen.", "resourcePortNumberDescription": "Die externe Portnummer für Proxy-Anfragen.",
"back": "Zurück",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"resourceConfig": "Konfiguration Snippets", "resourceConfig": "Konfiguration Snippets",
"resourceConfigDescription": "Kopieren und fügen Sie diese Konfigurations-Snippets ein, um die TCP/UDP Ressource einzurichten", "resourceConfigDescription": "Kopieren und fügen Sie diese Konfigurations-Snippets ein, um die TCP/UDP Ressource einzurichten",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Beim Löschen der Organisation ist ein Fehler aufgetreten.", "orgErrorDeleteMessage": "Beim Löschen der Organisation ist ein Fehler aufgetreten.",
"orgDeleted": "Organisation gelöscht", "orgDeleted": "Organisation gelöscht",
"orgDeletedMessage": "Die Organisation und ihre Daten wurden gelöscht.", "orgDeletedMessage": "Die Organisation und ihre Daten wurden gelöscht.",
"deleteAccount": "Konto löschen",
"deleteAccountDescription": "Lösche dein Konto, alle Organisationen, die du besitzt, und alle Daten innerhalb dieser Organisationen. Dies kann nicht rückgängig gemacht werden.",
"deleteAccountButton": "Konto löschen",
"deleteAccountConfirmTitle": "Konto löschen",
"deleteAccountConfirmMessage": "Dies wird Ihr Konto dauerhaft löschen, alle Organisationen, die Sie besitzen, und alle Daten innerhalb dieser Organisationen. Dies kann nicht rückgängig gemacht werden.",
"deleteAccountConfirmString": "Konto löschen",
"deleteAccountSuccess": "Konto gelöscht",
"deleteAccountSuccessMessage": "Ihr Konto wurde gelöscht.",
"deleteAccountError": "Konto konnte nicht gelöscht werden",
"deleteAccountPreviewAccount": "Ihr Konto",
"deleteAccountPreviewOrgs": "Organisationen, die Sie besitzen (und ihre Daten)",
"orgMissing": "Organisations-ID fehlt", "orgMissing": "Organisations-ID fehlt",
"orgMissingMessage": "Einladung kann ohne Organisations-ID nicht neu generiert werden.", "orgMissingMessage": "Einladung kann ohne Organisations-ID nicht neu generiert werden.",
"accessUsersManage": "Benutzer verwalten", "accessUsersManage": "Benutzer verwalten",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtern nach Genehmigungsstatus", "filterByApprovalState": "Filtern nach Genehmigungsstatus",
"approvalListEmpty": "Keine Genehmigungen", "approvalListEmpty": "Keine Genehmigungen",
"approvalState": "Genehmigungsstatus", "approvalState": "Genehmigungsstatus",
"approvalLoadMore": "Mehr laden",
"loadingApprovals": "Genehmigungen werden geladen",
"approve": "Bestätigen", "approve": "Bestätigen",
"approved": "Genehmigt", "approved": "Genehmigt",
"denied": "Verweigert", "denied": "Verweigert",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Anzahl der Standorte erhöhen", "sitestCountIncrease": "Anzahl der Standorte erhöhen",
"idpManage": "Identitätsanbieter verwalten", "idpManage": "Identitätsanbieter verwalten",
"idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten", "idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten",
"idpGlobalModeBanner": "Identitätsanbieter (IdPs) pro Organisation sind auf diesem Server deaktiviert. Es verwendet globale IdPs (geteilt über alle Organisationen). Verwalten Sie globale IdPs im <adminPanelLink>Admin-Panel</adminPanelLink>. Um IdPs pro Organisation zu aktivieren, bearbeiten Sie die Server-Konfiguration und setzen Sie den IdP-Modus auf org. <configDocsLink>Siehe Dokumentation</configDocsLink>. Wenn Sie weiterhin globale IdPs verwenden und diese in den Organisationseinstellungen verschwinden lassen wollen, setzen Sie den Modus explizit auf global in der Konfiguration.",
"idpGlobalModeBannerUpgradeRequired": "Identitätsanbieter (IdPs) pro Organisation sind auf diesem Server deaktiviert. Es verwendet globale IdPs (geteilt in allen Organisationen). Globale IdPs im <adminPanelLink>Admin-Panel</adminPanelLink>verwalten. Um Identitätsanbieter pro Organisation nutzen zu können, müssen Sie zur Enterprise Edition upgraden.",
"idpGlobalModeBannerLicenseRequired": "Identitätsanbieter (IdPs) pro Organisation sind auf diesem Server deaktiviert. Es verwendet globale IdPs (geteilt in allen Organisationen). Globale IdPs im <adminPanelLink>Admin-Panel</adminPanelLink>verwalten. Um Identitätsanbieter pro Organisation zu verwenden, ist eine Enterprise-Lizenz erforderlich.",
"idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht", "idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Sind Sie sicher, dass Sie den Identitätsanbieter dauerhaft löschen möchten?", "idpQuestionRemove": "Sind Sie sicher, dass Sie den Identitätsanbieter dauerhaft löschen möchten?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Einrichtung - Pangolin", "pangolinSetup": "Einrichtung - Pangolin",
"orgNameRequired": "Organisationsname ist erforderlich", "orgNameRequired": "Organisationsname ist erforderlich",
"orgIdRequired": "Organisations-ID ist erforderlich", "orgIdRequired": "Organisations-ID ist erforderlich",
"orgIdMaxLength": "Organisations-ID darf höchstens 32 Zeichen lang sein",
"orgErrorCreate": "Beim Erstellen der Organisation ist ein Fehler aufgetreten", "orgErrorCreate": "Beim Erstellen der Organisation ist ein Fehler aufgetreten",
"pageNotFound": "Seite nicht gefunden", "pageNotFound": "Seite nicht gefunden",
"pageNotFoundDescription": "Hoppla! Die gesuchte Seite existiert nicht.", "pageNotFoundDescription": "Hoppla! Die gesuchte Seite existiert nicht.",
@@ -1147,9 +1167,9 @@
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren", "actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
"actionCreateClient": "Client erstellen", "actionCreateClient": "Client erstellen",
"actionDeleteClient": "Client löschen", "actionDeleteClient": "Client löschen",
"actionArchiveClient": "Kunde archivieren", "actionArchiveClient": "Client archivieren",
"actionUnarchiveClient": "Client dearchivieren", "actionUnarchiveClient": "Client dearchivieren",
"actionBlockClient": "Klient sperren", "actionBlockClient": "Client sperren",
"actionUnblockClient": "Client entsperren", "actionUnblockClient": "Client entsperren",
"actionUpdateClient": "Client aktualisieren", "actionUpdateClient": "Client aktualisieren",
"actionListClients": "Clients auflisten", "actionListClients": "Clients auflisten",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Logs anzeigen", "actionViewLogs": "Logs anzeigen",
"noneSelected": "Keine ausgewählt", "noneSelected": "Keine ausgewählt",
"orgNotFound2": "Keine Organisationen gefunden.", "orgNotFound2": "Keine Organisationen gefunden.",
"searchProgress": "Suche...", "searchPlaceholder": "Suche...",
"emptySearchOptions": "Keine Optionen gefunden",
"create": "Erstellen", "create": "Erstellen",
"orgs": "Organisationen", "orgs": "Organisationen",
"loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.", "loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Log & Analytik", "sidebarLogAndAnalytics": "Log & Analytik",
"sidebarBluePrints": "Blaupausen", "sidebarBluePrints": "Blaupausen",
"sidebarOrganization": "Organisation", "sidebarOrganization": "Organisation",
"sidebarBillingAndLicenses": "Abrechnung & Lizenzen",
"sidebarLogsAnalytics": "Analytik", "sidebarLogsAnalytics": "Analytik",
"blueprints": "Blaupausen", "blueprints": "Blaupausen",
"blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen", "blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Übersicht über Nutzungsgrenzen", "billingUsageLimitsOverview": "Übersicht über Nutzungsgrenzen",
"billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@pangolin.net.", "billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@pangolin.net.",
"billingDataUsage": "Datenverbrauch", "billingDataUsage": "Datenverbrauch",
"billingOnlineTime": "Online-Zeit der Seite", "billingSites": "Seiten",
"billingUsers": "Aktive Benutzer", "billingUsers": "Benutzergeräte",
"billingDomains": "Aktive Domains", "billingDomains": "Domänen",
"billingRemoteExitNodes": "Aktive selbstgehostete Nodes", "billingOrganizations": "Orden",
"billingRemoteExitNodes": "Entfernte Knoten",
"billingNoLimitConfigured": "Kein Limit konfiguriert", "billingNoLimitConfigured": "Kein Limit konfiguriert",
"billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum", "billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum",
"billingIncludedUsage": "Inklusive Nutzung", "billingIncludedUsage": "Inklusive Nutzung",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Fehler beim Abrufen der Portal-URL", "billingFailedToGetPortalUrl": "Fehler beim Abrufen der Portal-URL",
"billingPortalError": "Portalfehler", "billingPortalError": "Portalfehler",
"billingDataUsageInfo": "Wenn Sie mit der Cloud verbunden sind, werden alle Daten über Ihre sicheren Tunnel belastet. Dies schließt eingehenden und ausgehenden Datenverkehr über alle Ihre Websites ein. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Daten werden nicht belastet, wenn Sie Knoten verwenden.", "billingDataUsageInfo": "Wenn Sie mit der Cloud verbunden sind, werden alle Daten über Ihre sicheren Tunnel belastet. Dies schließt eingehenden und ausgehenden Datenverkehr über alle Ihre Websites ein. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Daten werden nicht belastet, wenn Sie Knoten verwenden.",
"billingOnlineTimeInfo": "Sie werden belastet, abhängig davon, wie lange Ihre Seiten mit der Cloud verbunden bleiben. Zum Beispiel 44.640 Minuten entspricht einer Site, die 24 Stunden am Tag des Monats läuft. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Die Zeit wird nicht belastet, wenn Sie Knoten verwenden.", "billingSInfo": "Anzahl der Sites die Sie verwenden können",
"billingUsersInfo": "Sie werden für jeden Benutzer in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven Benutzerkonten in Ihrer Org berechnet.", "billingUsersInfo": "Wie viele Benutzer Sie verwenden können",
"billingDomainInfo": "Sie werden für jede Domain in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven Domain-Konten in Ihrer Org berechnet.", "billingDomainInfo": "Wie viele Domains Sie verwenden können",
"billingRemoteExitNodesInfo": "Sie werden für jeden verwalteten Knoten in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven verwalteten Knoten in Ihrer Org berechnet.", "billingRemoteExitNodesInfo": "Wie viele entfernte Knoten Sie verwenden können",
"billingLicenseKeys": "Lizenzschlüssel",
"billingLicenseKeysDescription": "Verwalten Sie Ihre Lizenzschlüssel Abonnements",
"billingLicenseSubscription": "Lizenzabonnement",
"billingInactive": "Inaktiv",
"billingLicenseItem": "Lizenz-Element",
"billingQuantity": "Menge",
"billingTotal": "gesamt",
"billingModifyLicenses": "Lizenzabonnement ändern",
"domainNotFound": "Domain nicht gefunden", "domainNotFound": "Domain nicht gefunden",
"domainNotFoundDescription": "Diese Ressource ist deaktiviert, weil die Domain nicht mehr in unserem System existiert. Bitte setzen Sie eine neue Domain für diese Ressource.", "domainNotFoundDescription": "Diese Ressource ist deaktiviert, weil die Domain nicht mehr in unserem System existiert. Bitte setzen Sie eine neue Domain für diese Ressource.",
"failed": "Fehlgeschlagen", "failed": "Fehlgeschlagen",
"createNewOrgDescription": "Eine neue Organisation erstellen", "createNewOrgDescription": "Eine neue Organisation erstellen",
"organization": "Organisation", "organization": "Organisation",
"primary": "Primär",
"port": "Port", "port": "Port",
"securityKeyManage": "Sicherheitsschlüssel verwalten", "securityKeyManage": "Sicherheitsschlüssel verwalten",
"securityKeyDescription": "Sicherheitsschlüssel für passwortlose Authentifizierung hinzufügen oder entfernen", "securityKeyDescription": "Sicherheitsschlüssel für passwortlose Authentifizierung hinzufügen oder entfernen",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich", "resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich",
"resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden", "resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden",
"billingPricingCalculatorLink": "Preisrechner", "billingPricingCalculatorLink": "Preisrechner",
"billingYourPlan": "Ihr Plan",
"billingViewOrModifyPlan": "Zeige oder ändere dein aktuelles Paket",
"billingViewPlanDetails": "Plan Details anzeigen",
"billingUsageAndLimits": "Nutzung und Einschränkungen",
"billingViewUsageAndLimits": "Schau dir die Grenzen und die aktuelle Nutzung deines Plans an",
"billingCurrentUsage": "Aktuelle Nutzung",
"billingMaximumLimits": "Maximale Grenzen",
"billingRemoteNodes": "Entfernte Knoten",
"billingUnlimited": "Unbegrenzt",
"billingPaidLicenseKeys": "Bezahlte Lizenzschlüssel",
"billingManageLicenseSubscription": "Verwalten Sie Ihr Abonnement für kostenpflichtige selbstgehostete Lizenzschlüssel",
"billingCurrentKeys": "Aktuelle Tasten",
"billingModifyCurrentPlan": "Aktuelles Paket ändern",
"billingConfirmUpgrade": "Upgrade bestätigen",
"billingConfirmDowngrade": "Downgrade bestätigen",
"billingConfirmUpgradeDescription": "Sie sind dabei, Ihr Paket zu aktualisieren. Schauen Sie sich die neuen Limits und Preise unten an.",
"billingConfirmDowngradeDescription": "Sie sind dabei, Ihren Plan herunterzustufen. Überprüfen Sie die neuen Limits und Preise unten.",
"billingPlanIncludes": "Plan beinhaltet",
"billingProcessing": "Verarbeitung...",
"billingConfirmUpgradeButton": "Upgrade bestätigen",
"billingConfirmDowngradeButton": "Downgrade bestätigen",
"billingLimitViolationWarning": "Nutzung überschreitet neue Plan-Grenzen",
"billingLimitViolationDescription": "Ihre aktuelle Nutzung überschreitet die Grenzen dieses Plans. Nach dem Downgrade werden alle Aktionen deaktiviert, bis Sie die Nutzung innerhalb der neuen Grenzen reduzieren. Bitte überprüfen Sie die Funktionen unten, die derzeit über den Grenzen liegen. Grenzwerte verletzen:",
"billingFeatureLossWarning": "Verfügbarkeitshinweis",
"billingFeatureLossDescription": "Durch Herabstufung werden Funktionen, die im neuen Paket nicht verfügbar sind, automatisch deaktiviert. Einige Einstellungen und Konfigurationen können verloren gehen. Bitte überprüfen Sie die Preismatrix um zu verstehen, welche Funktionen nicht mehr verfügbar sein werden.",
"billingUsageExceedsLimit": "Aktuelle Nutzung ({current}) überschreitet das Limit ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Ich stimme den", "IAgreeToThe": "Ich stimme den",
"termsOfService": "Nutzungsbedingungen zu", "termsOfService": "Nutzungsbedingungen zu",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?", "authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?",
"authPageBrandingDeleteConfirm": "Branding löschen bestätigen", "authPageBrandingDeleteConfirm": "Branding löschen bestätigen",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logo-URL oder Pfad",
"brandingLogoPathDescription": "Geben Sie eine URL oder einen lokalen Pfad ein.",
"brandingLogoURLDescription": "Geben Sie eine öffentlich zugängliche URL zu Ihrem Logobild ein.",
"brandingPrimaryColor": "Primär-Farbe", "brandingPrimaryColor": "Primär-Farbe",
"brandingLogoWidth": "Breite (px)", "brandingLogoWidth": "Breite (px)",
"brandingLogoHeight": "Höhe (px)", "brandingLogoHeight": "Höhe (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Zurück zum Standard Login", "orgAuthBackToSignIn": "Zurück zum Standard Login",
"orgAuthNoAccount": "Sie haben noch kein Konto?", "orgAuthNoAccount": "Sie haben noch kein Konto?",
"subscriptionRequiredToUse": "Um diese Funktion nutzen zu können, ist ein Abonnement erforderlich.", "subscriptionRequiredToUse": "Um diese Funktion nutzen zu können, ist ein Abonnement erforderlich.",
"mustUpgradeToUse": "Sie müssen Ihr Abonnement aktualisieren, um diese Funktion nutzen zu können.",
"subscriptionRequiredTierToUse": "Diese Funktion erfordert <tierLink>{tier}</tierLink> oder höher.",
"upgradeToTierToUse": "Upgrade auf <tierLink>{tier}</tierLink> oder höher, um diese Funktion zu nutzen.",
"subscriptionTierTier1": "Zuhause",
"subscriptionTierTier2": "Team",
"subscriptionTierTier3": "Geschäftlich",
"subscriptionTierEnterprise": "Firma",
"idpDisabled": "Identitätsanbieter sind deaktiviert.", "idpDisabled": "Identitätsanbieter sind deaktiviert.",
"orgAuthPageDisabled": "Organisations-Authentifizierungsseite ist deaktiviert.", "orgAuthPageDisabled": "Organisations-Authentifizierungsseite ist deaktiviert.",
"domainRestartedDescription": "Domain-Verifizierung erfolgreich neu gestartet", "domainRestartedDescription": "Domain-Verifizierung erfolgreich neu gestartet",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Lizenz erhalten",
"description": "Wählen Sie einen Plan und teilen Sie uns mit, wie Sie Pangolin verwenden möchten.",
"chooseTier": "Wählen Sie Ihren Plan",
"viewPricingLink": "Siehe Preise, Funktionen und Limits",
"tiers": {
"starter": {
"title": "Starter",
"description": "Enterprise Features, 25 Benutzer, 25 Sites und Community-Unterstützung."
},
"scale": {
"title": "Maßstab",
"description": "Enterprise Features, 50 Benutzer, 50 Sites und Prioritätsunterstützung."
}
},
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz — keine Kasse)",
"buttons": {
"continueToCheckout": "Weiter zur Kasse"
},
"toasts": {
"checkoutError": {
"title": "Checkout-Fehler",
"description": "Kasse konnte nicht gestartet werden. Bitte versuchen Sie es erneut."
}
}
},
"priority": "Priorität", "priority": "Priorität",
"priorityDescription": "Die Routen mit höherer Priorität werden zuerst ausgewertet. Priorität = 100 bedeutet automatische Bestellung (Systementscheidung). Verwenden Sie eine andere Nummer, um manuelle Priorität zu erzwingen.", "priorityDescription": "Die Routen mit höherer Priorität werden zuerst ausgewertet. Priorität = 100 bedeutet automatische Bestellung (Systementscheidung). Verwenden Sie eine andere Nummer, um manuelle Priorität zu erzwingen.",
"instanceName": "Instanzname", "instanceName": "Instanzname",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Ende des folgenden Jahres", "logRetentionEndOfFollowingYear": "Ende des folgenden Jahres",
"actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen", "actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen",
"accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen", "accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen",
"licenseRequiredToUse": "Um diese Funktion nutzen zu können, ist eine Enterprise-Lizenz erforderlich.", "licenseRequiredToUse": "Um diese Funktion nutzen zu können, ist eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz erforderlich. Diese Funktion ist auch in der <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> verfügbar.",
"ossEnterpriseEditionRequired": "Um diese Funktion nutzen zu können, ist die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> erforderlich. Diese Funktion ist auch in der <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> verfügbar.",
"certResolver": "Zertifikatsauflöser", "certResolver": "Zertifikatsauflöser",
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.", "certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
"selectCertResolver": "Zertifikatsauflöser auswählen", "selectCertResolver": "Zertifikatsauflöser auswählen",
@@ -2455,14 +2550,14 @@
"errorUnarchivingDevice": "Fehler beim Entarchivieren des Geräts", "errorUnarchivingDevice": "Fehler beim Entarchivieren des Geräts",
"failedToUnarchiveDevice": "Fehler beim Entfernen des Geräts", "failedToUnarchiveDevice": "Fehler beim Entfernen des Geräts",
"unarchive": "Archivieren", "unarchive": "Archivieren",
"archiveClient": "Kunde archivieren", "archiveClient": "Client archivieren",
"archiveClientQuestion": "Sind Sie sicher, dass Sie diesen Client archivieren möchten?", "archiveClientQuestion": "Sind Sie sicher, dass Sie diesen Client archivieren möchten?",
"archiveClientMessage": "Der Client wird archiviert und aus der Liste Ihrer aktiven Clients entfernt.", "archiveClientMessage": "Der Client wird archiviert und aus der Liste Ihrer aktiven Clients entfernt.",
"archiveClientConfirm": "Kunde archivieren", "archiveClientConfirm": "Client archivieren",
"blockClient": "Klient sperren", "blockClient": "Client sperren",
"blockClientQuestion": "Sind Sie sicher, dass Sie diesen Client blockieren möchten?", "blockClientQuestion": "Sind Sie sicher, dass Sie diesen Client blockieren möchten?",
"blockClientMessage": "Das Gerät wird gezwungen, die Verbindung zu trennen, wenn es gerade verbunden ist. Sie können das Gerät später entsperren.", "blockClientMessage": "Das Gerät wird gezwungen, die Verbindung zu trennen, wenn es gerade verbunden ist. Sie können das Gerät später entsperren.",
"blockClientConfirm": "Klient sperren", "blockClientConfirm": "Client sperren",
"active": "Aktiv", "active": "Aktiv",
"usernameOrEmail": "Benutzername oder E-Mail", "usernameOrEmail": "Benutzername oder E-Mail",
"selectYourOrganization": "Wählen Sie Ihre Organisation", "selectYourOrganization": "Wählen Sie Ihre Organisation",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Firewall aktiviert", "firewallEnabled": "Firewall aktiviert",
"autoUpdatesEnabled": "Automatische Updates aktiviert", "autoUpdatesEnabled": "Automatische Updates aktiviert",
"tpmAvailable": "TPM verfügbar", "tpmAvailable": "TPM verfügbar",
"windowsAntivirusEnabled": "Antivirus aktiviert",
"macosSipEnabled": "Schutz der Systemintegrität (SIP)", "macosSipEnabled": "Schutz der Systemintegrität (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Stealth-Modus", "macosFirewallStealthMode": "Firewall Stealth-Modus",

View File

@@ -18,6 +18,8 @@
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.", "componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.", "componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
"dismiss": "Dismiss", "dismiss": "Dismiss",
"subscriptionViolationMessage": "You're beyond your limits for your current plan. Correct the problem by removing sites, users, or other resources to stay within your plan.",
"subscriptionViolationViewBilling": "View billing",
"componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.", "componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.",
"componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!", "componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!",
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.", "inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
@@ -55,7 +57,7 @@
"siteDescription": "Create and manage sites to enable connectivity to private networks", "siteDescription": "Create and manage sites to enable connectivity to private networks",
"sitesBannerTitle": "Connect Any Network", "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.", "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", "sitesBannerButtonText": "Install Site Connector",
"approvalsBannerTitle": "Approve or Deny Device Access", "approvalsBannerTitle": "Approve or Deny Device Access",
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.", "approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
"approvalsBannerButtonText": "Learn More", "approvalsBannerButtonText": "Learn More",
@@ -79,8 +81,8 @@
"siteConfirmCopy": "I have copied the config", "siteConfirmCopy": "I have copied the config",
"searchSitesProgress": "Search sites...", "searchSitesProgress": "Search sites...",
"siteAdd": "Add Site", "siteAdd": "Add Site",
"siteInstallNewt": "Install Newt", "siteInstallNewt": "Install Site",
"siteInstallNewtDescription": "Get Newt running on your system", "siteInstallNewtDescription": "Install the site connector for your system",
"WgConfiguration": "WireGuard Configuration", "WgConfiguration": "WireGuard Configuration",
"WgConfigurationDescription": "Use the following configuration to connect to the network", "WgConfigurationDescription": "Use the following configuration to connect to the network",
"operatingSystem": "Operating System", "operatingSystem": "Operating System",
@@ -199,6 +201,7 @@
"protocolSelect": "Select a protocol", "protocolSelect": "Select a protocol",
"resourcePortNumber": "Port Number", "resourcePortNumber": "Port Number",
"resourcePortNumberDescription": "The external port number to proxy requests.", "resourcePortNumberDescription": "The external port number to proxy requests.",
"back": "Back",
"cancel": "Cancel", "cancel": "Cancel",
"resourceConfig": "Configuration Snippets", "resourceConfig": "Configuration Snippets",
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource", "resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "An error occurred while deleting the organization.", "orgErrorDeleteMessage": "An error occurred while deleting the organization.",
"orgDeleted": "Organization deleted", "orgDeleted": "Organization deleted",
"orgDeletedMessage": "The organization and its data has been deleted.", "orgDeletedMessage": "The organization and its data has been deleted.",
"deleteAccount": "Delete Account",
"deleteAccountDescription": "Permanently delete your account, all organizations you own, and all data within those organizations. This cannot be undone.",
"deleteAccountButton": "Delete Account",
"deleteAccountConfirmTitle": "Delete Account",
"deleteAccountConfirmMessage": "This will permanently wipe your account, all organizations you own, and all data within those organizations. This cannot be undone.",
"deleteAccountConfirmString": "delete account",
"deleteAccountSuccess": "Account Deleted",
"deleteAccountSuccessMessage": "Your account has been deleted.",
"deleteAccountError": "Failed to delete account",
"deleteAccountPreviewAccount": "Your Account",
"deleteAccountPreviewOrgs": "Organizations you own (and all their data)",
"orgMissing": "Organization ID Missing", "orgMissing": "Organization ID Missing",
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.", "orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
"accessUsersManage": "Manage Users", "accessUsersManage": "Manage Users",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filter By Approval State", "filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals", "approvalListEmpty": "No approvals",
"approvalState": "Approval State", "approvalState": "Approval State",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Approve", "approve": "Approve",
"approved": "Approved", "approved": "Approved",
"denied": "Denied", "denied": "Denied",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Increase site count", "sitestCountIncrease": "Increase site count",
"idpManage": "Manage Identity Providers", "idpManage": "Manage Identity Providers",
"idpManageDescription": "View and manage identity providers in the system", "idpManageDescription": "View and manage identity providers in the system",
"idpGlobalModeBanner": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To enable IdPs per organization, edit the server config and set IdP mode to org. <configDocsLink>See the docs</configDocsLink>. If you want to continue using global IdPs and make this disappear from the organization settings, explicitly set the mode to global in the config.",
"idpGlobalModeBannerUpgradeRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, you must upgrade to the Enterprise edition.",
"idpGlobalModeBannerLicenseRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, an Enterprise license is required.",
"idpDeletedDescription": "Identity provider deleted successfully", "idpDeletedDescription": "Identity provider deleted successfully",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?", "idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Setup - Pangolin", "pangolinSetup": "Setup - Pangolin",
"orgNameRequired": "Organization name is required", "orgNameRequired": "Organization name is required",
"orgIdRequired": "Organization ID is required", "orgIdRequired": "Organization ID is required",
"orgIdMaxLength": "Organization ID must be at most 32 characters",
"orgErrorCreate": "An error occurred while creating org", "orgErrorCreate": "An error occurred while creating org",
"pageNotFound": "Page Not Found", "pageNotFound": "Page Not Found",
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "View Logs", "actionViewLogs": "View Logs",
"noneSelected": "None selected", "noneSelected": "None selected",
"orgNotFound2": "No organizations found.", "orgNotFound2": "No organizations found.",
"searchProgress": "Search...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Create", "create": "Create",
"orgs": "Organizations", "orgs": "Organizations",
"loginError": "An unexpected error occurred. Please try again.", "loginError": "An unexpected error occurred. Please try again.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Log & Analytics", "sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blueprints", "sidebarBluePrints": "Blueprints",
"sidebarOrganization": "Organization", "sidebarOrganization": "Organization",
"sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics", "sidebarLogsAnalytics": "Analytics",
"blueprints": "Blueprints", "blueprints": "Blueprints",
"blueprintsDescription": "Apply declarative configurations and view previous runs", "blueprintsDescription": "Apply declarative configurations and view previous runs",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Usage Limits Overview", "billingUsageLimitsOverview": "Usage Limits Overview",
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.", "billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
"billingDataUsage": "Data Usage", "billingDataUsage": "Data Usage",
"billingOnlineTime": "Site Online Time", "billingSites": "Sites",
"billingUsers": "Active Users", "billingUsers": "Users",
"billingDomains": "Active Domains", "billingDomains": "Domains",
"billingRemoteExitNodes": "Active Self-hosted Nodes", "billingOrganizations": "Orgs",
"billingRemoteExitNodes": "Remote Nodes",
"billingNoLimitConfigured": "No limit configured", "billingNoLimitConfigured": "No limit configured",
"billingEstimatedPeriod": "Estimated Billing Period", "billingEstimatedPeriod": "Estimated Billing Period",
"billingIncludedUsage": "Included Usage", "billingIncludedUsage": "Included Usage",
@@ -1432,10 +1455,10 @@
"billingFailedToGetPortalUrl": "Failed to get portal URL", "billingFailedToGetPortalUrl": "Failed to get portal URL",
"billingPortalError": "Portal Error", "billingPortalError": "Portal Error",
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.", "billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.", "billingSInfo": "How many sites you can use",
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.", "billingUsersInfo": "How many users you can use",
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.", "billingDomainInfo": "How many domains you can use",
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.", "billingRemoteExitNodesInfo": "How many remote nodes you can use",
"billingLicenseKeys": "License Keys", "billingLicenseKeys": "License Keys",
"billingLicenseKeysDescription": "Manage your license key subscriptions", "billingLicenseKeysDescription": "Manage your license key subscriptions",
"billingLicenseSubscription": "License Subscription", "billingLicenseSubscription": "License Subscription",
@@ -1444,12 +1467,12 @@
"billingQuantity": "Quantity", "billingQuantity": "Quantity",
"billingTotal": "total", "billingTotal": "total",
"billingModifyLicenses": "Modify License Subscription", "billingModifyLicenses": "Modify License Subscription",
"billingPricingCalculatorLink": "View Pricing Calculator",
"domainNotFound": "Domain Not Found", "domainNotFound": "Domain Not Found",
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
"failed": "Failed", "failed": "Failed",
"createNewOrgDescription": "Create a new organization", "createNewOrgDescription": "Create a new organization",
"organization": "Organization", "organization": "Organization",
"primary": "Primary",
"port": "Port", "port": "Port",
"securityKeyManage": "Manage Security Keys", "securityKeyManage": "Manage Security Keys",
"securityKeyDescription": "Add or remove security keys for passwordless authentication", "securityKeyDescription": "Add or remove security keys for passwordless authentication",
@@ -1521,6 +1544,32 @@
"resourcePortRequired": "Port number is required for non-HTTP resources", "resourcePortRequired": "Port number is required for non-HTTP resources",
"resourcePortNotAllowed": "Port number should not be set for HTTP resources", "resourcePortNotAllowed": "Port number should not be set for HTTP resources",
"billingPricingCalculatorLink": "Pricing Calculator", "billingPricingCalculatorLink": "Pricing Calculator",
"billingYourPlan": "Your Plan",
"billingViewOrModifyPlan": "View or modify your current plan",
"billingViewPlanDetails": "View Plan Details",
"billingUsageAndLimits": "Usage and Limits",
"billingViewUsageAndLimits": "View your plan's limits and current usage",
"billingCurrentUsage": "Current Usage",
"billingMaximumLimits": "Maximum Limits",
"billingRemoteNodes": "Remote Nodes",
"billingUnlimited": "Unlimited",
"billingPaidLicenseKeys": "Paid License Keys",
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
"billingCurrentKeys": "Current Keys",
"billingModifyCurrentPlan": "Modify Current Plan",
"billingConfirmUpgrade": "Confirm Upgrade",
"billingConfirmDowngrade": "Confirm Downgrade",
"billingConfirmUpgradeDescription": "You are about to upgrade your plan. Review the new limits and pricing below.",
"billingConfirmDowngradeDescription": "You are about to downgrade your plan. Review the new limits and pricing below.",
"billingPlanIncludes": "Plan Includes",
"billingProcessing": "Processing...",
"billingConfirmUpgradeButton": "Confirm Upgrade",
"billingConfirmDowngradeButton": "Confirm Downgrade",
"billingLimitViolationWarning": "Usage Exceeds New Plan Limits",
"billingLimitViolationDescription": "Your current usage exceeds the limits of this plan. After downgrading, all actions will be disabled until you reduce usage within the new limits. Please review the features below that are currently over the limits. Limits in violation:",
"billingFeatureLossWarning": "Feature Availability Notice",
"billingFeatureLossDescription": "By downgrading, features not available in the new plan will be automatically disabled. Some settings and configurations may be lost. Please review the pricing matrix to understand which features will no longer be available.",
"billingUsageExceedsLimit": "Current usage ({current}) exceeds limit ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "I agree to the", "IAgreeToThe": "I agree to the",
"termsOfService": "terms of service", "termsOfService": "terms of service",
@@ -1545,8 +1594,8 @@
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.", "addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
"selectSites": "Select sites", "selectSites": "Select sites",
"sitesDescription": "The client will have connectivity to the selected sites", "sitesDescription": "The client will have connectivity to the selected sites",
"clientInstallOlm": "Install Olm", "clientInstallOlm": "Install Machine Client",
"clientInstallOlmDescription": "Get Olm running on your system", "clientInstallOlmDescription": "Install the machine client for your system",
"clientOlmCredentials": "Credentials", "clientOlmCredentials": "Credentials",
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server", "clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
"olmEndpoint": "Endpoint", "olmEndpoint": "Endpoint",
@@ -1886,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?", "authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?",
"authPageBrandingDeleteConfirm": "Confirm Delete Branding", "authPageBrandingDeleteConfirm": "Confirm Delete Branding",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Primary Color", "brandingPrimaryColor": "Primary Color",
"brandingLogoWidth": "Width (px)", "brandingLogoWidth": "Width (px)",
"brandingLogoHeight": "Height (px)", "brandingLogoHeight": "Height (px)",
@@ -1935,6 +1987,13 @@
"orgAuthBackToSignIn": "Back to standard sign in", "orgAuthBackToSignIn": "Back to standard sign in",
"orgAuthNoAccount": "Don't have an account?", "orgAuthNoAccount": "Don't have an account?",
"subscriptionRequiredToUse": "A subscription is required to use this feature.", "subscriptionRequiredToUse": "A subscription is required to use this feature.",
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
"subscriptionRequiredTierToUse": "This feature requires <tierLink>{tier}</tierLink> or higher.",
"upgradeToTierToUse": "Upgrade to <tierLink>{tier}</tierLink> or higher to use this feature.",
"subscriptionTierTier1": "Home",
"subscriptionTierTier2": "Team",
"subscriptionTierTier3": "Business",
"subscriptionTierEnterprise": "Enterprise",
"idpDisabled": "Identity providers are disabled.", "idpDisabled": "Identity providers are disabled.",
"orgAuthPageDisabled": "Organization auth page is disabled.", "orgAuthPageDisabled": "Organization auth page is disabled.",
"domainRestartedDescription": "Domain verification restarted successfully", "domainRestartedDescription": "Domain verification restarted successfully",
@@ -2023,7 +2082,7 @@
"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.", "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", "machineClientsBannerPangolinCLI": "Pangolin CLI",
"machineClientsBannerOlmCLI": "Olm CLI", "machineClientsBannerOlmCLI": "Olm CLI",
"machineClientsBannerOlmContainer": "Olm Container", "machineClientsBannerOlmContainer": "Container",
"clientsTableUserClients": "User", "clientsTableUserClients": "User",
"clientsTableMachineClients": "Machine", "clientsTableMachineClients": "Machine",
"licenseTableValidUntil": "Valid Until", "licenseTableValidUntil": "Valid Until",
@@ -2246,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "End of following year", "logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization", "actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization", "accessLogsDescription": "View access auth requests for resources in this organization",
"licenseRequiredToUse": "An Enterprise license is required to use this feature.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Certificate Resolver", "certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.", "certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver", "selectCertResolver": "Select Certificate Resolver",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Eres un miembro de {count, plural, =0 {ninguna organización} one {una organización} other {# organizaciones}}.", "componentsMember": "Eres un miembro de {count, plural, =0 {ninguna organización} one {una organización} other {# organizaciones}}.",
"componentsInvalidKey": "Se han detectado claves de licencia inválidas o caducadas. Siga los términos de licencia para seguir usando todas las características.", "componentsInvalidKey": "Se han detectado claves de licencia inválidas o caducadas. Siga los términos de licencia para seguir usando todas las características.",
"dismiss": "Descartar", "dismiss": "Descartar",
"subscriptionViolationMessage": "Estás más allá de tus límites para tu plan actual. Corrija el problema eliminando sitios, usuarios u otros recursos para permanecer dentro de tu plan.",
"subscriptionViolationViewBilling": "Ver facturación",
"componentsLicenseViolation": "Violación de la Licencia: Este servidor está usando sitios {usedSites} que exceden su límite de licencias de sitios {maxSites} . Siga los términos de licencia para seguir usando todas las características.", "componentsLicenseViolation": "Violación de la Licencia: Este servidor está usando sitios {usedSites} que exceden su límite de licencias de sitios {maxSites} . Siga los términos de licencia para seguir usando todas las características.",
"componentsSupporterMessage": "¡Gracias por apoyar a Pangolin como {tier}!", "componentsSupporterMessage": "¡Gracias por apoyar a Pangolin como {tier}!",
"inviteErrorNotValid": "Lo sentimos, pero parece que la invitación a la que intentas acceder no ha sido aceptada o ya no es válida.", "inviteErrorNotValid": "Lo sentimos, pero parece que la invitación a la que intentas acceder no ha sido aceptada o ya no es válida.",
@@ -199,6 +201,7 @@
"protocolSelect": "Seleccionar un protocolo", "protocolSelect": "Seleccionar un protocolo",
"resourcePortNumber": "Número de puerto", "resourcePortNumber": "Número de puerto",
"resourcePortNumberDescription": "El número de puerto externo a las solicitudes de proxy.", "resourcePortNumberDescription": "El número de puerto externo a las solicitudes de proxy.",
"back": "Atrás",
"cancel": "Cancelar", "cancel": "Cancelar",
"resourceConfig": "Fragmentos de configuración", "resourceConfig": "Fragmentos de configuración",
"resourceConfigDescription": "Copia y pega estos fragmentos de configuración para configurar el recurso TCP/UDP", "resourceConfigDescription": "Copia y pega estos fragmentos de configuración para configurar el recurso TCP/UDP",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Se ha producido un error al eliminar la organización.", "orgErrorDeleteMessage": "Se ha producido un error al eliminar la organización.",
"orgDeleted": "Organización eliminada", "orgDeleted": "Organización eliminada",
"orgDeletedMessage": "La organización y sus datos han sido eliminados.", "orgDeletedMessage": "La organización y sus datos han sido eliminados.",
"deleteAccount": "Eliminar cuenta",
"deleteAccountDescription": "Elimina permanentemente tu cuenta, todas las organizaciones que posees y todos los datos dentro de esas organizaciones. Esto no se puede deshacer.",
"deleteAccountButton": "Eliminar cuenta",
"deleteAccountConfirmTitle": "Eliminar cuenta",
"deleteAccountConfirmMessage": "Esto borrará permanentemente tu cuenta, todas las organizaciones que posees y todos los datos dentro de esas organizaciones. Esto no se puede deshacer.",
"deleteAccountConfirmString": "eliminar cuenta",
"deleteAccountSuccess": "Cuenta eliminada",
"deleteAccountSuccessMessage": "Tu cuenta ha sido eliminada.",
"deleteAccountError": "Error al eliminar la cuenta",
"deleteAccountPreviewAccount": "Tu cuenta",
"deleteAccountPreviewOrgs": "Organizaciones que tienes (y todos sus datos)",
"orgMissing": "Falta el ID de la organización", "orgMissing": "Falta el ID de la organización",
"orgMissingMessage": "No se puede regenerar la invitación sin el ID de la organización.", "orgMissingMessage": "No se puede regenerar la invitación sin el ID de la organización.",
"accessUsersManage": "Administrar usuarios", "accessUsersManage": "Administrar usuarios",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtrar por estado de aprobación", "filterByApprovalState": "Filtrar por estado de aprobación",
"approvalListEmpty": "No hay aprobaciones", "approvalListEmpty": "No hay aprobaciones",
"approvalState": "Estado de aprobación", "approvalState": "Estado de aprobación",
"approvalLoadMore": "Cargar más",
"loadingApprovals": "Cargando aprobaciones",
"approve": "Aprobar", "approve": "Aprobar",
"approved": "Aprobado", "approved": "Aprobado",
"denied": "Denegado", "denied": "Denegado",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Aumentar el número de sitios", "sitestCountIncrease": "Aumentar el número de sitios",
"idpManage": "Administrar proveedores de identidad", "idpManage": "Administrar proveedores de identidad",
"idpManageDescription": "Ver y administrar proveedores de identidad en el sistema", "idpManageDescription": "Ver y administrar proveedores de identidad en el sistema",
"idpGlobalModeBanner": "Los proveedores de identidad (IdPs) por organización están deshabilitados en este servidor. Está utilizando IdPs globales (compartidos entre todas las organizaciones). Administra los IdPs globales en el <adminPanelLink>panel de administración</adminPanelLink>. Para habilitar los IdPs por organización, edita la configuración del servidor y establece el modo de IdP en org. <configDocsLink>Consulta la documentación</configDocsLink>. Si deseas seguir utilizando IdPs globales y hacer que esto desaparezca de las configuraciones de la organización, establece explícitamente el modo en global en la configuración.",
"idpGlobalModeBannerUpgradeRequired": "Los proveedores de identidad (IdPs) por organización están deshabilitados en este servidor. Está utilizando IdPs globales (compartidos entre todas las organizaciones). Administra los IdPs globales en el <adminPanelLink>panel de administración</adminPanelLink>. Para usar proveedores de identidad por organización, debes actualizar a la edición Empresarial.",
"idpGlobalModeBannerLicenseRequired": "Los proveedores de identidad (IdPs) por organización están deshabilitados en este servidor. Está utilizando identificadores globales (compartidos en todas las organizaciones). Gestionar identificaciones globales en el panel <adminPanelLink>de administración</adminPanelLink>. Para utilizar proveedores de identidad por organización, se requiere una licencia de empresa.",
"idpDeletedDescription": "Proveedor de identidad eliminado correctamente", "idpDeletedDescription": "Proveedor de identidad eliminado correctamente",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "¿Está seguro que desea eliminar permanentemente el proveedor de identidad?", "idpQuestionRemove": "¿Está seguro que desea eliminar permanentemente el proveedor de identidad?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Configuración - Pangolin", "pangolinSetup": "Configuración - Pangolin",
"orgNameRequired": "El nombre de la organización es obligatorio", "orgNameRequired": "El nombre de la organización es obligatorio",
"orgIdRequired": "El ID de la organización es obligatorio", "orgIdRequired": "El ID de la organización es obligatorio",
"orgIdMaxLength": "El ID de la organización debe tener como máximo 32 caracteres",
"orgErrorCreate": "Se ha producido un error al crear el org", "orgErrorCreate": "Se ha producido un error al crear el org",
"pageNotFound": "Página no encontrada", "pageNotFound": "Página no encontrada",
"pageNotFoundDescription": "¡Vaya! La página que estás buscando no existe.", "pageNotFoundDescription": "¡Vaya! La página que estás buscando no existe.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Ver registros", "actionViewLogs": "Ver registros",
"noneSelected": "Ninguno seleccionado", "noneSelected": "Ninguno seleccionado",
"orgNotFound2": "No se encontraron organizaciones.", "orgNotFound2": "No se encontraron organizaciones.",
"searchProgress": "Buscar...", "searchPlaceholder": "Buscar...",
"emptySearchOptions": "No se encontraron opciones",
"create": "Crear", "create": "Crear",
"orgs": "Organizaciones", "orgs": "Organizaciones",
"loginError": "Ocurrió un error inesperado. Por favor, inténtelo de nuevo.", "loginError": "Ocurrió un error inesperado. Por favor, inténtelo de nuevo.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Registro y análisis", "sidebarLogAndAnalytics": "Registro y análisis",
"sidebarBluePrints": "Planos", "sidebarBluePrints": "Planos",
"sidebarOrganization": "Organización", "sidebarOrganization": "Organización",
"sidebarBillingAndLicenses": "Facturación y licencias",
"sidebarLogsAnalytics": "Analíticas", "sidebarLogsAnalytics": "Analíticas",
"blueprints": "Planos", "blueprints": "Planos",
"blueprintsDescription": "Aplicar configuraciones declarativas y ver ejecuciones anteriores", "blueprintsDescription": "Aplicar configuraciones declarativas y ver ejecuciones anteriores",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Descripción general de los límites de uso", "billingUsageLimitsOverview": "Descripción general de los límites de uso",
"billingMonitorUsage": "Monitorea tu uso comparado con los límites configurados. Si necesitas que aumenten los límites, contáctanos a soporte@pangolin.net.", "billingMonitorUsage": "Monitorea tu uso comparado con los límites configurados. Si necesitas que aumenten los límites, contáctanos a soporte@pangolin.net.",
"billingDataUsage": "Uso de datos", "billingDataUsage": "Uso de datos",
"billingOnlineTime": "Tiempo en línea del sitio", "billingSites": "Sitios",
"billingUsers": "Usuarios activos", "billingUsers": "Usuarios",
"billingDomains": "Dominios activos", "billingDomains": "Dominios",
"billingRemoteExitNodes": "Nodos autogestionados activos", "billingOrganizations": "Orgánico",
"billingRemoteExitNodes": "Nodos remotos",
"billingNoLimitConfigured": "No se ha configurado ningún límite", "billingNoLimitConfigured": "No se ha configurado ningún límite",
"billingEstimatedPeriod": "Período de facturación estimado", "billingEstimatedPeriod": "Período de facturación estimado",
"billingIncludedUsage": "Uso incluido", "billingIncludedUsage": "Uso incluido",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Error al obtener la URL del portal", "billingFailedToGetPortalUrl": "Error al obtener la URL del portal",
"billingPortalError": "Error del portal", "billingPortalError": "Error del portal",
"billingDataUsageInfo": "Se le cobran todos los datos transferidos a través de sus túneles seguros cuando se conectan a la nube. Esto incluye tanto tráfico entrante como saliente a través de todos sus sitios. Cuando alcance su límite, sus sitios se desconectarán hasta que actualice su plan o reduzca el uso. Los datos no se cargan cuando se usan nodos.", "billingDataUsageInfo": "Se le cobran todos los datos transferidos a través de sus túneles seguros cuando se conectan a la nube. Esto incluye tanto tráfico entrante como saliente a través de todos sus sitios. Cuando alcance su límite, sus sitios se desconectarán hasta que actualice su plan o reduzca el uso. Los datos no se cargan cuando se usan nodos.",
"billingOnlineTimeInfo": "Se te cobrará en función del tiempo que tus sitios permanezcan conectados a la nube. Por ejemplo, 44.640 minutos equivale a un sitio que funciona 24/7 durante un mes completo. Cuando alcance su límite, sus sitios se desconectarán hasta que mejore su plan o reduzca el uso. No se cargará el tiempo al usar nodos.", "billingSInfo": "Cuántos sitios puedes usar",
"billingUsersInfo": "Se le cobra por cada usuario en la organización. La facturación se calcula diariamente según el número de cuentas de usuario activas en su órgano.", "billingUsersInfo": "Cuántos usuarios puedes usar",
"billingDomainInfo": "Se le cobra por cada dominio en la organización. La facturación se calcula diariamente en función del número de cuentas de dominio activas en su órgano.", "billingDomainInfo": "Cuántos dominios puedes usar",
"billingRemoteExitNodesInfo": "Se le cobra por cada nodo administrado en la organización. La facturación se calcula diariamente en función del número de nodos activos gestionados en su órgano.", "billingRemoteExitNodesInfo": "Cuántos nodos remotos puedes usar",
"billingLicenseKeys": "Claves de licencia",
"billingLicenseKeysDescription": "Administrar las suscripciones de su clave de licencia",
"billingLicenseSubscription": "Suscripción de licencia",
"billingInactive": "Inactivo",
"billingLicenseItem": "Licencia",
"billingQuantity": "Cantidad",
"billingTotal": "total",
"billingModifyLicenses": "Modificar suscripción de licencia",
"domainNotFound": "Dominio no encontrado", "domainNotFound": "Dominio no encontrado",
"domainNotFoundDescription": "Este recurso está deshabilitado porque el dominio ya no existe en nuestro sistema. Por favor, establece un nuevo dominio para este recurso.", "domainNotFoundDescription": "Este recurso está deshabilitado porque el dominio ya no existe en nuestro sistema. Por favor, establece un nuevo dominio para este recurso.",
"failed": "Fallido", "failed": "Fallido",
"createNewOrgDescription": "Crear una nueva organización", "createNewOrgDescription": "Crear una nueva organización",
"organization": "Organización", "organization": "Organización",
"primary": "Principal",
"port": "Puerto", "port": "Puerto",
"securityKeyManage": "Gestionar llaves de seguridad", "securityKeyManage": "Gestionar llaves de seguridad",
"securityKeyDescription": "Agregar o eliminar llaves de seguridad para autenticación sin contraseña", "securityKeyDescription": "Agregar o eliminar llaves de seguridad para autenticación sin contraseña",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Se requiere número de puerto para recursos no HTTP", "resourcePortRequired": "Se requiere número de puerto para recursos no HTTP",
"resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP", "resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP",
"billingPricingCalculatorLink": "Calculadora de Precios", "billingPricingCalculatorLink": "Calculadora de Precios",
"billingYourPlan": "Su plan",
"billingViewOrModifyPlan": "Ver o modificar su plan actual",
"billingViewPlanDetails": "Ver detalles del plan",
"billingUsageAndLimits": "Uso y límites",
"billingViewUsageAndLimits": "Ver los límites de tu plan y el uso actual",
"billingCurrentUsage": "Uso actual",
"billingMaximumLimits": "Límites máximos",
"billingRemoteNodes": "Nodos remotos",
"billingUnlimited": "Ilimitado",
"billingPaidLicenseKeys": "Claves de licencia pagadas",
"billingManageLicenseSubscription": "Administra tu suscripción para las claves de licencia autoalojadas pagadas",
"billingCurrentKeys": "Claves actuales",
"billingModifyCurrentPlan": "Modificar plan actual",
"billingConfirmUpgrade": "Confirmar actualización",
"billingConfirmDowngrade": "Confirmar descenso",
"billingConfirmUpgradeDescription": "Estás a punto de actualizar tu plan. Revisa los nuevos límites y precios a continuación.",
"billingConfirmDowngradeDescription": "Está a punto de rebajar su plan. Revise los nuevos límites y los precios a continuación.",
"billingPlanIncludes": "Plan Incluye",
"billingProcessing": "Procesando...",
"billingConfirmUpgradeButton": "Confirmar actualización",
"billingConfirmDowngradeButton": "Confirmar descenso",
"billingLimitViolationWarning": "El uso excede los nuevos límites del plan",
"billingLimitViolationDescription": "Su uso actual excede los límites de este plan. Después de degradar, todas las acciones se desactivarán hasta que reduzca el uso dentro de los nuevos límites. Por favor, revisa las siguientes características que están actualmente por encima de los límites. Límites en violación:",
"billingFeatureLossWarning": "Aviso de disponibilidad de funcionalidad",
"billingFeatureLossDescription": "Al degradar, las características no disponibles en el nuevo plan se desactivarán automáticamente. Algunas configuraciones y configuraciones pueden perderse. Por favor, revise la matriz de precios para entender qué características ya no estarán disponibles.",
"billingUsageExceedsLimit": "El uso actual ({current}) supera el límite ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Estoy de acuerdo con los", "IAgreeToThe": "Estoy de acuerdo con los",
"termsOfService": "términos del servicio", "termsOfService": "términos del servicio",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "¿Está seguro de que desea eliminar la marca de las páginas de autenticación?", "authPageBrandingQuestionRemove": "¿Está seguro de que desea eliminar la marca de las páginas de autenticación?",
"authPageBrandingDeleteConfirm": "Confirmar eliminación de la marca", "authPageBrandingDeleteConfirm": "Confirmar eliminación de la marca",
"brandingLogoURL": "URL del logotipo", "brandingLogoURL": "URL del logotipo",
"brandingLogoURLOrPath": "URL o ruta de Logo",
"brandingLogoPathDescription": "Introduzca una URL o una ruta local.",
"brandingLogoURLDescription": "Introduzca una URL de acceso público a su imagen de logotipo.",
"brandingPrimaryColor": "Color primario", "brandingPrimaryColor": "Color primario",
"brandingLogoWidth": "Ancho (px)", "brandingLogoWidth": "Ancho (px)",
"brandingLogoHeight": "Altura (px)", "brandingLogoHeight": "Altura (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Volver a iniciar sesión estándar", "orgAuthBackToSignIn": "Volver a iniciar sesión estándar",
"orgAuthNoAccount": "¿No tienes una cuenta?", "orgAuthNoAccount": "¿No tienes una cuenta?",
"subscriptionRequiredToUse": "Se requiere una suscripción para utilizar esta función.", "subscriptionRequiredToUse": "Se requiere una suscripción para utilizar esta función.",
"mustUpgradeToUse": "Debes actualizar tu suscripción para usar esta función.",
"subscriptionRequiredTierToUse": "Esta función requiere <tierLink>{tier}</tierLink> o superior.",
"upgradeToTierToUse": "Actualiza a <tierLink>{tier}</tierLink> o superior para usar esta función.",
"subscriptionTierTier1": "Inicio",
"subscriptionTierTier2": "Equipo",
"subscriptionTierTier3": "Negocio",
"subscriptionTierEnterprise": "Empresa",
"idpDisabled": "Los proveedores de identidad están deshabilitados.", "idpDisabled": "Los proveedores de identidad están deshabilitados.",
"orgAuthPageDisabled": "La página de autenticación de la organización está deshabilitada.", "orgAuthPageDisabled": "La página de autenticación de la organización está deshabilitada.",
"domainRestartedDescription": "Verificación de dominio reiniciada con éxito", "domainRestartedDescription": "Verificación de dominio reiniciada con éxito",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Obtener una licencia",
"description": "Elige un plan y dinos cómo planeas usar Pangolin.",
"chooseTier": "Elige tu plan",
"viewPricingLink": "Ver precios, características y límites",
"tiers": {
"starter": {
"title": "Interruptor",
"description": "Características de la empresa, 25 usuarios, 25 sitios y soporte comunitario."
},
"scale": {
"title": "Escala",
"description": "Características de la empresa, 50 usuarios, 50 sitios y soporte prioritario."
}
},
"personalUseOnly": "Solo uso personal (licencia gratuita, sin pago)",
"buttons": {
"continueToCheckout": "Continuar con el pago"
},
"toasts": {
"checkoutError": {
"title": "Error de pago",
"description": "No se pudo iniciar el pago. Por favor, inténtelo de nuevo."
}
}
},
"priority": "Prioridad", "priority": "Prioridad",
"priorityDescription": "Las rutas de prioridad más alta son evaluadas primero. Prioridad = 100 significa orden automático (decisiones del sistema). Utilice otro número para hacer cumplir la prioridad manual.", "priorityDescription": "Las rutas de prioridad más alta son evaluadas primero. Prioridad = 100 significa orden automático (decisiones del sistema). Utilice otro número para hacer cumplir la prioridad manual.",
"instanceName": "Nombre de instancia", "instanceName": "Nombre de instancia",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Fin del año siguiente", "logRetentionEndOfFollowingYear": "Fin del año siguiente",
"actionLogsDescription": "Ver un historial de acciones realizadas en esta organización", "actionLogsDescription": "Ver un historial de acciones realizadas en esta organización",
"accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización", "accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización",
"licenseRequiredToUse": "Se requiere una licencia Enterprise para utilizar esta función.", "licenseRequiredToUse": "Se requiere una licencia <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> para utilizar esta función. Esta característica también está disponible en <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "La <enterpriseEditionLink>versión Enterprise</enterpriseEditionLink> es necesaria para utilizar esta función. Esta función también está disponible en <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Resolver certificado", "certResolver": "Resolver certificado",
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.", "certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
"selectCertResolver": "Seleccionar Resolver Certificado", "selectCertResolver": "Seleccionar Resolver Certificado",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Cortafuegos activado", "firewallEnabled": "Cortafuegos activado",
"autoUpdatesEnabled": "Actualizaciones automáticas habilitadas", "autoUpdatesEnabled": "Actualizaciones automáticas habilitadas",
"tpmAvailable": "TPM disponible", "tpmAvailable": "TPM disponible",
"windowsAntivirusEnabled": "Antivirus activado",
"macosSipEnabled": "Protección de integridad del sistema (SIP)", "macosSipEnabled": "Protección de integridad del sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Sigilo Firewall", "macosFirewallStealthMode": "Modo Sigilo Firewall",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Vous {count, plural, =0 {n'} other {} }êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.", "componentsMember": "Vous {count, plural, =0 {n'} other {} }êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.",
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Veuillez respecter les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", "componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Veuillez respecter les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"dismiss": "Rejeter", "dismiss": "Rejeter",
"subscriptionViolationMessage": "Vous dépassez vos limites pour votre forfait actuel. Corrigez le problème en supprimant des sites, des utilisateurs ou d'autres ressources pour rester dans votre forfait.",
"subscriptionViolationViewBilling": "Voir la facturation",
"componentsLicenseViolation": "Violation de licence : ce serveur utilise {usedSites} nœuds, ce qui dépasse la limite autorisée de {maxSites} nœuds. Respectez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", "componentsLicenseViolation": "Violation de licence : ce serveur utilise {usedSites} nœuds, ce qui dépasse la limite autorisée de {maxSites} nœuds. Respectez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!", "componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder n'ait pas été acceptée ou ne soit plus valide.", "inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder n'ait pas été acceptée ou ne soit plus valide.",
@@ -199,6 +201,7 @@
"protocolSelect": "Choisir un protocole", "protocolSelect": "Choisir un protocole",
"resourcePortNumber": "Numéro de port", "resourcePortNumber": "Numéro de port",
"resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.", "resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.",
"back": "Précédent",
"cancel": "Abandonner", "cancel": "Abandonner",
"resourceConfig": "Snippets de configuration", "resourceConfig": "Snippets de configuration",
"resourceConfigDescription": "Copiez et collez ces extraits de configuration pour configurer la ressource TCP/UDP", "resourceConfigDescription": "Copiez et collez ces extraits de configuration pour configurer la ressource TCP/UDP",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Une erreur s'est produite lors de la suppression de l'organisation.", "orgErrorDeleteMessage": "Une erreur s'est produite lors de la suppression de l'organisation.",
"orgDeleted": "Organisation supprimée", "orgDeleted": "Organisation supprimée",
"orgDeletedMessage": "L'organisation et ses données ont été supprimées.", "orgDeletedMessage": "L'organisation et ses données ont été supprimées.",
"deleteAccount": "Supprimer le compte",
"deleteAccountDescription": "Supprimer définitivement votre compte, toutes les organisations que vous possédez et toutes les données au sein de ces organisations. Cela ne peut pas être annulé.",
"deleteAccountButton": "Supprimer le compte",
"deleteAccountConfirmTitle": "Supprimer le compte",
"deleteAccountConfirmMessage": "Cela effacera définitivement votre compte, toutes les organisations que vous possédez et toutes les données au sein de ces organisations. Cela ne peut pas être annulé.",
"deleteAccountConfirmString": "supprimer le compte",
"deleteAccountSuccess": "Compte supprimé",
"deleteAccountSuccessMessage": "Votre compte a été supprimé.",
"deleteAccountError": "Échec de la suppression du compte",
"deleteAccountPreviewAccount": "Votre Compte",
"deleteAccountPreviewOrgs": "Organisations que vous possédez (et toutes leurs données)",
"orgMissing": "ID d'organisation manquant", "orgMissing": "ID d'organisation manquant",
"orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.", "orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.",
"accessUsersManage": "Gérer les utilisateurs", "accessUsersManage": "Gérer les utilisateurs",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtrer par État d'Approbation", "filterByApprovalState": "Filtrer par État d'Approbation",
"approvalListEmpty": "Aucune approbation", "approvalListEmpty": "Aucune approbation",
"approvalState": "État d'approbation", "approvalState": "État d'approbation",
"approvalLoadMore": "Charger plus",
"loadingApprovals": "Chargement des approbations",
"approve": "Approuver", "approve": "Approuver",
"approved": "Approuvé", "approved": "Approuvé",
"denied": "Refusé", "denied": "Refusé",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Augmenter le nombre de sites", "sitestCountIncrease": "Augmenter le nombre de sites",
"idpManage": "Gérer les fournisseurs d'identité", "idpManage": "Gérer les fournisseurs d'identité",
"idpManageDescription": "Voir et gérer les fournisseurs d'identité dans le système", "idpManageDescription": "Voir et gérer les fournisseurs d'identité dans le système",
"idpGlobalModeBanner": "Les fournisseurs d'identité (IdPs) par organisation sont désactivés sur ce serveur. Il utilise des IdPs globaux (partagés entre toutes les organisations). Gérez les IdPs globaux dans le panneau d'administration <adminPanelLink></adminPanelLink>. Pour activer les IdPs par organisation, éditez la configuration du serveur et réglez le mode IdP sur org. <configDocsLink>Voir la documentation</configDocsLink>. Si vous voulez continuer à utiliser les IdPs globaux et faire disparaître cela des paramètres de l'organisation, définissez explicitement le mode à global dans la configuration.",
"idpGlobalModeBannerUpgradeRequired": "Les fournisseurs d'identité (IdPs) par organisation sont désactivés sur ce serveur. Il utilise des IdPs globaux (partagés entre toutes les organisations). Gérer les IdPs globaux dans le panneau d'administration <adminPanelLink></adminPanelLink>. Pour utiliser les fournisseurs d'identité par organisation, vous devez passer à l'édition Entreprise.",
"idpGlobalModeBannerLicenseRequired": "Les fournisseurs d'identité (IdPs) par organisation sont désactivés sur ce serveur. Il utilise des IdPs globaux (partagés entre toutes les organisations). Gérer les IdPs globaux dans le panneau d'administration <adminPanelLink></adminPanelLink>. Pour utiliser les fournisseurs d'identité par organisation, une licence d'entreprise est requise.",
"idpDeletedDescription": "Fournisseur d'identité supprimé avec succès", "idpDeletedDescription": "Fournisseur d'identité supprimé avec succès",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Êtes-vous sûr de vouloir supprimer définitivement le fournisseur d'identité?", "idpQuestionRemove": "Êtes-vous sûr de vouloir supprimer définitivement le fournisseur d'identité?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Configuration - Pangolin", "pangolinSetup": "Configuration - Pangolin",
"orgNameRequired": "Le nom de l'organisation est requis", "orgNameRequired": "Le nom de l'organisation est requis",
"orgIdRequired": "L'ID de l'organisation est requis", "orgIdRequired": "L'ID de l'organisation est requis",
"orgIdMaxLength": "L'identifiant de l'organisation doit comporter au plus 32 caractères",
"orgErrorCreate": "Une erreur s'est produite lors de la création de l'organisation", "orgErrorCreate": "Une erreur s'est produite lors de la création de l'organisation",
"pageNotFound": "Page non trouvée", "pageNotFound": "Page non trouvée",
"pageNotFoundDescription": "Oups! La page que vous recherchez n'existe pas.", "pageNotFoundDescription": "Oups! La page que vous recherchez n'existe pas.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Voir les logs", "actionViewLogs": "Voir les logs",
"noneSelected": "Aucune sélection", "noneSelected": "Aucune sélection",
"orgNotFound2": "Aucune organisation trouvée.", "orgNotFound2": "Aucune organisation trouvée.",
"searchProgress": "Rechercher...", "searchPlaceholder": "Recherche...",
"emptySearchOptions": "Aucune option trouvée",
"create": "Créer", "create": "Créer",
"orgs": "Organisations", "orgs": "Organisations",
"loginError": "Une erreur inattendue s'est produite. Veuillez réessayer.", "loginError": "Une erreur inattendue s'est produite. Veuillez réessayer.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Journaux & Analytiques", "sidebarLogAndAnalytics": "Journaux & Analytiques",
"sidebarBluePrints": "Configs", "sidebarBluePrints": "Configs",
"sidebarOrganization": "Organisation", "sidebarOrganization": "Organisation",
"sidebarBillingAndLicenses": "Facturation & Licences",
"sidebarLogsAnalytics": "Analyses", "sidebarLogsAnalytics": "Analyses",
"blueprints": "Configs", "blueprints": "Configs",
"blueprintsDescription": "Appliquer les configurations déclaratives et afficher les exécutions précédentes", "blueprintsDescription": "Appliquer les configurations déclaratives et afficher les exécutions précédentes",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Vue d'ensemble des limites d'utilisation", "billingUsageLimitsOverview": "Vue d'ensemble des limites d'utilisation",
"billingMonitorUsage": "Surveillez votre consommation par rapport aux limites configurées. Si vous avez besoin d'une augmentation des limites, veuillez nous contacter à support@pangolin.net.", "billingMonitorUsage": "Surveillez votre consommation par rapport aux limites configurées. Si vous avez besoin d'une augmentation des limites, veuillez nous contacter à support@pangolin.net.",
"billingDataUsage": "Utilisation des données", "billingDataUsage": "Utilisation des données",
"billingOnlineTime": "Temps en ligne du site", "billingSites": "Nœuds",
"billingUsers": "Utilisateurs actifs", "billingUsers": "Utilisateurs",
"billingDomains": "Domaines actifs", "billingDomains": "Domaines",
"billingRemoteExitNodes": "Nœuds auto-hébergés actifs", "billingOrganizations": "Organes",
"billingRemoteExitNodes": "Nœuds distants",
"billingNoLimitConfigured": "Aucune limite configurée", "billingNoLimitConfigured": "Aucune limite configurée",
"billingEstimatedPeriod": "Période de facturation estimée", "billingEstimatedPeriod": "Période de facturation estimée",
"billingIncludedUsage": "Utilisation incluse", "billingIncludedUsage": "Utilisation incluse",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Échec pour obtenir l'URL du portail", "billingFailedToGetPortalUrl": "Échec pour obtenir l'URL du portail",
"billingPortalError": "Erreur du portail", "billingPortalError": "Erreur du portail",
"billingDataUsageInfo": "Vous êtes facturé pour toutes les données transférées via vos tunnels sécurisés lorsque vous êtes connecté au cloud. Cela inclut le trafic entrant et sortant sur tous vos sites. Lorsque vous atteignez votre limite, vos sites se déconnecteront jusqu'à ce que vous mettiez à niveau votre plan ou réduisiez l'utilisation. Les données ne sont pas facturées lors de l'utilisation de nœuds.", "billingDataUsageInfo": "Vous êtes facturé pour toutes les données transférées via vos tunnels sécurisés lorsque vous êtes connecté au cloud. Cela inclut le trafic entrant et sortant sur tous vos sites. Lorsque vous atteignez votre limite, vos sites se déconnecteront jusqu'à ce que vous mettiez à niveau votre plan ou réduisiez l'utilisation. Les données ne sont pas facturées lors de l'utilisation de nœuds.",
"billingOnlineTimeInfo": "Vous êtes facturé en fonction de la durée de connexion de vos sites au cloud. Par exemple, 44 640 minutes équivaut à un site fonctionnant 24/7 pendant un mois complet. Lorsque vous atteignez votre limite, vos sites se déconnecteront jusqu'à ce que vous mettiez à niveau votre forfait ou réduisiez votre consommation. Le temps n'est pas facturé lors de l'utilisation de nœuds.", "billingSInfo": "Combien de sites vous pouvez utiliser",
"billingUsersInfo": "Vous êtes facturé pour chaque utilisateur de l'organisation. La facturation est calculée quotidiennement en fonction du nombre de comptes d'utilisateurs actifs dans votre organisation.", "billingUsersInfo": "Combien d'utilisateurs vous pouvez utiliser",
"billingDomainInfo": "Vous êtes facturé pour chaque domaine de l'organisation. La facturation est calculée quotidiennement en fonction du nombre de comptes de domaine actifs dans votre organisation.", "billingDomainInfo": "Combien de domaines vous pouvez utiliser",
"billingRemoteExitNodesInfo": "Vous êtes facturé pour chaque noeud géré dans l'organisation. La facturation est calculée quotidiennement en fonction du nombre de nœuds gérés actifs dans votre organisation.", "billingRemoteExitNodesInfo": "Combien de nœuds distants vous pouvez utiliser",
"billingLicenseKeys": "Clés de licence",
"billingLicenseKeysDescription": "Gérer vos abonnements à la clé de licence",
"billingLicenseSubscription": "Abonnement à la licence",
"billingInactive": "Inactif",
"billingLicenseItem": "Article de la licence",
"billingQuantity": "Quantité",
"billingTotal": "total",
"billingModifyLicenses": "Modifier l'abonnement à la licence",
"domainNotFound": "Domaine introuvable", "domainNotFound": "Domaine introuvable",
"domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.", "domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.",
"failed": "Échec", "failed": "Échec",
"createNewOrgDescription": "Créer une nouvelle organisation", "createNewOrgDescription": "Créer une nouvelle organisation",
"organization": "Organisation", "organization": "Organisation",
"primary": "Primaire",
"port": "Port", "port": "Port",
"securityKeyManage": "Gérer les clés de sécurité", "securityKeyManage": "Gérer les clés de sécurité",
"securityKeyDescription": "Ajouter ou supprimer des clés de sécurité pour l'authentification sans mot de passe", "securityKeyDescription": "Ajouter ou supprimer des clés de sécurité pour l'authentification sans mot de passe",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Le numéro de port est requis pour les ressources non-HTTP", "resourcePortRequired": "Le numéro de port est requis pour les ressources non-HTTP",
"resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP", "resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP",
"billingPricingCalculatorLink": "Calculateur de prix", "billingPricingCalculatorLink": "Calculateur de prix",
"billingYourPlan": "Votre plan",
"billingViewOrModifyPlan": "Voir ou modifier votre forfait actuel",
"billingViewPlanDetails": "Voir les détails du plan",
"billingUsageAndLimits": "Utilisation et limites",
"billingViewUsageAndLimits": "Voir les limites de votre plan et l'utilisation actuelle",
"billingCurrentUsage": "Utilisation actuelle",
"billingMaximumLimits": "Limites maximum",
"billingRemoteNodes": "Nœuds distants",
"billingUnlimited": "Illimité",
"billingPaidLicenseKeys": "Clés de licence payantes",
"billingManageLicenseSubscription": "Gérer votre abonnement pour les clés de licence auto-hébergées payantes",
"billingCurrentKeys": "Clés actuelles",
"billingModifyCurrentPlan": "Modifier le plan actuel",
"billingConfirmUpgrade": "Confirmer la mise à niveau",
"billingConfirmDowngrade": "Confirmer la rétrogradation",
"billingConfirmUpgradeDescription": "Vous êtes sur le point de mettre à niveau votre offre. Examinez les nouvelles limites et les nouveaux prix ci-dessous.",
"billingConfirmDowngradeDescription": "Vous êtes sur le point de rétrograder votre forfait. Examinez les nouvelles limites et les prix ci-dessous.",
"billingPlanIncludes": "Le forfait comprend",
"billingProcessing": "Traitement en cours...",
"billingConfirmUpgradeButton": "Confirmer la mise à niveau",
"billingConfirmDowngradeButton": "Confirmer la rétrogradation",
"billingLimitViolationWarning": "Utilisation dépassée les nouvelles limites de plan",
"billingLimitViolationDescription": "Votre utilisation actuelle dépasse les limites de ce plan. Après rétrogradation, toutes les actions seront désactivées jusqu'à ce que vous réduisiez l'utilisation dans les nouvelles limites. Veuillez consulter les fonctionnalités ci-dessous qui dépassent actuellement les limites. Limites en violation :",
"billingFeatureLossWarning": "Avis de disponibilité des fonctionnalités",
"billingFeatureLossDescription": "En rétrogradant, les fonctionnalités non disponibles dans le nouveau plan seront automatiquement désactivées. Certains paramètres et configurations peuvent être perdus. Veuillez consulter la matrice de prix pour comprendre quelles fonctionnalités ne seront plus disponibles.",
"billingUsageExceedsLimit": "L'utilisation actuelle ({current}) dépasse la limite ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Je suis d'accord avec", "IAgreeToThe": "Je suis d'accord avec",
"termsOfService": "les conditions d'utilisation", "termsOfService": "les conditions d'utilisation",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Êtes-vous sûr de vouloir supprimer la marque des pages d'authentification ?", "authPageBrandingQuestionRemove": "Êtes-vous sûr de vouloir supprimer la marque des pages d'authentification ?",
"authPageBrandingDeleteConfirm": "Confirmer la suppression de la marque", "authPageBrandingDeleteConfirm": "Confirmer la suppression de la marque",
"brandingLogoURL": "URL du logo", "brandingLogoURL": "URL du logo",
"brandingLogoURLOrPath": "URL du logo ou du chemin d'accès",
"brandingLogoPathDescription": "Entrez une URL ou un chemin local.",
"brandingLogoURLDescription": "Entrez une URL accessible au public à votre image de logo.",
"brandingPrimaryColor": "Couleur principale", "brandingPrimaryColor": "Couleur principale",
"brandingLogoWidth": "Largeur (px)", "brandingLogoWidth": "Largeur (px)",
"brandingLogoHeight": "Hauteur (px)", "brandingLogoHeight": "Hauteur (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Retour à la connexion standard", "orgAuthBackToSignIn": "Retour à la connexion standard",
"orgAuthNoAccount": "Vous n'avez pas de compte ?", "orgAuthNoAccount": "Vous n'avez pas de compte ?",
"subscriptionRequiredToUse": "Un abonnement est requis pour utiliser cette fonctionnalité.", "subscriptionRequiredToUse": "Un abonnement est requis pour utiliser cette fonctionnalité.",
"mustUpgradeToUse": "Vous devez mettre à jour votre abonnement pour utiliser cette fonctionnalité.",
"subscriptionRequiredTierToUse": "Cette fonctionnalité nécessite <tierLink>{tier}</tierLink> ou supérieur.",
"upgradeToTierToUse": "Passez à <tierLink>{tier}</tierLink> ou plus pour utiliser cette fonctionnalité.",
"subscriptionTierTier1": "Domicile",
"subscriptionTierTier2": "Equipe",
"subscriptionTierTier3": "Entreprise",
"subscriptionTierEnterprise": "Entreprise",
"idpDisabled": "Les fournisseurs d'identité sont désactivés.", "idpDisabled": "Les fournisseurs d'identité sont désactivés.",
"orgAuthPageDisabled": "La page d'authentification de l'organisation est désactivée.", "orgAuthPageDisabled": "La page d'authentification de l'organisation est désactivée.",
"domainRestartedDescription": "La vérification du domaine a été redémarrée avec succès", "domainRestartedDescription": "La vérification du domaine a été redémarrée avec succès",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Obtenir une licence",
"description": "Choisissez un plan et dites-nous comment vous comptez utiliser Pangolin.",
"chooseTier": "Choisissez votre forfait",
"viewPricingLink": "Voir les prix, les fonctionnalités et les limites",
"tiers": {
"starter": {
"title": "Démarrage",
"description": "Fonctionnalités d'entreprise, 25 utilisateurs, 25 sites et un support communautaire."
},
"scale": {
"title": "Échelle",
"description": "Fonctionnalités d'entreprise, 50 utilisateurs, 50 sites et une prise en charge prioritaire."
}
},
"personalUseOnly": "Utilisation personnelle uniquement (licence gratuite — sans checkout)",
"buttons": {
"continueToCheckout": "Continuer vers le paiement"
},
"toasts": {
"checkoutError": {
"title": "Erreur de paiement",
"description": "Impossible de commencer la commande. Veuillez réessayer."
}
}
},
"priority": "Priorité", "priority": "Priorité",
"priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.", "priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.",
"instanceName": "Nom de l'instance", "instanceName": "Nom de l'instance",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Fin de l'année suivante", "logRetentionEndOfFollowingYear": "Fin de l'année suivante",
"actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation", "actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation",
"accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation", "accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation",
"licenseRequiredToUse": "Une licence Entreprise est nécessaire pour utiliser cette fonctionnalité.", "licenseRequiredToUse": "Une licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> est nécessaire pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "La version <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Résolveur de certificat", "certResolver": "Résolveur de certificat",
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.", "certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
"selectCertResolver": "Sélectionnez le résolveur de certificat", "selectCertResolver": "Sélectionnez le résolveur de certificat",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Pare-feu activé", "firewallEnabled": "Pare-feu activé",
"autoUpdatesEnabled": "Mises à jour automatiques activées", "autoUpdatesEnabled": "Mises à jour automatiques activées",
"tpmAvailable": "TPM disponible", "tpmAvailable": "TPM disponible",
"windowsAntivirusEnabled": "Antivirus activé",
"macosSipEnabled": "Protection contre l'intégrité du système (SIP)", "macosSipEnabled": "Protection contre l'intégrité du système (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Mode furtif du pare-feu", "macosFirewallStealthMode": "Mode furtif du pare-feu",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.", "componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.",
"componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.", "componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.",
"dismiss": "Ignora", "dismiss": "Ignora",
"subscriptionViolationMessage": "Hai superato i tuoi limiti per il tuo piano attuale. Correggi il problema rimuovendo siti, utenti o altre risorse per rimanere all'interno del tuo piano.",
"subscriptionViolationViewBilling": "Visualizza fatturazione",
"componentsLicenseViolation": "Violazione della licenza: Questo server sta usando i siti {usedSites} che superano il suo limite concesso in licenza per i siti {maxSites} . Segui i termini di licenza per continuare a usare tutte le funzionalità.", "componentsLicenseViolation": "Violazione della licenza: Questo server sta usando i siti {usedSites} che superano il suo limite concesso in licenza per i siti {maxSites} . Segui i termini di licenza per continuare a usare tutte le funzionalità.",
"componentsSupporterMessage": "Grazie per aver supportato Pangolin come {tier}!", "componentsSupporterMessage": "Grazie per aver supportato Pangolin come {tier}!",
"inviteErrorNotValid": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia stato accettato o non sia più valido.", "inviteErrorNotValid": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia stato accettato o non sia più valido.",
@@ -199,6 +201,7 @@
"protocolSelect": "Seleziona un protocollo", "protocolSelect": "Seleziona un protocollo",
"resourcePortNumber": "Numero Porta", "resourcePortNumber": "Numero Porta",
"resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.", "resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.",
"back": "Indietro",
"cancel": "Annulla", "cancel": "Annulla",
"resourceConfig": "Snippet Di Configurazione", "resourceConfig": "Snippet Di Configurazione",
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP", "resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione dell'organizzazione.", "orgErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione dell'organizzazione.",
"orgDeleted": "Organizzazione eliminata", "orgDeleted": "Organizzazione eliminata",
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.", "orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
"deleteAccount": "Elimina Account",
"deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
"deleteAccountButton": "Elimina Account",
"deleteAccountConfirmTitle": "Elimina Account",
"deleteAccountConfirmMessage": "Questo cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
"deleteAccountConfirmString": "elimina account",
"deleteAccountSuccess": "Account Eliminato",
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
"deleteAccountError": "Impossibile eliminare l'account",
"deleteAccountPreviewAccount": "Il Tuo Account",
"deleteAccountPreviewOrgs": "Organizzazioni che possiedi (e tutti i loro dati)",
"orgMissing": "ID Organizzazione Mancante", "orgMissing": "ID Organizzazione Mancante",
"orgMissingMessage": "Impossibile rigenerare l'invito senza un ID organizzazione.", "orgMissingMessage": "Impossibile rigenerare l'invito senza un ID organizzazione.",
"accessUsersManage": "Gestisci Utenti", "accessUsersManage": "Gestisci Utenti",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtra Per Stato Di Approvazione", "filterByApprovalState": "Filtra Per Stato Di Approvazione",
"approvalListEmpty": "Nessuna approvazione", "approvalListEmpty": "Nessuna approvazione",
"approvalState": "Stato Di Approvazione", "approvalState": "Stato Di Approvazione",
"approvalLoadMore": "Carica altro",
"loadingApprovals": "Caricamento Approvazioni",
"approve": "Approva", "approve": "Approva",
"approved": "Approvato", "approved": "Approvato",
"denied": "Negato", "denied": "Negato",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Aumenta conteggio siti", "sitestCountIncrease": "Aumenta conteggio siti",
"idpManage": "Gestisci Provider di Identità", "idpManage": "Gestisci Provider di Identità",
"idpManageDescription": "Visualizza e gestisci i provider di identità nel sistema", "idpManageDescription": "Visualizza e gestisci i provider di identità nel sistema",
"idpGlobalModeBanner": "I provider di identità (IdP) per organizzazione sono disabilitati su questo server. Sta utilizzando IdP globali (condivisi in tutte le organizzazioni). Gestisci IdP globali nel pannello di amministrazione <adminPanelLink></adminPanelLink>. Per abilitare IdP per organizzazione, modificare la configurazione del server e impostare la modalità IdP su org. <configDocsLink>Vedere i documenti</configDocsLink>. Se si desidera continuare a utilizzare IdP globali e far sparire questo dalle impostazioni dell'organizzazione, impostare esplicitamente la modalità globale nella configurazione.",
"idpGlobalModeBannerUpgradeRequired": "I provider di identità (IdP) per organizzazione sono disabilitati su questo server. Utilizza IdP globali (condivisi tra tutte le organizzazioni). Gestisci gli IdP globali nel pannello di amministrazione <adminPanelLink></adminPanelLink>. Per utilizzare i provider di identità per organizzazione, è necessario aggiornare all'edizione Enterprise.",
"idpGlobalModeBannerLicenseRequired": "I provider di identità (IdP) per organizzazione sono disabilitati su questo server. Utilizza IdP globali (condivisi tra tutte le organizzazioni). Gestisci IdP globali nel pannello di amministrazione <adminPanelLink></adminPanelLink>. Per utilizzare provider di identità per organizzazione, è richiesta una licenza Enterprise.",
"idpDeletedDescription": "Provider di identità eliminato con successo", "idpDeletedDescription": "Provider di identità eliminato con successo",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Sei sicuro di voler eliminare definitivamente il provider di identità?", "idpQuestionRemove": "Sei sicuro di voler eliminare definitivamente il provider di identità?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Configurazione - Pangolin", "pangolinSetup": "Configurazione - Pangolin",
"orgNameRequired": "Il nome dell'organizzazione è obbligatorio", "orgNameRequired": "Il nome dell'organizzazione è obbligatorio",
"orgIdRequired": "L'ID dell'organizzazione è obbligatorio", "orgIdRequired": "L'ID dell'organizzazione è obbligatorio",
"orgIdMaxLength": "L'ID dell'organizzazione deve contenere al massimo 32 caratteri",
"orgErrorCreate": "Si è verificato un errore durante la creazione dell'organizzazione", "orgErrorCreate": "Si è verificato un errore durante la creazione dell'organizzazione",
"pageNotFound": "Pagina Non Trovata", "pageNotFound": "Pagina Non Trovata",
"pageNotFoundDescription": "Oops! La pagina che stai cercando non esiste.", "pageNotFoundDescription": "Oops! La pagina che stai cercando non esiste.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Visualizza Log", "actionViewLogs": "Visualizza Log",
"noneSelected": "Nessuna selezione", "noneSelected": "Nessuna selezione",
"orgNotFound2": "Nessuna organizzazione trovata.", "orgNotFound2": "Nessuna organizzazione trovata.",
"searchProgress": "Ricerca...", "searchPlaceholder": "Cerca...",
"emptySearchOptions": "Nessuna opzione trovata",
"create": "Crea", "create": "Crea",
"orgs": "Organizzazioni", "orgs": "Organizzazioni",
"loginError": "Si è verificato un errore imprevisto. Riprova.", "loginError": "Si è verificato un errore imprevisto. Riprova.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Log & Analytics", "sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Progetti", "sidebarBluePrints": "Progetti",
"sidebarOrganization": "Organizzazione", "sidebarOrganization": "Organizzazione",
"sidebarBillingAndLicenses": "Fatturazione E Licenze",
"sidebarLogsAnalytics": "Analisi", "sidebarLogsAnalytics": "Analisi",
"blueprints": "Progetti", "blueprints": "Progetti",
"blueprintsDescription": "Applica le configurazioni dichiarative e visualizza le partite precedenti", "blueprintsDescription": "Applica le configurazioni dichiarative e visualizza le partite precedenti",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Panoramica dei Limiti di Utilizzo", "billingUsageLimitsOverview": "Panoramica dei Limiti di Utilizzo",
"billingMonitorUsage": "Monitora il tuo utilizzo rispetto ai limiti configurati. Se hai bisogno di aumentare i limiti, contattaci all'indirizzo support@pangolin.net.", "billingMonitorUsage": "Monitora il tuo utilizzo rispetto ai limiti configurati. Se hai bisogno di aumentare i limiti, contattaci all'indirizzo support@pangolin.net.",
"billingDataUsage": "Utilizzo dei Dati", "billingDataUsage": "Utilizzo dei Dati",
"billingOnlineTime": "Tempo Online del Sito", "billingSites": "Siti",
"billingUsers": "Utenti Attivi", "billingUsers": "Utenti",
"billingDomains": "Domini Attivi", "billingDomains": "Domini",
"billingRemoteExitNodes": "Nodi Self-hosted Attivi", "billingOrganizations": "Organi",
"billingRemoteExitNodes": "Nodi Remoti",
"billingNoLimitConfigured": "Nessun limite configurato", "billingNoLimitConfigured": "Nessun limite configurato",
"billingEstimatedPeriod": "Periodo di Fatturazione Stimato", "billingEstimatedPeriod": "Periodo di Fatturazione Stimato",
"billingIncludedUsage": "Utilizzo Incluso", "billingIncludedUsage": "Utilizzo Incluso",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Errore durante l'ottenimento dell'URL del portale", "billingFailedToGetPortalUrl": "Errore durante l'ottenimento dell'URL del portale",
"billingPortalError": "Errore del Portale", "billingPortalError": "Errore del Portale",
"billingDataUsageInfo": "Hai addebitato tutti i dati trasferiti attraverso i tunnel sicuri quando sei connesso al cloud. Questo include sia il traffico in entrata e in uscita attraverso tutti i siti. Quando si raggiunge il limite, i siti si disconnetteranno fino a quando non si aggiorna il piano o si riduce l'utilizzo. I dati non vengono caricati quando si utilizzano nodi.", "billingDataUsageInfo": "Hai addebitato tutti i dati trasferiti attraverso i tunnel sicuri quando sei connesso al cloud. Questo include sia il traffico in entrata e in uscita attraverso tutti i siti. Quando si raggiunge il limite, i siti si disconnetteranno fino a quando non si aggiorna il piano o si riduce l'utilizzo. I dati non vengono caricati quando si utilizzano nodi.",
"billingOnlineTimeInfo": "Ti viene addebitato in base al tempo in cui i tuoi siti rimangono connessi al cloud. Ad esempio, 44,640 minuti è uguale a un sito in esecuzione 24/7 per un mese intero. Quando raggiungi il tuo limite, i tuoi siti si disconnetteranno fino a quando non aggiorni il tuo piano o riduci l'utilizzo. Il tempo non viene caricato quando si usano i nodi.", "billingSInfo": "Quanti siti puoi usare",
"billingUsersInfo": "Sei addebitato per ogni utente nell'organizzazione. La fatturazione viene calcolata quotidianamente in base al numero di account utente attivi nel tuo org.", "billingUsersInfo": "Quanti utenti puoi usare",
"billingDomainInfo": "Sei addebitato per ogni dominio nell'organizzazione. La fatturazione viene calcolata quotidianamente in base al numero di account di dominio attivi nel tuo org.", "billingDomainInfo": "Quanti domini puoi usare",
"billingRemoteExitNodesInfo": "Sei addebitato per ogni nodo gestito nell'organizzazione. La fatturazione viene calcolata quotidianamente in base al numero di nodi gestiti attivi nel tuo org.", "billingRemoteExitNodesInfo": "Quanti nodi remoti puoi usare",
"billingLicenseKeys": "Chiavi di Licenza",
"billingLicenseKeysDescription": "Gestisci le sottoscrizioni alla chiave di licenza",
"billingLicenseSubscription": "Abbonamento Licenza",
"billingInactive": "Inattivo",
"billingLicenseItem": "Elemento Licenza",
"billingQuantity": "Quantità",
"billingTotal": "totale",
"billingModifyLicenses": "Modifica Abbonamento Licenza",
"domainNotFound": "Domini Non Trovati", "domainNotFound": "Domini Non Trovati",
"domainNotFoundDescription": "Questa risorsa è disabilitata perché il dominio non esiste più nel nostro sistema. Si prega di impostare un nuovo dominio per questa risorsa.", "domainNotFoundDescription": "Questa risorsa è disabilitata perché il dominio non esiste più nel nostro sistema. Si prega di impostare un nuovo dominio per questa risorsa.",
"failed": "Fallito", "failed": "Fallito",
"createNewOrgDescription": "Crea una nuova organizzazione", "createNewOrgDescription": "Crea una nuova organizzazione",
"organization": "Organizzazione", "organization": "Organizzazione",
"primary": "Principale",
"port": "Porta", "port": "Porta",
"securityKeyManage": "Gestisci chiavi di sicurezza", "securityKeyManage": "Gestisci chiavi di sicurezza",
"securityKeyDescription": "Aggiungi o rimuovi chiavi di sicurezza per l'autenticazione senza password", "securityKeyDescription": "Aggiungi o rimuovi chiavi di sicurezza per l'autenticazione senza password",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP", "resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP",
"resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP", "resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP",
"billingPricingCalculatorLink": "Calcolatore di Prezzi", "billingPricingCalculatorLink": "Calcolatore di Prezzi",
"billingYourPlan": "Il Tuo Piano",
"billingViewOrModifyPlan": "Visualizza o modifica il tuo piano corrente",
"billingViewPlanDetails": "Visualizza Dettagli Piano",
"billingUsageAndLimits": "Utilizzo e limiti",
"billingViewUsageAndLimits": "Visualizza i limiti del tuo piano e l'utilizzo corrente",
"billingCurrentUsage": "Utilizzo Corrente",
"billingMaximumLimits": "Limiti Massimi",
"billingRemoteNodes": "Nodi Remoti",
"billingUnlimited": "Illimitato",
"billingPaidLicenseKeys": "Chiavi Di Licenza Pagate",
"billingManageLicenseSubscription": "Gestisci il tuo abbonamento per le chiavi di licenza self-hosted a pagamento",
"billingCurrentKeys": "Tasti Attuali",
"billingModifyCurrentPlan": "Modifica Il Piano Corrente",
"billingConfirmUpgrade": "Conferma Aggiornamento",
"billingConfirmDowngrade": "Conferma Downgrade",
"billingConfirmUpgradeDescription": "Stai per aggiornare il tuo piano. Controlla i nuovi limiti e prezzi qui sotto.",
"billingConfirmDowngradeDescription": "Stai per effettuare il downgrade del tuo piano. Controlla i nuovi limiti e i prezzi qui sotto.",
"billingPlanIncludes": "Piano Include",
"billingProcessing": "Elaborazione...",
"billingConfirmUpgradeButton": "Conferma Aggiornamento",
"billingConfirmDowngradeButton": "Conferma Downgrade",
"billingLimitViolationWarning": "Utilizzo Supera I Nuovi Limiti Del Piano",
"billingLimitViolationDescription": "Il tuo utilizzo attuale supera i limiti di questo piano. Dopo il downgrading, tutte le azioni saranno disabilitate fino a ridurre l'utilizzo entro i nuovi limiti. Si prega di rivedere le caratteristiche qui sotto che sono attualmente oltre i limiti. Limiti di violazione:",
"billingFeatureLossWarning": "Avviso Di Disponibilità Caratteristica",
"billingFeatureLossDescription": "Con il downgrading, le funzioni non disponibili nel nuovo piano saranno disattivate automaticamente. Alcune impostazioni e configurazioni potrebbero andare perse. Controlla la matrice dei prezzi per capire quali funzioni non saranno più disponibili.",
"billingUsageExceedsLimit": "L'utilizzo corrente ({current}) supera il limite ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Accetto i", "IAgreeToThe": "Accetto i",
"termsOfService": "termini di servizio", "termsOfService": "termini di servizio",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Sei sicuro di voler rimuovere il branding per le pagine di autenticazione?", "authPageBrandingQuestionRemove": "Sei sicuro di voler rimuovere il branding per le pagine di autenticazione?",
"authPageBrandingDeleteConfirm": "Conferma Eliminazione Branding", "authPageBrandingDeleteConfirm": "Conferma Eliminazione Branding",
"brandingLogoURL": "URL Logo", "brandingLogoURL": "URL Logo",
"brandingLogoURLOrPath": "URL o percorso del logo",
"brandingLogoPathDescription": "Inserisci un URL o un percorso locale.",
"brandingLogoURLDescription": "Inserisci un URL accessibile al pubblico per la tua immagine del logo.",
"brandingPrimaryColor": "Colore Primario", "brandingPrimaryColor": "Colore Primario",
"brandingLogoWidth": "Larghezza (px)", "brandingLogoWidth": "Larghezza (px)",
"brandingLogoHeight": "Altezza (px)", "brandingLogoHeight": "Altezza (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Torna alla modalità di accesso standard", "orgAuthBackToSignIn": "Torna alla modalità di accesso standard",
"orgAuthNoAccount": "Non hai un account?", "orgAuthNoAccount": "Non hai un account?",
"subscriptionRequiredToUse": "Per utilizzare questa funzionalità è necessario un abbonamento.", "subscriptionRequiredToUse": "Per utilizzare questa funzionalità è necessario un abbonamento.",
"mustUpgradeToUse": "Devi aggiornare il tuo abbonamento per utilizzare questa funzionalità.",
"subscriptionRequiredTierToUse": "Questa funzione richiede <tierLink>{tier}</tierLink> o superiore.",
"upgradeToTierToUse": "Aggiorna ad <tierLink>{tier}</tierLink> o superiore per utilizzare questa funzionalità.",
"subscriptionTierTier1": "Home",
"subscriptionTierTier2": "Squadra",
"subscriptionTierTier3": "Business",
"subscriptionTierEnterprise": "Impresa",
"idpDisabled": "I provider di identità sono disabilitati.", "idpDisabled": "I provider di identità sono disabilitati.",
"orgAuthPageDisabled": "La pagina di autenticazione dell'organizzazione è disabilitata.", "orgAuthPageDisabled": "La pagina di autenticazione dell'organizzazione è disabilitata.",
"domainRestartedDescription": "Verifica del dominio riavviata con successo", "domainRestartedDescription": "Verifica del dominio riavviata con successo",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Ottieni una licenza",
"description": "Scegli un piano e ci dica come intendi usare Pangolin.",
"chooseTier": "Scegli il tuo piano",
"viewPricingLink": "Vedi prezzi, funzionalità e limiti",
"tiers": {
"starter": {
"title": "Avviatore",
"description": "Caratteristiche aziendali, 25 utenti, 25 siti e supporto alla comunità."
},
"scale": {
"title": "Scala",
"description": "Funzionalità aziendali, 50 utenti, 50 siti e supporto prioritario."
}
},
"personalUseOnly": "Solo uso personale (licenza gratuita — nessun checkout)",
"buttons": {
"continueToCheckout": "Continua al Checkout"
},
"toasts": {
"checkoutError": {
"title": "Errore di pagamento",
"description": "Impossibile avviare il checkout. Per favore riprova."
}
}
},
"priority": "Priorità", "priority": "Priorità",
"priorityDescription": "I percorsi prioritari più alti sono valutati prima. Priorità = 100 significa ordinamento automatico (decidi di sistema). Usa un altro numero per applicare la priorità manuale.", "priorityDescription": "I percorsi prioritari più alti sono valutati prima. Priorità = 100 significa ordinamento automatico (decidi di sistema). Usa un altro numero per applicare la priorità manuale.",
"instanceName": "Nome Istanza", "instanceName": "Nome Istanza",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Fine dell'anno successivo", "logRetentionEndOfFollowingYear": "Fine dell'anno successivo",
"actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione", "actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione",
"accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione", "accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione",
"licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza Enterprise.", "licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> . Questa funzionalità è disponibile anche in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "L' <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> è necessaria per utilizzare questa funzione. Questa funzionalità è disponibile anche in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Risolutore Di Certificato", "certResolver": "Risolutore Di Certificato",
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.", "certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
"selectCertResolver": "Seleziona Risolutore Di Certificato", "selectCertResolver": "Seleziona Risolutore Di Certificato",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Firewall Abilitato", "firewallEnabled": "Firewall Abilitato",
"autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati", "autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati",
"tpmAvailable": "TPM Disponibile", "tpmAvailable": "TPM Disponibile",
"windowsAntivirusEnabled": "Antivirus Abilitato",
"macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)", "macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo Del Firewall", "macosFirewallStealthMode": "Modo Furtivo Del Firewall",

View File

@@ -18,6 +18,8 @@
"componentsMember": "당신은 {count, plural, =0 {조직이 없습니다} one {하나의 조직} other {# 개의 조직}}의 구성원입니다.", "componentsMember": "당신은 {count, plural, =0 {조직이 없습니다} one {하나의 조직} other {# 개의 조직}}의 구성원입니다.",
"componentsInvalidKey": "유효하지 않거나 만료된 라이센스 키가 감지되었습니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.", "componentsInvalidKey": "유효하지 않거나 만료된 라이센스 키가 감지되었습니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.",
"dismiss": "해제", "dismiss": "해제",
"subscriptionViolationMessage": "현재 계획의 한계를 초과했습니다. 사이트, 사용자 또는 기타 리소스를 제거하여 계획 내에 머물도록 해결하세요.",
"subscriptionViolationViewBilling": "청구 보기",
"componentsLicenseViolation": "라이센스 위반: 이 서버는 {usedSites} 사이트를 사용하고 있으며, 이는 {maxSites} 사이트의 라이센스 한도를 초과합니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.", "componentsLicenseViolation": "라이센스 위반: 이 서버는 {usedSites} 사이트를 사용하고 있으며, 이는 {maxSites} 사이트의 라이센스 한도를 초과합니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.",
"componentsSupporterMessage": "{tier}로 판골린을 지원해 주셔서 감사합니다!", "componentsSupporterMessage": "{tier}로 판골린을 지원해 주셔서 감사합니다!",
"inviteErrorNotValid": "죄송하지만, 접근하려는 초대가 수락되지 않았거나 더 이상 유효하지 않은 것 같습니다.", "inviteErrorNotValid": "죄송하지만, 접근하려는 초대가 수락되지 않았거나 더 이상 유효하지 않은 것 같습니다.",
@@ -199,6 +201,7 @@
"protocolSelect": "프로토콜 선택", "protocolSelect": "프로토콜 선택",
"resourcePortNumber": "포트 번호", "resourcePortNumber": "포트 번호",
"resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.", "resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.",
"back": "뒤로",
"cancel": "취소", "cancel": "취소",
"resourceConfig": "구성 스니펫", "resourceConfig": "구성 스니펫",
"resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣습니다.", "resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣습니다.",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "조직을 삭제하는 중 오류가 발생했습니다.", "orgErrorDeleteMessage": "조직을 삭제하는 중 오류가 발생했습니다.",
"orgDeleted": "조직이 삭제되었습니다.", "orgDeleted": "조직이 삭제되었습니다.",
"orgDeletedMessage": "조직과 그 데이터가 삭제되었습니다.", "orgDeletedMessage": "조직과 그 데이터가 삭제되었습니다.",
"deleteAccount": "계정 삭제",
"deleteAccountDescription": "계정, 소유한 모든 조직 및 조직 내의 모든 데이터를 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.",
"deleteAccountButton": "계정 삭제",
"deleteAccountConfirmTitle": "계정 삭제",
"deleteAccountConfirmMessage": "이 작업은 귀하의 계정, 소유한 모든 조직 및 조직 내 모든 데이터를 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.",
"deleteAccountConfirmString": "계정 삭제",
"deleteAccountSuccess": "계정 삭제됨",
"deleteAccountSuccessMessage": "계정이 삭제되었습니다.",
"deleteAccountError": "계정 삭제 실패",
"deleteAccountPreviewAccount": "귀하의 계정",
"deleteAccountPreviewOrgs": "귀하가 소유한 조직(포함된 모든 데이터)",
"orgMissing": "조직 ID가 누락되었습니다", "orgMissing": "조직 ID가 누락되었습니다",
"orgMissingMessage": "조직 ID 없이 초대장을 재생성할 수 없습니다.", "orgMissingMessage": "조직 ID 없이 초대장을 재생성할 수 없습니다.",
"accessUsersManage": "사용자 관리", "accessUsersManage": "사용자 관리",
@@ -459,6 +473,8 @@
"filterByApprovalState": "승인 상태로 필터링", "filterByApprovalState": "승인 상태로 필터링",
"approvalListEmpty": "승인이 없습니다.", "approvalListEmpty": "승인이 없습니다.",
"approvalState": "승인 상태", "approvalState": "승인 상태",
"approvalLoadMore": "더 불러오기",
"loadingApprovals": "승인 불러오는 중",
"approve": "승인", "approve": "승인",
"approved": "승인됨", "approved": "승인됨",
"denied": "거부됨", "denied": "거부됨",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "사이트 수 증가", "sitestCountIncrease": "사이트 수 증가",
"idpManage": "아이덴티티 공급자 관리", "idpManage": "아이덴티티 공급자 관리",
"idpManageDescription": "시스템에서 ID 제공자를 보고 관리합니다", "idpManageDescription": "시스템에서 ID 제공자를 보고 관리합니다",
"idpGlobalModeBanner": "조직별 신원 제공자(IdP)는 이 서버에서 비활성화되었습니다. 이 서버는 모든 조직에 걸쳐 공유된 글로벌 IdP를 사용 중입니다. <adminPanelLink>관리자 패널</adminPanelLink>에서 글로벌 IdP를 관리하십시오. 조직별 IdP를 활성화하려면 서버 설정을 편집하고 IdP 모드를 조직으로 설정하십시오. <configDocsLink>문서 보기</configDocsLink>. 글로벌 IdP 사용을 계속하고 조직 설정에서 이 항목을 제거하려면 설정에서 모드를 글로벌로 명시적으로 설정하십시오.",
"idpGlobalModeBannerUpgradeRequired": "조직별 신원 제공자(IdP)는 이 서버에서 비활성화되었습니다. 이 서버는 모든 조직에 걸쳐 공유된 글로벌 IdP를 사용 중입니다. <adminPanelLink>관리자 패널</adminPanelLink>에서 글로벌 IdP를 관리하십시오. 조직별 신원 제공자를 사용하려면 Enterprise 에디션으로 업그레이드해야 합니다.",
"idpGlobalModeBannerLicenseRequired": "조직별 신원 제공자(IdP)는 이 서버에서 비활성화되었습니다. 이 서버는 모든 조직에 걸쳐 공유된 글로벌 IdP를 사용 중입니다. <adminPanelLink>관리자 패널</adminPanelLink>에서 글로벌 IdP를 관리하십시오. 조직별 신원 제공자를 사용하려면 엔터프라이즈 라이선스가 필요합니다.",
"idpDeletedDescription": "신원 공급자가 성공적으로 삭제되었습니다", "idpDeletedDescription": "신원 공급자가 성공적으로 삭제되었습니다",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "아이덴티티 공급자를 영구적으로 삭제하시겠습니까?", "idpQuestionRemove": "아이덴티티 공급자를 영구적으로 삭제하시겠습니까?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "설정 - 판골린", "pangolinSetup": "설정 - 판골린",
"orgNameRequired": "조직 이름은 필수입니다.", "orgNameRequired": "조직 이름은 필수입니다.",
"orgIdRequired": "조직 ID가 필요합니다", "orgIdRequired": "조직 ID가 필요합니다",
"orgIdMaxLength": "조직 ID는 최대 32자 이내여야 합니다",
"orgErrorCreate": "조직 생성 중 오류가 발생했습니다.", "orgErrorCreate": "조직 생성 중 오류가 발생했습니다.",
"pageNotFound": "페이지를 찾을 수 없습니다", "pageNotFound": "페이지를 찾을 수 없습니다",
"pageNotFoundDescription": "앗! 찾고 있는 페이지가 존재하지 않습니다.", "pageNotFoundDescription": "앗! 찾고 있는 페이지가 존재하지 않습니다.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "로그 보기", "actionViewLogs": "로그 보기",
"noneSelected": "선택된 항목 없음", "noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.", "orgNotFound2": "조직이 없습니다.",
"searchProgress": "검색...", "searchPlaceholder": "검색...",
"emptySearchOptions": "옵션이 없습니다",
"create": "생성", "create": "생성",
"orgs": "조직", "orgs": "조직",
"loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.", "loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "로그 & 통계", "sidebarLogAndAnalytics": "로그 & 통계",
"sidebarBluePrints": "청사진", "sidebarBluePrints": "청사진",
"sidebarOrganization": "조직", "sidebarOrganization": "조직",
"sidebarBillingAndLicenses": "결제 및 라이선스",
"sidebarLogsAnalytics": "분석", "sidebarLogsAnalytics": "분석",
"blueprints": "청사진", "blueprints": "청사진",
"blueprintsDescription": "선언적 구성을 적용하고 이전 실행을 봅니다", "blueprintsDescription": "선언적 구성을 적용하고 이전 실행을 봅니다",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "사용 한도 개요", "billingUsageLimitsOverview": "사용 한도 개요",
"billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@pangolin.net로 연락하십시오.", "billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@pangolin.net로 연락하십시오.",
"billingDataUsage": "데이터 사용량", "billingDataUsage": "데이터 사용량",
"billingOnlineTime": "사이트 온라인 시간", "billingSites": "사이트",
"billingUsers": "활성 사용자", "billingUsers": "사용자",
"billingDomains": "활성 도메인", "billingDomains": "도메인",
"billingRemoteExitNodes": "활성 자체 호스팅 노드", "billingOrganizations": "조직",
"billingRemoteExitNodes": "원격 노드",
"billingNoLimitConfigured": "구성된 한도가 없습니다.", "billingNoLimitConfigured": "구성된 한도가 없습니다.",
"billingEstimatedPeriod": "예상 청구 기간", "billingEstimatedPeriod": "예상 청구 기간",
"billingIncludedUsage": "포함 사용량", "billingIncludedUsage": "포함 사용량",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "포털 URL을 가져오는 데 실패했습니다.", "billingFailedToGetPortalUrl": "포털 URL을 가져오는 데 실패했습니다.",
"billingPortalError": "포털 오류", "billingPortalError": "포털 오류",
"billingDataUsageInfo": "클라우드에 연결할 때 보안 터널을 통해 전송된 모든 데이터에 대해 비용이 청구됩니다. 여기에는 모든 사이트의 들어오고 나가는 트래픽이 포함됩니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용하는 경우 데이터는 요금이 청구되지 않습니다.", "billingDataUsageInfo": "클라우드에 연결할 때 보안 터널을 통해 전송된 모든 데이터에 대해 비용이 청구됩니다. 여기에는 모든 사이트의 들어오고 나가는 트래픽이 포함됩니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용하는 경우 데이터는 요금이 청구되지 않습니다.",
"billingOnlineTimeInfo": "사이트가 클라우드에 연결된 시간에 따라 요금이 청구됩니다. 예를 들어, 44,640분은 사이트가 한 달 내내 24시간 작동하는 것과 같습니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용할 때 시간은 요금이 청구되지 않습니다.", "billingSInfo": "사용할 수 있는 사이트 수",
"billingUsersInfo": "조직의 사용자마다 요금이 청구됩니다. 청구는 조직의 활성 사용자 계정 수에 따라 매일 계산됩니다.", "billingUsersInfo": "사용할 수 있는 사용자 수",
"billingDomainInfo": "조직의 도메인마다 요금이 청구됩니다. 청구는 조직의 활성 도메인 계정 수에 따라 매일 계산됩니다.", "billingDomainInfo": "사용할 수 있는 도메인 수",
"billingRemoteExitNodesInfo": "조직의 관리 노드마다 요금이 청구됩니다. 청구는 조직의 활성 관리 노드 수에 따라 매일 계산됩니다.", "billingRemoteExitNodesInfo": "사용할 수 있는 원격 노드 수",
"billingLicenseKeys": "라이센스 키",
"billingLicenseKeysDescription": "라이센스 키 구독을 관리하세요",
"billingLicenseSubscription": "라이센스 구독",
"billingInactive": "비활성화됨",
"billingLicenseItem": "라이센스 항목",
"billingQuantity": "수량",
"billingTotal": "총계",
"billingModifyLicenses": "라이센스 구독 수정",
"domainNotFound": "도메인을 찾을 수 없습니다", "domainNotFound": "도메인을 찾을 수 없습니다",
"domainNotFoundDescription": "이 리소스는 도메인이 더 이상 시스템에 존재하지 않아 비활성화되었습니다. 이 리소스에 대한 새 도메인을 설정하세요.", "domainNotFoundDescription": "이 리소스는 도메인이 더 이상 시스템에 존재하지 않아 비활성화되었습니다. 이 리소스에 대한 새 도메인을 설정하세요.",
"failed": "실패", "failed": "실패",
"createNewOrgDescription": "새 조직 생성", "createNewOrgDescription": "새 조직 생성",
"organization": "조직", "organization": "조직",
"primary": "기본",
"port": "포트", "port": "포트",
"securityKeyManage": "보안 키 관리", "securityKeyManage": "보안 키 관리",
"securityKeyDescription": "비밀번호 없는 인증을 위해 보안 키를 추가하거나 제거합니다.", "securityKeyDescription": "비밀번호 없는 인증을 위해 보안 키를 추가하거나 제거합니다.",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다", "resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다",
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요", "resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요",
"billingPricingCalculatorLink": "가격 계산기", "billingPricingCalculatorLink": "가격 계산기",
"billingYourPlan": "귀하의 계획",
"billingViewOrModifyPlan": "현재 계획 보기 또는 수정",
"billingViewPlanDetails": "계획 세부정보 보기",
"billingUsageAndLimits": "사용량 및 제한",
"billingViewUsageAndLimits": "계획의 제한 및 현재 사용량 보기",
"billingCurrentUsage": "현재 사용량",
"billingMaximumLimits": "최대 제한",
"billingRemoteNodes": "원격 노드",
"billingUnlimited": "무제한",
"billingPaidLicenseKeys": "유료 라이센스 키",
"billingManageLicenseSubscription": "유료 독립 호스트 라이센스 키를 위한 구독 관리",
"billingCurrentKeys": "현재 키",
"billingModifyCurrentPlan": "현재 계획 수정",
"billingConfirmUpgrade": "업그레이드 확인",
"billingConfirmDowngrade": "다운그레이드 확인",
"billingConfirmUpgradeDescription": "계획을 업그레이드하려고 합니다. 아래의 새로운 제한 및 가격을 검토하세요.",
"billingConfirmDowngradeDescription": "계획을 다운그레이드하려고 합니다. 아래의 새로운 제한 및 가격을 검토하세요.",
"billingPlanIncludes": "계획 포함",
"billingProcessing": "처리 중...",
"billingConfirmUpgradeButton": "업그레이드 확인",
"billingConfirmDowngradeButton": "다운그레이드 확인",
"billingLimitViolationWarning": "사용량이 새 계획의 제한을 초과합니다.",
"billingLimitViolationDescription": "현재 사용량이 이 계획의 제한을 초과합니다. 다운그레이드 후 모든 작업은 새로운 제한 내로 사용량을 줄일 때까지 비활성화됩니다. 현재 초과된 제한 특징들을 검토하세요. 위반된 제한:",
"billingFeatureLossWarning": "기능 가용성 알림",
"billingFeatureLossDescription": "다운그레이드함으로써 새 계획에서 사용할 수 없는 기능은 자동으로 비활성화됩니다. 일부 설정 및 구성은 손실될 수 있습니다. 어떤 기능들이 더 이상 사용 불가능한지 이해하기 위해 가격표를 검토하세요.",
"billingUsageExceedsLimit": "현재 사용량 ({current})이 제한 ({limit})을 초과합니다",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "동의합니다", "IAgreeToThe": "동의합니다",
"termsOfService": "서비스 약관", "termsOfService": "서비스 약관",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?", "authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?",
"authPageBrandingDeleteConfirm": "브랜딩 삭제 확인", "authPageBrandingDeleteConfirm": "브랜딩 삭제 확인",
"brandingLogoURL": "로고 URL", "brandingLogoURL": "로고 URL",
"brandingLogoURLOrPath": "로고 URL 또는 경로",
"brandingLogoPathDescription": "URL 또는 로컬 경로를 입력하세요.",
"brandingLogoURLDescription": "로고 이미지에 대한 공용 URL을 입력하십시오.",
"brandingPrimaryColor": "기본 색상", "brandingPrimaryColor": "기본 색상",
"brandingLogoWidth": "너비(px)", "brandingLogoWidth": "너비(px)",
"brandingLogoHeight": "높이(px)", "brandingLogoHeight": "높이(px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "표준 로그인을 통해 돌아가기", "orgAuthBackToSignIn": "표준 로그인을 통해 돌아가기",
"orgAuthNoAccount": "계정이 없으신가요?", "orgAuthNoAccount": "계정이 없으신가요?",
"subscriptionRequiredToUse": "이 기능을 사용하려면 구독이 필요합니다.", "subscriptionRequiredToUse": "이 기능을 사용하려면 구독이 필요합니다.",
"mustUpgradeToUse": "이 기능을 사용하려면 구독을 업그레이드해야 합니다.",
"subscriptionRequiredTierToUse": "이 기능을 사용하려면 <tierLink>{tier}</tierLink> 이상의 등급이 필요합니다.",
"upgradeToTierToUse": "이 기능을 사용하려면 <tierLink>{tier}</tierLink> 이상으로 업그레이드하세요.",
"subscriptionTierTier1": "홈",
"subscriptionTierTier2": "팀",
"subscriptionTierTier3": "비즈니스",
"subscriptionTierEnterprise": "기업",
"idpDisabled": "신원 공급자가 비활성화되었습니다.", "idpDisabled": "신원 공급자가 비활성화되었습니다.",
"orgAuthPageDisabled": "조직 인증 페이지가 비활성화되었습니다.", "orgAuthPageDisabled": "조직 인증 페이지가 비활성화되었습니다.",
"domainRestartedDescription": "도메인 인증이 성공적으로 재시작되었습니다.", "domainRestartedDescription": "도메인 인증이 성공적으로 재시작되었습니다.",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "라이센스 가져오기",
"description": "계획을 선택하고 Pangolin을 어떻게 사용할지 알려주세요.",
"chooseTier": "계획 선택",
"viewPricingLink": "가격, 기능 및 제한 보기",
"tiers": {
"starter": {
"title": "스타터",
"description": "기업 기능, 25명의 사용자, 25개의 사이트, 커뮤니티 지원."
},
"scale": {
"title": "스케일",
"description": "기업 기능, 50명의 사용자, 50개의 사이트, 우선 지원."
}
},
"personalUseOnly": "개인 사용 전용 (무료 라이센스 — 체크아웃 없음)",
"buttons": {
"continueToCheckout": "결제로 진행"
},
"toasts": {
"checkoutError": {
"title": "체크아웃 오류",
"description": "체크아웃을 시작할 수 없습니다. 다시 시도하세요."
}
}
},
"priority": "우선순위", "priority": "우선순위",
"priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.", "priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.",
"instanceName": "인스턴스 이름", "instanceName": "인스턴스 이름",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "다음 연도 말", "logRetentionEndOfFollowingYear": "다음 연도 말",
"actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다", "actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다",
"accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다", "accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다",
"licenseRequiredToUse": "이 기능을 사용하려면 Enterprise 라이선스가 필요합니다.", "licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다.",
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다.",
"certResolver": "인증서 해결사", "certResolver": "인증서 해결사",
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.", "certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
"selectCertResolver": "인증서 해결사 선택", "selectCertResolver": "인증서 해결사 선택",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "방화벽 활성화", "firewallEnabled": "방화벽 활성화",
"autoUpdatesEnabled": "자동 업데이트 활성화", "autoUpdatesEnabled": "자동 업데이트 활성화",
"tpmAvailable": "TPM 사용 가능", "tpmAvailable": "TPM 사용 가능",
"windowsAntivirusEnabled": "안티바이러스 활성화됨",
"macosSipEnabled": "시스템 무결성 보호 (SIP)", "macosSipEnabled": "시스템 무결성 보호 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "방화벽 스텔스 모드", "macosFirewallStealthMode": "방화벽 스텔스 모드",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Du er {count, plural, =0 {ikke medlem av noen organisasjoner} one {medlem av en organisasjon} other {medlem av # organisasjoner}}.", "componentsMember": "Du er {count, plural, =0 {ikke medlem av noen organisasjoner} one {medlem av en organisasjon} other {medlem av # organisasjoner}}.",
"componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", "componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.",
"dismiss": "Avvis", "dismiss": "Avvis",
"subscriptionViolationMessage": "Du er utenfor grensen for gjeldende plan. Rett problemet ved å fjerne nettsteder, brukere eller andre ressurser for å bli innenfor planen din.",
"subscriptionViolationViewBilling": "Vis fakturering",
"componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", "componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.",
"componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!", "componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!",
"inviteErrorNotValid": "Beklager, men det ser ut som invitasjonen du prøver å bruke ikke har blitt akseptert eller ikke er gyldig lenger.", "inviteErrorNotValid": "Beklager, men det ser ut som invitasjonen du prøver å bruke ikke har blitt akseptert eller ikke er gyldig lenger.",
@@ -199,6 +201,7 @@
"protocolSelect": "Velg en protokoll", "protocolSelect": "Velg en protokoll",
"resourcePortNumber": "Portnummer", "resourcePortNumber": "Portnummer",
"resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.", "resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.",
"back": "Tilbake",
"cancel": "Avbryt", "cancel": "Avbryt",
"resourceConfig": "Konfigurasjonsutdrag", "resourceConfig": "Konfigurasjonsutdrag",
"resourceConfigDescription": "Kopier og lim inn disse konfigurasjons-øyeblikkene for å sette opp TCP/UDP ressursen", "resourceConfigDescription": "Kopier og lim inn disse konfigurasjons-øyeblikkene for å sette opp TCP/UDP ressursen",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.", "orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.",
"orgDeleted": "Organisasjon slettet", "orgDeleted": "Organisasjon slettet",
"orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.", "orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.",
"deleteAccount": "Slett konto",
"deleteAccountDescription": "Slett kontoen din permanent, alle organisasjoner du eier, og alle data i disse organisasjonene. Dette kan ikke angres.",
"deleteAccountButton": "Slett konto",
"deleteAccountConfirmTitle": "Slett konto",
"deleteAccountConfirmMessage": "Dette vil slette kontoen din, alle organisasjoner du eier og alle data i disse organisasjonene. Dette kan ikke gjøres om.",
"deleteAccountConfirmString": "Slett konto",
"deleteAccountSuccess": "Kontoen er slettet",
"deleteAccountSuccessMessage": "Kontoen din er slettet.",
"deleteAccountError": "Kunne ikke slette konto",
"deleteAccountPreviewAccount": "Din konto",
"deleteAccountPreviewOrgs": "Organisasjoner du eier (og alle deres data)",
"orgMissing": "Organisasjons-ID Mangler", "orgMissing": "Organisasjons-ID Mangler",
"orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.", "orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.",
"accessUsersManage": "Administrer brukere", "accessUsersManage": "Administrer brukere",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtrer etter godkjenningsstatus", "filterByApprovalState": "Filtrer etter godkjenningsstatus",
"approvalListEmpty": "Ingen godkjenninger", "approvalListEmpty": "Ingen godkjenninger",
"approvalState": "Godkjennings tilstand", "approvalState": "Godkjennings tilstand",
"approvalLoadMore": "Last mer",
"loadingApprovals": "Laster inn godkjenninger",
"approve": "Godkjenn", "approve": "Godkjenn",
"approved": "Godkjent", "approved": "Godkjent",
"denied": "Avvist", "denied": "Avvist",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Øk antall områder", "sitestCountIncrease": "Øk antall områder",
"idpManage": "Administrer Identitetsleverandører", "idpManage": "Administrer Identitetsleverandører",
"idpManageDescription": "Vis og administrer identitetsleverandører i systemet", "idpManageDescription": "Vis og administrer identitetsleverandører i systemet",
"idpGlobalModeBanner": "Identitetsleverandører (IdPs) per organisasjon er deaktivert på denne serveren. Den bruker globale IdP (delt over alle organisasjoner). Administrer globale IdP'er i <adminPanelLink>admin-panelet</adminPanelLink>. For å aktivere IdP per organisasjon, rediger serverkonfigurasjonen og sett IdP-modus til org. <configDocsLink>Se dokumentasjonen</configDocsLink>. Hvis du vil fortsette å bruke globale IdPs og få denne til å forsvinne fra organisasjonens innstillinger, satt eksplisitt modusen til global i konfigurasjonen.",
"idpGlobalModeBannerUpgradeRequired": "Identitetsleverandører (IdPs) per organisasjon er deaktivert på denne serveren. Den bruker globale IdPs (delt på tvers av alle organisasjoner). Administrer globale IdPs i <adminPanelLink>administrasjons-panelet</adminPanelLink>. For å bruke identitetsleverandører per organisasjon, må du oppgradere til Enterprise-utgaven.",
"idpGlobalModeBannerLicenseRequired": "Identitetsleverandører (IdPs) per organisasjon er deaktivert på denne serveren. Den bruker globale IdPs (delt på tvers av alle organisasjoner). Administrer globale IdPs i <adminPanelLink>administrasjons-panelet</adminPanelLink>. For å bruke identitetsleverandører per organisasjon, kreves en Enterprise-lisens.",
"idpDeletedDescription": "Identitetsleverandør slettet vellykket", "idpDeletedDescription": "Identitetsleverandør slettet vellykket",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren permanent?", "idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren permanent?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Oppsett - Pangolin", "pangolinSetup": "Oppsett - Pangolin",
"orgNameRequired": "Organisasjonsnavn er påkrevd", "orgNameRequired": "Organisasjonsnavn er påkrevd",
"orgIdRequired": "Organisasjons-ID er påkrevd", "orgIdRequired": "Organisasjons-ID er påkrevd",
"orgIdMaxLength": "Organisasjons-ID må maksimalt være 32 tegn",
"orgErrorCreate": "En feil oppstod under oppretting av organisasjon", "orgErrorCreate": "En feil oppstod under oppretting av organisasjon",
"pageNotFound": "Siden ble ikke funnet", "pageNotFound": "Siden ble ikke funnet",
"pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.", "pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Vis logger", "actionViewLogs": "Vis logger",
"noneSelected": "Ingen valgt", "noneSelected": "Ingen valgt",
"orgNotFound2": "Ingen organisasjoner funnet.", "orgNotFound2": "Ingen organisasjoner funnet.",
"searchProgress": "Søker...", "searchPlaceholder": "Søk...",
"emptySearchOptions": "Ingen valg funnet",
"create": "Opprett", "create": "Opprett",
"orgs": "Organisasjoner", "orgs": "Organisasjoner",
"loginError": "En uventet feil oppstod. Vennligst prøv igjen.", "loginError": "En uventet feil oppstod. Vennligst prøv igjen.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Logg og analyser", "sidebarLogAndAnalytics": "Logg og analyser",
"sidebarBluePrints": "Tegninger", "sidebarBluePrints": "Tegninger",
"sidebarOrganization": "Organisasjon", "sidebarOrganization": "Organisasjon",
"sidebarBillingAndLicenses": "Fakturering & lisenser",
"sidebarLogsAnalytics": "Analyser", "sidebarLogsAnalytics": "Analyser",
"blueprints": "Tegninger", "blueprints": "Tegninger",
"blueprintsDescription": "Bruk deklarative konfigurasjoner og vis tidligere kjøringer", "blueprintsDescription": "Bruk deklarative konfigurasjoner og vis tidligere kjøringer",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Oversikt over bruksgrenser", "billingUsageLimitsOverview": "Oversikt over bruksgrenser",
"billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@pangolin.net.", "billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@pangolin.net.",
"billingDataUsage": "Databruk", "billingDataUsage": "Databruk",
"billingOnlineTime": "Online tid for nettsteder", "billingSites": "Områder",
"billingUsers": "Aktive brukere", "billingUsers": "Brukere",
"billingDomains": "Aktive domener", "billingDomains": "Domener",
"billingRemoteExitNodes": "Aktive selvstyrte noder", "billingOrganizations": "Orger",
"billingRemoteExitNodes": "Eksterne Noder",
"billingNoLimitConfigured": "Ingen grense konfigurert", "billingNoLimitConfigured": "Ingen grense konfigurert",
"billingEstimatedPeriod": "Estimert faktureringsperiode", "billingEstimatedPeriod": "Estimert faktureringsperiode",
"billingIncludedUsage": "Inkludert Bruk", "billingIncludedUsage": "Inkludert Bruk",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Mislyktes å hente portal URL", "billingFailedToGetPortalUrl": "Mislyktes å hente portal URL",
"billingPortalError": "Portalfeil", "billingPortalError": "Portalfeil",
"billingDataUsageInfo": "Du er ladet for all data som overføres gjennom dine sikre tunneler når du er koblet til skyen. Dette inkluderer både innkommende og utgående trafikk på alle dine nettsteder. Når du når grensen din, vil sidene koble fra til du oppgraderer planen eller reduserer bruken. Data belastes ikke ved bruk av EK-grupper.", "billingDataUsageInfo": "Du er ladet for all data som overføres gjennom dine sikre tunneler når du er koblet til skyen. Dette inkluderer både innkommende og utgående trafikk på alle dine nettsteder. Når du når grensen din, vil sidene koble fra til du oppgraderer planen eller reduserer bruken. Data belastes ikke ved bruk av EK-grupper.",
"billingOnlineTimeInfo": "Du er ladet på hvor lenge sidene dine forblir koblet til skyen. For eksempel tilsvarer 44,640 minutter ett nettsted som går 24/7 i en hel måned. Når du når grensen din, vil sidene koble fra til du oppgraderer planen eller reduserer bruken. Tid belastes ikke når du bruker noder.", "billingSInfo": "Hvor mange nettsteder du kan bruke",
"billingUsersInfo": "Du lades for hver bruker i organisasjonen. Fakturering beregnes daglig basert på antall aktive brukerkontoer i dine org.", "billingUsersInfo": "Hvor mange brukere du kan bruke",
"billingDomainInfo": "Du lades for hvert domene i organisasjonen. Fakturering beregnes daglig basert på antallet aktive domenekontoer i din org.", "billingDomainInfo": "Hvor mange domener du kan bruke",
"billingRemoteExitNodesInfo": "Du lades for hver håndterte node i organisasjonen. Fakturering beregnes daglig basert på antallet aktive håndterte noder i dine org.", "billingRemoteExitNodesInfo": "Hvor mange fjernnoder du kan bruke",
"billingLicenseKeys": "Lisensnøkler",
"billingLicenseKeysDescription": "Administrer dine lisensnøkkelabonnementer",
"billingLicenseSubscription": "Lisens abonnement",
"billingInactive": "Inaktiv",
"billingLicenseItem": "Lisens artikkel",
"billingQuantity": "Antall",
"billingTotal": "totalt",
"billingModifyLicenses": "Endre lisensabonnement",
"domainNotFound": "Domene ikke funnet", "domainNotFound": "Domene ikke funnet",
"domainNotFoundDescription": "Denne ressursen er deaktivert fordi domenet ikke lenger eksisterer i systemet vårt. Vennligst angi et nytt domene for denne ressursen.", "domainNotFoundDescription": "Denne ressursen er deaktivert fordi domenet ikke lenger eksisterer i systemet vårt. Vennligst angi et nytt domene for denne ressursen.",
"failed": "Mislyktes", "failed": "Mislyktes",
"createNewOrgDescription": "Opprett en ny organisasjon", "createNewOrgDescription": "Opprett en ny organisasjon",
"organization": "Organisasjon", "organization": "Organisasjon",
"primary": "Primær",
"port": "Port", "port": "Port",
"securityKeyManage": "Administrer sikkerhetsnøkler", "securityKeyManage": "Administrer sikkerhetsnøkler",
"securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering", "securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser", "resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser",
"resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser", "resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser",
"billingPricingCalculatorLink": "Pris Kalkulator", "billingPricingCalculatorLink": "Pris Kalkulator",
"billingYourPlan": "Din funksjonsplan",
"billingViewOrModifyPlan": "Vis eller endre gjeldende abonnement",
"billingViewPlanDetails": "Se Planleggings detaljer",
"billingUsageAndLimits": "Bruk og grenser",
"billingViewUsageAndLimits": "Se planets grenser og gjeldende bruk",
"billingCurrentUsage": "Gjeldende bruk",
"billingMaximumLimits": "Maks antall grenser",
"billingRemoteNodes": "Eksterne Noder",
"billingUnlimited": "Ubegrenset",
"billingPaidLicenseKeys": "Betalt lisensnøkler",
"billingManageLicenseSubscription": "Administrer abonnementet for betalte lisensnøkler selv hostet",
"billingCurrentKeys": "Nåværende nøkler",
"billingModifyCurrentPlan": "Endre gjeldende plan",
"billingConfirmUpgrade": "Bekreft oppgradering",
"billingConfirmDowngrade": "Bekreft nedgradering",
"billingConfirmUpgradeDescription": "Du er i ferd med å oppgradere abonnementet ditt. Gå gjennom de nye grensene og pris nedenfor.",
"billingConfirmDowngradeDescription": "Du er i ferd med å nedgradere planen din. Gå gjennom de nye grensene og pris nedenfor.",
"billingPlanIncludes": "Plan Inkluderer",
"billingProcessing": "Behandler...",
"billingConfirmUpgradeButton": "Bekreft oppgradering",
"billingConfirmDowngradeButton": "Bekreft nedgradering",
"billingLimitViolationWarning": "Bruk overbelastede grenser for ny plan",
"billingLimitViolationDescription": "Gjeldende bruk overskrider grensene for denne planen. Etter nedgradering vil alle handlinger deaktiveres inntil du reduserer bruken innenfor de nye grensene. Vennligst se igjennom funksjonene under som er i øyeblikket over grensene. Begrensninger i vold:",
"billingFeatureLossWarning": "Fremhev tilgjengelig varsel",
"billingFeatureLossDescription": "Ved å nedgradere vil funksjoner som ikke er tilgjengelige i den nye planen automatisk bli deaktivert. Noen innstillinger og konfigurasjoner kan gå tapt. Vennligst gjennomgå prismatrisen for å forstå hvilke funksjoner som ikke lenger vil være tilgjengelige.",
"billingUsageExceedsLimit": "Gjeldende bruk ({current}) overskrider grensen ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Jeg godtar", "IAgreeToThe": "Jeg godtar",
"termsOfService": "brukervilkårene", "termsOfService": "brukervilkårene",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?", "authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
"authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging", "authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logoen URL eller sti",
"brandingLogoPathDescription": "Skriv inn en URL eller en lokal bane.",
"brandingLogoURLDescription": "Skriv inn en offentlig tilgjengelig nettadresse til din logobilde.",
"brandingPrimaryColor": "Primærfarge", "brandingPrimaryColor": "Primærfarge",
"brandingLogoWidth": "Bredde (px)", "brandingLogoWidth": "Bredde (px)",
"brandingLogoHeight": "Høyde (px)", "brandingLogoHeight": "Høyde (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Tilbake til standard innlogging", "orgAuthBackToSignIn": "Tilbake til standard innlogging",
"orgAuthNoAccount": "Har du ikke konto?", "orgAuthNoAccount": "Har du ikke konto?",
"subscriptionRequiredToUse": "Et abonnement er påkrevd for å bruke denne funksjonen.", "subscriptionRequiredToUse": "Et abonnement er påkrevd for å bruke denne funksjonen.",
"mustUpgradeToUse": "Du må oppgradere ditt abonnement for å bruke denne funksjonen.",
"subscriptionRequiredTierToUse": "Denne funksjonen krever <tierLink>{tier}</tierLink> eller høyere.",
"upgradeToTierToUse": "Oppgrader til <tierLink>{tier}</tierLink> eller høyere for å bruke denne funksjonen.",
"subscriptionTierTier1": "Hjem",
"subscriptionTierTier2": "Lag",
"subscriptionTierTier3": "Forretninger",
"subscriptionTierEnterprise": "Bedrift",
"idpDisabled": "Identitetsleverandører er deaktivert.", "idpDisabled": "Identitetsleverandører er deaktivert.",
"orgAuthPageDisabled": "Informasjons-siden for organisasjon er deaktivert.", "orgAuthPageDisabled": "Informasjons-siden for organisasjon er deaktivert.",
"domainRestartedDescription": "Domene-verifiseringen ble startet på nytt", "domainRestartedDescription": "Domene-verifiseringen ble startet på nytt",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Få en lisens",
"description": "Velg en plan og fortell oss hvordan du planlegger å bruke Pangolin.",
"chooseTier": "Velg din funksjonsplan",
"viewPricingLink": "Se prising, egenskaper og grenser",
"tiers": {
"starter": {
"title": "Begynner",
"description": "Enterprise features, 25 brukere, 25 sitater og støtte fra fellesskapet."
},
"scale": {
"title": "Skala",
"description": "Enterprise features, 50 brukere, 50 nettsteder og prioritetsstøtte."
}
},
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen utsjekking)",
"buttons": {
"continueToCheckout": "Fortsett til kassen"
},
"toasts": {
"checkoutError": {
"title": "Feil ved utsjekk",
"description": "Kan ikke starte kassen. Prøv på nytt."
}
}
},
"priority": "Prioritet", "priority": "Prioritet",
"priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.", "priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.",
"instanceName": "Forekomst navn", "instanceName": "Forekomst navn",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Slutt på neste år", "logRetentionEndOfFollowingYear": "Slutt på neste år",
"actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen", "actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen",
"accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen", "accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen",
"licenseRequiredToUse": "En Enterprise lisens er påkrevd for å bruke denne funksjonen.", "licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens er påkrevd for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Sertifikat løser", "certResolver": "Sertifikat løser",
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.", "certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
"selectCertResolver": "Velg sertifikatløser", "selectCertResolver": "Velg sertifikatløser",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Brannmur aktivert", "firewallEnabled": "Brannmur aktivert",
"autoUpdatesEnabled": "Automatiske oppdateringer aktivert", "autoUpdatesEnabled": "Automatiske oppdateringer aktivert",
"tpmAvailable": "TPM tilgjengelig", "tpmAvailable": "TPM tilgjengelig",
"windowsAntivirusEnabled": "Antivirus aktivert",
"macosSipEnabled": "System Integritetsbeskyttelse (SIP)", "macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Brannmur Usynlig Modus", "macosFirewallStealthMode": "Brannmur Usynlig Modus",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Je bent lid van {count, plural, =0 {geen organisatie} one {één organisatie} other {# organisaties}}.", "componentsMember": "Je bent lid van {count, plural, =0 {geen organisatie} one {één organisatie} other {# organisaties}}.",
"componentsInvalidKey": "Ongeldige of verlopen licentiesleutels gedetecteerd. Volg de licentievoorwaarden om alle functies te blijven gebruiken.", "componentsInvalidKey": "Ongeldige of verlopen licentiesleutels gedetecteerd. Volg de licentievoorwaarden om alle functies te blijven gebruiken.",
"dismiss": "Uitschakelen", "dismiss": "Uitschakelen",
"subscriptionViolationMessage": "U overschrijdt uw huidige abonnement. Corrigeer het probleem door sites, gebruikers of andere bronnen te verwijderen om binnen uw plan te blijven.",
"subscriptionViolationViewBilling": "Facturering bekijken",
"componentsLicenseViolation": "Licentie overtreding: Deze server gebruikt {usedSites} sites die de gelicentieerde limiet van {maxSites} sites overschrijden. Volg de licentievoorwaarden om door te gaan met het gebruik van alle functies.", "componentsLicenseViolation": "Licentie overtreding: Deze server gebruikt {usedSites} sites die de gelicentieerde limiet van {maxSites} sites overschrijden. Volg de licentievoorwaarden om door te gaan met het gebruik van alle functies.",
"componentsSupporterMessage": "Bedankt voor het ondersteunen van Pangolin als {tier}!", "componentsSupporterMessage": "Bedankt voor het ondersteunen van Pangolin als {tier}!",
"inviteErrorNotValid": "Het spijt ons, maar de uitnodiging die je probeert te bezoeken is niet geaccepteerd of is niet meer geldig.", "inviteErrorNotValid": "Het spijt ons, maar de uitnodiging die je probeert te bezoeken is niet geaccepteerd of is niet meer geldig.",
@@ -199,6 +201,7 @@
"protocolSelect": "Selecteer een protocol", "protocolSelect": "Selecteer een protocol",
"resourcePortNumber": "Nummer van poort", "resourcePortNumber": "Nummer van poort",
"resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.", "resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.",
"back": "Achterzijde",
"cancel": "Annuleren", "cancel": "Annuleren",
"resourceConfig": "Configuratie tekstbouwstenen", "resourceConfig": "Configuratie tekstbouwstenen",
"resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om de TCP/UDP-bron in te stellen", "resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om de TCP/UDP-bron in te stellen",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Er is een fout opgetreden tijdens het verwijderen van de organisatie.", "orgErrorDeleteMessage": "Er is een fout opgetreden tijdens het verwijderen van de organisatie.",
"orgDeleted": "Organisatie verwijderd", "orgDeleted": "Organisatie verwijderd",
"orgDeletedMessage": "De organisatie en haar gegevens zijn verwijderd.", "orgDeletedMessage": "De organisatie en haar gegevens zijn verwijderd.",
"deleteAccount": "Verwijder account",
"deleteAccountDescription": "Verwijdert permanent uw account, alle organisaties die u bezit, en alle gegevens binnen deze organisaties. Dit kan niet ongedaan worden gemaakt.",
"deleteAccountButton": "Verwijder account",
"deleteAccountConfirmTitle": "Verwijder account",
"deleteAccountConfirmMessage": "Dit zal uw account permanent wissen, alle organisaties die u bezit, en alle gegevens binnen deze organisaties. Dit kan niet ongedaan worden gemaakt.",
"deleteAccountConfirmString": "verwijder account",
"deleteAccountSuccess": "Account verwijderd",
"deleteAccountSuccessMessage": "Uw account is verwijderd.",
"deleteAccountError": "Kan account niet verwijderen",
"deleteAccountPreviewAccount": "Uw account",
"deleteAccountPreviewOrgs": "Organisaties die je bezit (en al hun gegevens)",
"orgMissing": "Organisatie-ID ontbreekt", "orgMissing": "Organisatie-ID ontbreekt",
"orgMissingMessage": "Niet in staat om de uitnodiging te regenereren zonder organisatie-ID.", "orgMissingMessage": "Niet in staat om de uitnodiging te regenereren zonder organisatie-ID.",
"accessUsersManage": "Gebruikers beheren", "accessUsersManage": "Gebruikers beheren",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filter op goedkeuringsstatus", "filterByApprovalState": "Filter op goedkeuringsstatus",
"approvalListEmpty": "Geen goedkeuringen", "approvalListEmpty": "Geen goedkeuringen",
"approvalState": "Goedkeuring status", "approvalState": "Goedkeuring status",
"approvalLoadMore": "Meer laden",
"loadingApprovals": "Goedkeuringen laden",
"approve": "Goedkeuren", "approve": "Goedkeuren",
"approved": "Goedgekeurd", "approved": "Goedgekeurd",
"denied": "Geweigerd", "denied": "Geweigerd",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Toename van site vergroten", "sitestCountIncrease": "Toename van site vergroten",
"idpManage": "Identiteitsaanbieders beheren", "idpManage": "Identiteitsaanbieders beheren",
"idpManageDescription": "Identiteitsaanbieders in het systeem bekijken en beheren", "idpManageDescription": "Identiteitsaanbieders in het systeem bekijken en beheren",
"idpGlobalModeBanner": "Identiteitsaanbieders (IdPs) per organisatie zijn uitgeschakeld op deze server. Het gebruikt globale IdPs (gedeeld tussen alle organisaties). Beheer globale IdPs in het <adminPanelLink>beheerderspaneel</adminPanelLink>. Om IdPs per organisatie in te schakelen, bewerk de server configuratie en zet IdP modus op org. <configDocsLink>Zie de documenten</configDocsLink>. Als je globale IdPs wilt blijven gebruiken en dit uit de organisatie-instellingen wilt laten verdwijnen, zet dan expliciet de modus naar globaal in de config.",
"idpGlobalModeBannerUpgradeRequired": "Identity providers (IdPs) per organisatie zijn uitgeschakeld op deze server. Het gebruikt globale IdPs (gedeeld in alle organisaties) Beheer globale IdPs in het <adminPanelLink>beheerderspaneel</adminPanelLink>. Om identiteitsproviders per organisatie te gebruiken, moet u upgraden naar de Enterprise editie.",
"idpGlobalModeBannerLicenseRequired": "Identity providers (IdPs) per organisatie zijn uitgeschakeld op deze server. Het gebruikt globale IdPs (gedeeld in alle organisaties) Beheer globale IdPs in het <adminPanelLink>beheerderspaneel</adminPanelLink>. Om identiteitsaanbieders per organisatie te gebruiken, is een Enterprise-licentie vereist.",
"idpDeletedDescription": "Identity provider succesvol verwijderd", "idpDeletedDescription": "Identity provider succesvol verwijderd",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Weet u zeker dat u de identiteitsprovider permanent wilt verwijderen?", "idpQuestionRemove": "Weet u zeker dat u de identiteitsprovider permanent wilt verwijderen?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Instellen - Pangolin", "pangolinSetup": "Instellen - Pangolin",
"orgNameRequired": "Organisatienaam is vereist", "orgNameRequired": "Organisatienaam is vereist",
"orgIdRequired": "Organisatie-ID is vereist", "orgIdRequired": "Organisatie-ID is vereist",
"orgIdMaxLength": "Organisatie-ID mag maximaal 32 tekens lang zijn",
"orgErrorCreate": "Fout opgetreden tijdens het aanmaken org", "orgErrorCreate": "Fout opgetreden tijdens het aanmaken org",
"pageNotFound": "Pagina niet gevonden", "pageNotFound": "Pagina niet gevonden",
"pageNotFoundDescription": "Oeps! De pagina die je zoekt bestaat niet.", "pageNotFoundDescription": "Oeps! De pagina die je zoekt bestaat niet.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Logboeken bekijken", "actionViewLogs": "Logboeken bekijken",
"noneSelected": "Niet geselecteerd", "noneSelected": "Niet geselecteerd",
"orgNotFound2": "Geen organisaties gevonden.", "orgNotFound2": "Geen organisaties gevonden.",
"searchProgress": "Zoeken...", "searchPlaceholder": "Zoeken...",
"emptySearchOptions": "Geen opties gevonden",
"create": "Aanmaken", "create": "Aanmaken",
"orgs": "Organisaties", "orgs": "Organisaties",
"loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.", "loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Log & Analytics", "sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blauwdrukken", "sidebarBluePrints": "Blauwdrukken",
"sidebarOrganization": "Organisatie", "sidebarOrganization": "Organisatie",
"sidebarBillingAndLicenses": "Facturatie & Licenties",
"sidebarLogsAnalytics": "Analyses", "sidebarLogsAnalytics": "Analyses",
"blueprints": "Blauwdrukken", "blueprints": "Blauwdrukken",
"blueprintsDescription": "Gebruik declaratieve configuraties en bekijk vorige uitvoeringen.", "blueprintsDescription": "Gebruik declaratieve configuraties en bekijk vorige uitvoeringen.",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Overzicht gebruikslimieten", "billingUsageLimitsOverview": "Overzicht gebruikslimieten",
"billingMonitorUsage": "Houd uw gebruik in de gaten ten opzichte van de ingestelde limieten. Als u verhoogde limieten nodig heeft, neem dan contact met ons op support@pangolin.net.", "billingMonitorUsage": "Houd uw gebruik in de gaten ten opzichte van de ingestelde limieten. Als u verhoogde limieten nodig heeft, neem dan contact met ons op support@pangolin.net.",
"billingDataUsage": "Gegevensgebruik", "billingDataUsage": "Gegevensgebruik",
"billingOnlineTime": "Site Online Tijd", "billingSites": "Sites",
"billingUsers": "Actieve Gebruikers", "billingUsers": "Gebruikers",
"billingDomains": "Actieve Domeinen", "billingDomains": "Domeinen",
"billingRemoteExitNodes": "Actieve Zelfgehoste Nodes", "billingOrganizations": "Ordenen",
"billingRemoteExitNodes": "Externe knooppunten",
"billingNoLimitConfigured": "Geen limiet ingesteld", "billingNoLimitConfigured": "Geen limiet ingesteld",
"billingEstimatedPeriod": "Geschatte Facturatie Periode", "billingEstimatedPeriod": "Geschatte Facturatie Periode",
"billingIncludedUsage": "Opgenomen Gebruik", "billingIncludedUsage": "Opgenomen Gebruik",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Niet gelukt om portal URL te krijgen", "billingFailedToGetPortalUrl": "Niet gelukt om portal URL te krijgen",
"billingPortalError": "Portal Fout", "billingPortalError": "Portal Fout",
"billingDataUsageInfo": "U bent in rekening gebracht voor alle gegevens die via uw beveiligde tunnels via de cloud worden verzonden. Dit omvat zowel inkomende als uitgaande verkeer over al uw sites. Wanneer u uw limiet bereikt zullen uw sites de verbinding verbreken totdat u uw abonnement upgradet of het gebruik vermindert. Gegevens worden niet in rekening gebracht bij het gebruik van knooppunten.", "billingDataUsageInfo": "U bent in rekening gebracht voor alle gegevens die via uw beveiligde tunnels via de cloud worden verzonden. Dit omvat zowel inkomende als uitgaande verkeer over al uw sites. Wanneer u uw limiet bereikt zullen uw sites de verbinding verbreken totdat u uw abonnement upgradet of het gebruik vermindert. Gegevens worden niet in rekening gebracht bij het gebruik van knooppunten.",
"billingOnlineTimeInfo": "U wordt in rekening gebracht op basis van hoe lang uw sites verbonden blijven met de cloud. Bijvoorbeeld 44,640 minuten is gelijk aan één site met 24/7 voor een volledige maand. Wanneer u uw limiet bereikt, zal de verbinding tussen uw sites worden verbroken totdat u een upgrade van uw abonnement uitvoert of het gebruik vermindert. Tijd wordt niet belast bij het gebruik van knooppunten.", "billingSInfo": "Hoeveel sites u kunt gebruiken",
"billingUsersInfo": "U bent in rekening gebracht voor elke gebruiker in de organisatie. Facturering wordt dagelijks berekend op basis van het aantal actieve gebruikersaccounts in uw org.", "billingUsersInfo": "Hoeveel gebruikers je kan gebruiken",
"billingDomainInfo": "U wordt voor elk domein in de organisatie in rekening gebracht. Facturering wordt dagelijks berekend op basis van het aantal actieve domeinaccounts in uw org.", "billingDomainInfo": "Hoeveel domeinen je kunt gebruiken",
"billingRemoteExitNodesInfo": "U bent belast voor elke beheerde node in de organisatie. Facturering wordt dagelijks berekend op basis van het aantal actieve beheerde knooppunten in uw org.", "billingRemoteExitNodesInfo": "Hoeveel externe nodes je kunt gebruiken",
"billingLicenseKeys": "Licentie Sleutels",
"billingLicenseKeysDescription": "Beheer uw licentiesleutelabonnementen",
"billingLicenseSubscription": "Licentie abonnement",
"billingInactive": "Inactief",
"billingLicenseItem": "Licentie artikel",
"billingQuantity": "Aantal",
"billingTotal": "totaal",
"billingModifyLicenses": "Licentieabonnement wijzigen",
"domainNotFound": "Domein niet gevonden", "domainNotFound": "Domein niet gevonden",
"domainNotFoundDescription": "Deze bron is uitgeschakeld omdat het domein niet langer in ons systeem bestaat. Stel een nieuw domein in voor deze bron.", "domainNotFoundDescription": "Deze bron is uitgeschakeld omdat het domein niet langer in ons systeem bestaat. Stel een nieuw domein in voor deze bron.",
"failed": "Mislukt", "failed": "Mislukt",
"createNewOrgDescription": "Maak een nieuwe organisatie", "createNewOrgDescription": "Maak een nieuwe organisatie",
"organization": "Organisatie", "organization": "Organisatie",
"primary": "Primair",
"port": "Poort", "port": "Poort",
"securityKeyManage": "Beveiligingssleutels beheren", "securityKeyManage": "Beveiligingssleutels beheren",
"securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie", "securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen", "resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen",
"resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen", "resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen",
"billingPricingCalculatorLink": "Prijs Calculator", "billingPricingCalculatorLink": "Prijs Calculator",
"billingYourPlan": "Uw abonnement",
"billingViewOrModifyPlan": "Bekijk of wijzig uw huidige abonnement",
"billingViewPlanDetails": "Abonnementsdetails bekijken",
"billingUsageAndLimits": "Gebruik en limieten",
"billingViewUsageAndLimits": "Limiet van je abonnement en huidig gebruik bekijken",
"billingCurrentUsage": "Huidig gebruik",
"billingMaximumLimits": "Maximaal aantal limieten",
"billingRemoteNodes": "Externe knooppunten",
"billingUnlimited": "Onbeperkt",
"billingPaidLicenseKeys": "Betaalde licentiesleutels",
"billingManageLicenseSubscription": "Beheer je abonnement voor betaalde zelf gehoste licentiesleutels",
"billingCurrentKeys": "Huidige toetsen",
"billingModifyCurrentPlan": "Huidig plan wijzigen",
"billingConfirmUpgrade": "Bevestig Upgrade",
"billingConfirmDowngrade": "Downgraden bevestigen",
"billingConfirmUpgradeDescription": "U staat op het punt uw abonnement te upgraden. Controleer de nieuwe limieten en prijzen hieronder.",
"billingConfirmDowngradeDescription": "U staat op het punt om uw abonnement te downgraden. Controleer de nieuwe limieten en prijzen hieronder.",
"billingPlanIncludes": "Abonnement bevat",
"billingProcessing": "Verwerken...",
"billingConfirmUpgradeButton": "Bevestig Upgrade",
"billingConfirmDowngradeButton": "Downgraden bevestigen",
"billingLimitViolationWarning": "Gebruik Overschrijdt nieuwe Plan Limieten",
"billingLimitViolationDescription": "Uw huidige verbruik overschrijdt de limieten van dit plan. Na het downgraden worden alle acties uitgeschakeld totdat u het verbruik vermindert binnen de nieuwe grenzen. Controleer de onderstaande functies die de limieten overschrijden. Beperkingen in overtreding:",
"billingFeatureLossWarning": "Kennisgeving beschikbaarheid",
"billingFeatureLossDescription": "Door downgraden worden functies die niet beschikbaar zijn in het nieuwe abonnement automatisch uitgeschakeld. Sommige instellingen en configuraties kunnen verloren gaan. Raadpleeg de prijsmatrix om te begrijpen welke functies niet langer beschikbaar zijn.",
"billingUsageExceedsLimit": "Huidig gebruik ({current}) overschrijdt limiet ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Ik ga akkoord met de", "IAgreeToThe": "Ik ga akkoord met de",
"termsOfService": "servicevoorwaarden", "termsOfService": "servicevoorwaarden",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?", "authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?",
"authPageBrandingDeleteConfirm": "Bevestig verwijder Branding", "authPageBrandingDeleteConfirm": "Bevestig verwijder Branding",
"brandingLogoURL": "Het logo-URL", "brandingLogoURL": "Het logo-URL",
"brandingLogoURLOrPath": "Logo URL of pad",
"brandingLogoPathDescription": "Voer een URL of een lokaal pad in.",
"brandingLogoURLDescription": "Voer een openbaar toegankelijke URL in voor uw logo afbeelding.",
"brandingPrimaryColor": "Primaire kleur", "brandingPrimaryColor": "Primaire kleur",
"brandingLogoWidth": "Breedte (px)", "brandingLogoWidth": "Breedte (px)",
"brandingLogoHeight": "Hoogte (px)", "brandingLogoHeight": "Hoogte (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Terug naar standaard aanmelden", "orgAuthBackToSignIn": "Terug naar standaard aanmelden",
"orgAuthNoAccount": "Nog geen account?", "orgAuthNoAccount": "Nog geen account?",
"subscriptionRequiredToUse": "Een abonnement is vereist om deze functie te gebruiken.", "subscriptionRequiredToUse": "Een abonnement is vereist om deze functie te gebruiken.",
"mustUpgradeToUse": "U moet uw abonnement upgraden om deze functie te gebruiken.",
"subscriptionRequiredTierToUse": "Deze functie vereist <tierLink>{tier}</tierLink> of hoger.",
"upgradeToTierToUse": "Upgrade naar <tierLink>{tier}</tierLink> of hoger om deze functie te gebruiken.",
"subscriptionTierTier1": "Startpagina",
"subscriptionTierTier2": "Team",
"subscriptionTierTier3": "Bedrijfsleven",
"subscriptionTierEnterprise": "Onderneming",
"idpDisabled": "Identiteitsaanbieders zijn uitgeschakeld.", "idpDisabled": "Identiteitsaanbieders zijn uitgeschakeld.",
"orgAuthPageDisabled": "Pagina voor organisatie-authenticatie is uitgeschakeld.", "orgAuthPageDisabled": "Pagina voor organisatie-authenticatie is uitgeschakeld.",
"domainRestartedDescription": "Domeinverificatie met succes opnieuw gestart", "domainRestartedDescription": "Domeinverificatie met succes opnieuw gestart",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Krijg een licentie",
"description": "Kies een plan en vertel ons hoe u Pangolin wilt gebruiken.",
"chooseTier": "Kies uw abonnement",
"viewPricingLink": "Zie prijzen, functies en limieten",
"tiers": {
"starter": {
"title": "Beginner",
"description": "Enterprise functies, 25 gebruikers, 25 sites en community ondersteuning."
},
"scale": {
"title": "Schaal",
"description": "Enterprise functies, 50 gebruikers, 50 sites en prioriteit ondersteuning."
}
},
"personalUseOnly": "Alleen persoonlijk gebruik (gratis licentie - geen afrekenen)",
"buttons": {
"continueToCheckout": "Doorgaan naar afrekenen"
},
"toasts": {
"checkoutError": {
"title": "Fout bij afrekenen",
"description": "Kan de afhandeling niet starten. Probeer het opnieuw."
}
}
},
"priority": "Prioriteit", "priority": "Prioriteit",
"priorityDescription": "routes met hogere prioriteit worden eerst geëvalueerd. Prioriteit = 100 betekent automatisch bestellen (systeem beslist de). Gebruik een ander nummer om handmatige prioriteit af te dwingen.", "priorityDescription": "routes met hogere prioriteit worden eerst geëvalueerd. Prioriteit = 100 betekent automatisch bestellen (systeem beslist de). Gebruik een ander nummer om handmatige prioriteit af te dwingen.",
"instanceName": "Naam instantie", "instanceName": "Naam instantie",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Einde van volgend jaar", "logRetentionEndOfFollowingYear": "Einde van volgend jaar",
"actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie", "actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie",
"accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken", "accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken",
"licenseRequiredToUse": "Een Enterprise-licentie is vereist om deze functie te gebruiken.", "licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "De <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Certificaat Resolver", "certResolver": "Certificaat Resolver",
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.", "certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
"selectCertResolver": "Certificaat Resolver selecteren", "selectCertResolver": "Certificaat Resolver selecteren",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Firewall ingeschakeld", "firewallEnabled": "Firewall ingeschakeld",
"autoUpdatesEnabled": "Auto Updates Ingeschakeld", "autoUpdatesEnabled": "Auto Updates Ingeschakeld",
"tpmAvailable": "TPM beschikbaar", "tpmAvailable": "TPM beschikbaar",
"windowsAntivirusEnabled": "Antivirus ingeschakeld",
"macosSipEnabled": "Systeemintegriteitsbescherming (SIP)", "macosSipEnabled": "Systeemintegriteitsbescherming (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Verberg Modus", "macosFirewallStealthMode": "Firewall Verberg Modus",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Jesteś członkiem {count, plural, =0 {żadna organizacja} one {jedna organizacja} few {# organizacje} many {# organizacji} other {# organizacji}}.", "componentsMember": "Jesteś członkiem {count, plural, =0 {żadna organizacja} one {jedna organizacja} few {# organizacje} many {# organizacji} other {# organizacji}}.",
"componentsInvalidKey": "Wykryto nieprawidłowe lub wygasłe klucze licencyjne. Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.", "componentsInvalidKey": "Wykryto nieprawidłowe lub wygasłe klucze licencyjne. Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.",
"dismiss": "Odrzuć", "dismiss": "Odrzuć",
"subscriptionViolationMessage": "Nie masz ograniczeń dla aktualnego planu. Popraw problem poprzez usunięcie stron, użytkowników lub innych zasobów, aby pozostać w swoim planie.",
"subscriptionViolationViewBilling": "Zobacz rozliczenie",
"componentsLicenseViolation": "Naruszenie licencji: Ten serwer używa stron {usedSites} , które przekraczają limit licencyjny stron {maxSites} . Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.", "componentsLicenseViolation": "Naruszenie licencji: Ten serwer używa stron {usedSites} , które przekraczają limit licencyjny stron {maxSites} . Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.",
"componentsSupporterMessage": "Dziękujemy za wsparcie Pangolina jako {tier}!", "componentsSupporterMessage": "Dziękujemy za wsparcie Pangolina jako {tier}!",
"inviteErrorNotValid": "Przykro nam, ale wygląda na to, że zaproszenie, do którego próbujesz uzyskać dostęp, nie zostało zaakceptowane lub jest już nieważne.", "inviteErrorNotValid": "Przykro nam, ale wygląda na to, że zaproszenie, do którego próbujesz uzyskać dostęp, nie zostało zaakceptowane lub jest już nieważne.",
@@ -199,6 +201,7 @@
"protocolSelect": "Wybierz protokół", "protocolSelect": "Wybierz protokół",
"resourcePortNumber": "Numer portu", "resourcePortNumber": "Numer portu",
"resourcePortNumberDescription": "Numer portu zewnętrznego do żądań proxy.", "resourcePortNumberDescription": "Numer portu zewnętrznego do żądań proxy.",
"back": "Powrót",
"cancel": "Anuluj", "cancel": "Anuluj",
"resourceConfig": "Snippety konfiguracji", "resourceConfig": "Snippety konfiguracji",
"resourceConfigDescription": "Skopiuj i wklej te fragmenty konfiguracji, aby skonfigurować zasób TCP/UDP", "resourceConfigDescription": "Skopiuj i wklej te fragmenty konfiguracji, aby skonfigurować zasób TCP/UDP",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Wystąpił błąd podczas usuwania organizacji.", "orgErrorDeleteMessage": "Wystąpił błąd podczas usuwania organizacji.",
"orgDeleted": "Organizacja usunięta", "orgDeleted": "Organizacja usunięta",
"orgDeletedMessage": "Organizacja i jej dane zostały usunięte.", "orgDeletedMessage": "Organizacja i jej dane zostały usunięte.",
"deleteAccount": "Usuń konto",
"deleteAccountDescription": "Trwale usuń swoje konto, wszystkie organizacje, które posiadasz, oraz wszystkie dane w ramach tych organizacji. Tej operacji nie można cofnąć.",
"deleteAccountButton": "Usuń konto",
"deleteAccountConfirmTitle": "Usuń konto",
"deleteAccountConfirmMessage": "Spowoduje to trwałe usunięcie konta, wszystkich organizacji, które posiadasz, oraz wszystkich danych w tych organizacjach. Tej operacji nie można cofnąć.",
"deleteAccountConfirmString": "usuń konto",
"deleteAccountSuccess": "Konto usunięte",
"deleteAccountSuccessMessage": "Twoje konto zostało usunięte.",
"deleteAccountError": "Nie udało się usunąć konta",
"deleteAccountPreviewAccount": "Twoje konto",
"deleteAccountPreviewOrgs": "Organizacje, które jesteś właścicielem (i wszystkie ich dane)",
"orgMissing": "Brak ID organizacji", "orgMissing": "Brak ID organizacji",
"orgMissingMessage": "Nie można ponownie wygenerować zaproszenia bez ID organizacji.", "orgMissingMessage": "Nie można ponownie wygenerować zaproszenia bez ID organizacji.",
"accessUsersManage": "Zarządzaj użytkownikami", "accessUsersManage": "Zarządzaj użytkownikami",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtruj według państwa zatwierdzenia", "filterByApprovalState": "Filtruj według państwa zatwierdzenia",
"approvalListEmpty": "Brak zatwierdzeń", "approvalListEmpty": "Brak zatwierdzeń",
"approvalState": "Państwo zatwierdzające", "approvalState": "Państwo zatwierdzające",
"approvalLoadMore": "Załaduj więcej",
"loadingApprovals": "Wczytywanie zatwierdzeń",
"approve": "Zatwierdź", "approve": "Zatwierdź",
"approved": "Zatwierdzone", "approved": "Zatwierdzone",
"denied": "Odmowa", "denied": "Odmowa",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Zwiększ liczbę witryn", "sitestCountIncrease": "Zwiększ liczbę witryn",
"idpManage": "Zarządzaj dostawcami tożsamości", "idpManage": "Zarządzaj dostawcami tożsamości",
"idpManageDescription": "Wyświetl i zarządzaj dostawcami tożsamości w systemie", "idpManageDescription": "Wyświetl i zarządzaj dostawcami tożsamości w systemie",
"idpGlobalModeBanner": "Dostawcy tożsamości (IdPs) na organizację są wyłączeni na tym serwerze. Używa globalnych IdP (współdzielonych ze wszystkimi organizacjami). Zarządzaj globalnymi IdP w panelu administracyjnym <adminPanelLink></adminPanelLink>. Aby włączyć IdP na organizację, edytuj konfigurację serwera i ustaw tryb IdP na org. <configDocsLink>Zobacz dokumentację</configDocsLink>. Jeśli chcesz nadal używać globalnych IdP i sprawić, że zniknie to z ustawień organizacji, wyraźnie ustaw tryb globalny w konfiguracji.",
"idpGlobalModeBannerUpgradeRequired": "Dostawcy tożsamości (IdPs) na organizację są wyłączeni na tym serwerze. Używają globalnych IdP (współdzielonych między wszystkimi organizacjami). Zarządzaj globalnymi IdP w panelu administracyjnym <adminPanelLink></adminPanelLink>. Aby korzystać z dostawców tożsamości na organizację, musisz zaktualizować do edycji Enterprise.",
"idpGlobalModeBannerLicenseRequired": "Dostawcy tożsamości (IdPs) na organizację są wyłączeni na tym serwerze. Używają globalnych IdP (współdzielonych między wszystkimi organizacjami). Zarządzaj globalnymi IdP w panelu administracyjnym <adminPanelLink></adminPanelLink>. Aby korzystać z dostawców tożsamości na organizację, wymagana jest licencja Enterprise.",
"idpDeletedDescription": "Dostawca tożsamości został pomyślnie usunięty", "idpDeletedDescription": "Dostawca tożsamości został pomyślnie usunięty",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Czy na pewno chcesz trwale usunąć dostawcę tożsamości?", "idpQuestionRemove": "Czy na pewno chcesz trwale usunąć dostawcę tożsamości?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Konfiguracja - Pangolin", "pangolinSetup": "Konfiguracja - Pangolin",
"orgNameRequired": "Nazwa organizacji jest wymagana", "orgNameRequired": "Nazwa organizacji jest wymagana",
"orgIdRequired": "ID organizacji jest wymagane", "orgIdRequired": "ID organizacji jest wymagane",
"orgIdMaxLength": "Identyfikator organizacji musi mieć co najwyżej 32 znaki",
"orgErrorCreate": "Wystąpił błąd podczas tworzenia organizacji", "orgErrorCreate": "Wystąpił błąd podczas tworzenia organizacji",
"pageNotFound": "Nie znaleziono strony", "pageNotFound": "Nie znaleziono strony",
"pageNotFoundDescription": "Ups! Strona, której szukasz, nie istnieje.", "pageNotFoundDescription": "Ups! Strona, której szukasz, nie istnieje.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Zobacz dzienniki", "actionViewLogs": "Zobacz dzienniki",
"noneSelected": "Nie wybrano", "noneSelected": "Nie wybrano",
"orgNotFound2": "Nie znaleziono organizacji.", "orgNotFound2": "Nie znaleziono organizacji.",
"searchProgress": "Szukaj...", "searchPlaceholder": "Szukaj...",
"emptySearchOptions": "Nie znaleziono opcji",
"create": "Utwórz", "create": "Utwórz",
"orgs": "Organizacje", "orgs": "Organizacje",
"loginError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.", "loginError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Dziennik & Analityka", "sidebarLogAndAnalytics": "Dziennik & Analityka",
"sidebarBluePrints": "Schematy", "sidebarBluePrints": "Schematy",
"sidebarOrganization": "Organizacja", "sidebarOrganization": "Organizacja",
"sidebarBillingAndLicenses": "Płatność i licencje",
"sidebarLogsAnalytics": "Analityka", "sidebarLogsAnalytics": "Analityka",
"blueprints": "Schematy", "blueprints": "Schematy",
"blueprintsDescription": "Zastosuj konfiguracje deklaracyjne i wyświetl poprzednie operacje", "blueprintsDescription": "Zastosuj konfiguracje deklaracyjne i wyświetl poprzednie operacje",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Przegląd Limitów Użytkowania", "billingUsageLimitsOverview": "Przegląd Limitów Użytkowania",
"billingMonitorUsage": "Monitoruj swoje wykorzystanie w porównaniu do skonfigurowanych limitów. Jeśli potrzebujesz zwiększenia limitów, skontaktuj się z nami pod adresem support@pangolin.net.", "billingMonitorUsage": "Monitoruj swoje wykorzystanie w porównaniu do skonfigurowanych limitów. Jeśli potrzebujesz zwiększenia limitów, skontaktuj się z nami pod adresem support@pangolin.net.",
"billingDataUsage": "Użycie danych", "billingDataUsage": "Użycie danych",
"billingOnlineTime": "Czas Online Strony", "billingSites": "Witryny",
"billingUsers": "Aktywni użytkownicy", "billingUsers": "Użytkownicy",
"billingDomains": "Aktywne domeny", "billingDomains": "Domeny",
"billingRemoteExitNodes": "Aktywne samodzielnie-hostowane węzły", "billingOrganizations": "O masie całkowitej pojazdu przekraczającej 5 ton, ale nieprzekraczającej 5 ton",
"billingRemoteExitNodes": "Zdalne węzły",
"billingNoLimitConfigured": "Nie skonfigurowano limitu", "billingNoLimitConfigured": "Nie skonfigurowano limitu",
"billingEstimatedPeriod": "Szacowany Okres Rozliczeniowy", "billingEstimatedPeriod": "Szacowany Okres Rozliczeniowy",
"billingIncludedUsage": "Zawarte użycie", "billingIncludedUsage": "Zawarte użycie",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Nie udało się uzyskać adresu URL portalu", "billingFailedToGetPortalUrl": "Nie udało się uzyskać adresu URL portalu",
"billingPortalError": "Błąd Portalu", "billingPortalError": "Błąd Portalu",
"billingDataUsageInfo": "Jesteś obciążony za wszystkie dane przesyłane przez bezpieczne tunele, gdy jesteś podłączony do chmury. Obejmuje to zarówno ruch przychodzący, jak i wychodzący we wszystkich Twoich witrynach. Gdy osiągniesz swój limit, twoje strony zostaną rozłączone, dopóki nie zaktualizujesz planu lub nie ograniczysz użycia. Dane nie będą naliczane przy użyciu węzłów.", "billingDataUsageInfo": "Jesteś obciążony za wszystkie dane przesyłane przez bezpieczne tunele, gdy jesteś podłączony do chmury. Obejmuje to zarówno ruch przychodzący, jak i wychodzący we wszystkich Twoich witrynach. Gdy osiągniesz swój limit, twoje strony zostaną rozłączone, dopóki nie zaktualizujesz planu lub nie ograniczysz użycia. Dane nie będą naliczane przy użyciu węzłów.",
"billingOnlineTimeInfo": "Opłata zależy od tego, jak długo twoje strony pozostają połączone z chmurą. Na przykład 44,640 minut oznacza jedną stronę działającą 24/7 przez cały miesiąc. Kiedy osiągniesz swój limit, twoje strony zostaną rozłączone, dopóki nie zaktualizujesz planu lub nie zmniejsz jego wykorzystania. Czas nie będzie naliczany przy użyciu węzłów.", "billingSInfo": "Ile stron możesz użyć",
"billingUsersInfo": "Opłata za każdego użytkownika w organizacji. Płatność jest obliczana codziennie na podstawie liczby aktywnych kont użytkowników w Twojej organizacji.", "billingUsersInfo": "Ile użytkowników możesz użyć",
"billingDomainInfo": "Opłata za każdą domenę w organizacji. Płatność jest obliczana codziennie na podstawie liczby aktywnych kont domen w Twojej organizacji.", "billingDomainInfo": "Ile domen możesz użyć",
"billingRemoteExitNodesInfo": "Opłata za każdy zarządzany węzeł w organizacji. Płatność jest obliczana codziennie na podstawie liczby aktywnych zarządzanych węzłów w Twojej organizacji.", "billingRemoteExitNodesInfo": "Ile zdalnych węzłów możesz użyć",
"billingLicenseKeys": "Klucze licencyjne",
"billingLicenseKeysDescription": "Zarządzaj subskrypcjami kluczy licencyjnych",
"billingLicenseSubscription": "Subskrypcja licencji",
"billingInactive": "Nieaktywny",
"billingLicenseItem": "Element licencji",
"billingQuantity": "Ilość",
"billingTotal": "łącznie",
"billingModifyLicenses": "Modyfikuj subskrypcję licencji",
"domainNotFound": "Nie znaleziono domeny", "domainNotFound": "Nie znaleziono domeny",
"domainNotFoundDescription": "Zasób jest wyłączony, ponieważ domena nie istnieje już w naszym systemie. Proszę ustawić nową domenę dla tego zasobu.", "domainNotFoundDescription": "Zasób jest wyłączony, ponieważ domena nie istnieje już w naszym systemie. Proszę ustawić nową domenę dla tego zasobu.",
"failed": "Niepowodzenie", "failed": "Niepowodzenie",
"createNewOrgDescription": "Utwórz nową organizację", "createNewOrgDescription": "Utwórz nową organizację",
"organization": "Organizacja", "organization": "Organizacja",
"primary": "Podstawowy",
"port": "Port", "port": "Port",
"securityKeyManage": "Zarządzaj kluczami bezpieczeństwa", "securityKeyManage": "Zarządzaj kluczami bezpieczeństwa",
"securityKeyDescription": "Dodaj lub usuń klucze bezpieczeństwa do uwierzytelniania bez hasła", "securityKeyDescription": "Dodaj lub usuń klucze bezpieczeństwa do uwierzytelniania bez hasła",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP", "resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP",
"resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP", "resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP",
"billingPricingCalculatorLink": "Kalkulator Cen", "billingPricingCalculatorLink": "Kalkulator Cen",
"billingYourPlan": "Twój plan",
"billingViewOrModifyPlan": "Wyświetl lub zmodyfikuj swój aktualny plan",
"billingViewPlanDetails": "Zobacz szczegóły planu",
"billingUsageAndLimits": "Stosowanie i ograniczenia",
"billingViewUsageAndLimits": "Zobacz limity swojego planu i bieżące użycie",
"billingCurrentUsage": "Bieżące użycie",
"billingMaximumLimits": "Maksymalne limity",
"billingRemoteNodes": "Zdalne węzły",
"billingUnlimited": "Nieograniczona",
"billingPaidLicenseKeys": "Płatne klucze licencyjne",
"billingManageLicenseSubscription": "Zarządzaj subskrypcją płatnych własnych kluczy licencyjnych",
"billingCurrentKeys": "Bieżące klucze",
"billingModifyCurrentPlan": "Modyfikuj bieżący plan",
"billingConfirmUpgrade": "Potwierdź aktualizację",
"billingConfirmDowngrade": "Potwierdź obniżenie",
"billingConfirmUpgradeDescription": "Zamierzasz ulepszyć swój plan. Przejrzyj nowe limity i ceny poniżej.",
"billingConfirmDowngradeDescription": "Zamierzasz obniżyć swój plan. Przejrzyj nowe limity i ceny poniżej.",
"billingPlanIncludes": "Plan zawiera",
"billingProcessing": "Przetwarzanie...",
"billingConfirmUpgradeButton": "Potwierdź aktualizację",
"billingConfirmDowngradeButton": "Potwierdź obniżenie",
"billingLimitViolationWarning": "Użycie przekracza nowe limity planu",
"billingLimitViolationDescription": "Bieżące użycie przekracza limity tego planu. Po obniżeniu, wszystkie działania zostaną wyłączone, dopóki nie zmniejsz zużycia w ramach nowych limitów. Zapoznaj się z poniższymi funkcjami, które obecnie przekraczają limity. Limity naruszenia:",
"billingFeatureLossWarning": "Powiadomienie o dostępności funkcji",
"billingFeatureLossDescription": "Po obniżeniu wartości funkcje niedostępne w nowym planie zostaną automatycznie wyłączone. Niektóre ustawienia i konfiguracje mogą zostać utracone. Zapoznaj się z matrycą cenową, aby zrozumieć, które funkcje nie będą już dostępne.",
"billingUsageExceedsLimit": "Bieżące użycie ({current}) przekracza limit ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Zgadzam się z", "IAgreeToThe": "Zgadzam się z",
"termsOfService": "warunkami usługi", "termsOfService": "warunkami usługi",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Czy na pewno chcesz usunąć branding dla stron uwierzytelniania?", "authPageBrandingQuestionRemove": "Czy na pewno chcesz usunąć branding dla stron uwierzytelniania?",
"authPageBrandingDeleteConfirm": "Potwierdź usunięcie brandingu", "authPageBrandingDeleteConfirm": "Potwierdź usunięcie brandingu",
"brandingLogoURL": "URL logo", "brandingLogoURL": "URL logo",
"brandingLogoURLOrPath": "Adres URL logo lub ścieżka",
"brandingLogoPathDescription": "Wprowadź adres URL lub ścieżkę lokalną.",
"brandingLogoURLDescription": "Wprowadź publicznie dostępny adres URL do obrazu logo.",
"brandingPrimaryColor": "Główny kolor", "brandingPrimaryColor": "Główny kolor",
"brandingLogoWidth": "Szerokość (piksele)", "brandingLogoWidth": "Szerokość (piksele)",
"brandingLogoHeight": "Wysokość (piksele)", "brandingLogoHeight": "Wysokość (piksele)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Powrót do standardowego logowania", "orgAuthBackToSignIn": "Powrót do standardowego logowania",
"orgAuthNoAccount": "Nie masz konta?", "orgAuthNoAccount": "Nie masz konta?",
"subscriptionRequiredToUse": "Do korzystania z tej funkcji wymagana jest subskrypcja.", "subscriptionRequiredToUse": "Do korzystania z tej funkcji wymagana jest subskrypcja.",
"mustUpgradeToUse": "Musisz uaktualnić subskrypcję, aby korzystać z tej funkcji.",
"subscriptionRequiredTierToUse": "Ta funkcja wymaga funkcji <tierLink>{tier}</tierLink> lub wyższej.",
"upgradeToTierToUse": "Aby skorzystać z tej funkcji, przejdź na <tierLink>{tier}</tierLink> lub wyższy pakiet.",
"subscriptionTierTier1": "Strona główna",
"subscriptionTierTier2": "Drużyna",
"subscriptionTierTier3": "Biznes",
"subscriptionTierEnterprise": "Przedsiębiorstwo",
"idpDisabled": "Dostawcy tożsamości są wyłączeni", "idpDisabled": "Dostawcy tożsamości są wyłączeni",
"orgAuthPageDisabled": "Strona autoryzacji organizacji jest wyłączona.", "orgAuthPageDisabled": "Strona autoryzacji organizacji jest wyłączona.",
"domainRestartedDescription": "Weryfikacja domeny zrestartowana pomyślnie", "domainRestartedDescription": "Weryfikacja domeny zrestartowana pomyślnie",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Uzyskaj licencję",
"description": "Wybierz plan i powiedz nam, jak planujesz korzystać z Pangolin.",
"chooseTier": "Wybierz swój plan",
"viewPricingLink": "Zobacz cenniki, funkcje i limity",
"tiers": {
"starter": {
"title": "Rozpocznij",
"description": "Środki te przeznaczone są na pokrycie wydatków na personel i wydatków administracyjnych Agencji (tytuły 1 i 2) oraz jej wydatków operacyjnych (tytuł 3)."
},
"scale": {
"title": "Skala",
"description": "Cechy przedsiębiorstw, 50 użytkowników, 50 obiektów i wsparcie priorytetowe."
}
},
"personalUseOnly": "Wyłącznie do użytku osobistego (bezpłatna licencja brak zamówień)",
"buttons": {
"continueToCheckout": "Przejdź do zamówienia"
},
"toasts": {
"checkoutError": {
"title": "Błąd zamówienia",
"description": "Nie można uruchomić zamówienia. Spróbuj ponownie."
}
}
},
"priority": "Priorytet", "priority": "Priorytet",
"priorityDescription": "Najpierw oceniane są trasy priorytetowe. Priorytet = 100 oznacza automatyczne zamawianie (decyzje systemowe). Użyj innego numeru, aby wyegzekwować ręczny priorytet.", "priorityDescription": "Najpierw oceniane są trasy priorytetowe. Priorytet = 100 oznacza automatyczne zamawianie (decyzje systemowe). Użyj innego numeru, aby wyegzekwować ręczny priorytet.",
"instanceName": "Nazwa instancji", "instanceName": "Nazwa instancji",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Koniec następnego roku", "logRetentionEndOfFollowingYear": "Koniec następnego roku",
"actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji", "actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji",
"accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji", "accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji",
"licenseRequiredToUse": "Licencja Enterprise jest wymagana do korzystania z tej funkcji.", "licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> . Ta funkcja jest również dostępna w <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Rozwiązywanie certyfikatów", "certResolver": "Rozwiązywanie certyfikatów",
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.", "certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
"selectCertResolver": "Wybierz Resolver certyfikatów", "selectCertResolver": "Wybierz Resolver certyfikatów",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Zapora włączona", "firewallEnabled": "Zapora włączona",
"autoUpdatesEnabled": "Automatyczne aktualizacje włączone", "autoUpdatesEnabled": "Automatyczne aktualizacje włączone",
"tpmAvailable": "TPM dostępne", "tpmAvailable": "TPM dostępne",
"windowsAntivirusEnabled": "Antywirus włączony",
"macosSipEnabled": "Ochrona integralności systemu (SIP)", "macosSipEnabled": "Ochrona integralności systemu (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Tryb Stealth zapory", "macosFirewallStealthMode": "Tryb Stealth zapory",

View File

@@ -18,6 +18,8 @@
"componentsMember": "É membro de {count, plural, =0 {nenhuma organização} one {uma organização} other {# organizações}}.", "componentsMember": "É membro de {count, plural, =0 {nenhuma organização} one {uma organização} other {# organizações}}.",
"componentsInvalidKey": "Chaves de licença inválidas ou expiradas detectadas. Siga os termos da licença para continuar usando todos os recursos.", "componentsInvalidKey": "Chaves de licença inválidas ou expiradas detectadas. Siga os termos da licença para continuar usando todos os recursos.",
"dismiss": "Rejeitar", "dismiss": "Rejeitar",
"subscriptionViolationMessage": "Você está além dos seus limites para o seu plano atual. Corrija o problema removendo sites, usuários, ou outros recursos para ficar em seu plano.",
"subscriptionViolationViewBilling": "Ver faturamento",
"componentsLicenseViolation": "Violação de Licença: Este servidor está usando sites {usedSites} que excedem o limite licenciado de sites {maxSites} . Siga os termos da licença para continuar usando todos os recursos.", "componentsLicenseViolation": "Violação de Licença: Este servidor está usando sites {usedSites} que excedem o limite licenciado de sites {maxSites} . Siga os termos da licença para continuar usando todos os recursos.",
"componentsSupporterMessage": "Obrigado por apoiar o Pangolin como um {tier}!", "componentsSupporterMessage": "Obrigado por apoiar o Pangolin como um {tier}!",
"inviteErrorNotValid": "Desculpe, mas parece que o convite que está a tentar aceder não foi aceito ou não é mais válido.", "inviteErrorNotValid": "Desculpe, mas parece que o convite que está a tentar aceder não foi aceito ou não é mais válido.",
@@ -199,6 +201,7 @@
"protocolSelect": "Selecione um protocolo", "protocolSelect": "Selecione um protocolo",
"resourcePortNumber": "Número da Porta", "resourcePortNumber": "Número da Porta",
"resourcePortNumberDescription": "O número da porta externa para requisições de proxy.", "resourcePortNumberDescription": "O número da porta externa para requisições de proxy.",
"back": "Anterior",
"cancel": "cancelar", "cancel": "cancelar",
"resourceConfig": "Snippets de Configuração", "resourceConfig": "Snippets de Configuração",
"resourceConfigDescription": "Copie e cole estes snippets de configuração para configurar o recurso TCP/UDP", "resourceConfigDescription": "Copie e cole estes snippets de configuração para configurar o recurso TCP/UDP",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Ocorreu um erro ao apagar a organização.", "orgErrorDeleteMessage": "Ocorreu um erro ao apagar a organização.",
"orgDeleted": "Organização excluída", "orgDeleted": "Organização excluída",
"orgDeletedMessage": "A organização e seus dados foram excluídos.", "orgDeletedMessage": "A organização e seus dados foram excluídos.",
"deleteAccount": "Excluir Conta",
"deleteAccountDescription": "Exclua permanentemente sua conta, todas as organizações que você possui e todos os dados nessas organizações. Isso não pode ser desfeito.",
"deleteAccountButton": "Excluir Conta",
"deleteAccountConfirmTitle": "Excluir Conta",
"deleteAccountConfirmMessage": "Isto limpará permanentemente sua conta, todas as organizações que você possui e todos os dados dentro dessas organizações. Isso não pode ser desfeito.",
"deleteAccountConfirmString": "excluir conta",
"deleteAccountSuccess": "Conta excluída",
"deleteAccountSuccessMessage": "Sua conta foi excluída.",
"deleteAccountError": "Falha ao excluir conta",
"deleteAccountPreviewAccount": "Sua conta",
"deleteAccountPreviewOrgs": "Organizações que você possui (e todos os dados deles)",
"orgMissing": "ID da Organização Ausente", "orgMissing": "ID da Organização Ausente",
"orgMissingMessage": "Não é possível regenerar o convite sem um ID de organização.", "orgMissingMessage": "Não é possível regenerar o convite sem um ID de organização.",
"accessUsersManage": "Gerir Utilizadores", "accessUsersManage": "Gerir Utilizadores",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Filtrar por estado de aprovação", "filterByApprovalState": "Filtrar por estado de aprovação",
"approvalListEmpty": "Sem aprovações", "approvalListEmpty": "Sem aprovações",
"approvalState": "Estado de aprovação", "approvalState": "Estado de aprovação",
"approvalLoadMore": "Carregue mais",
"loadingApprovals": "Carregando aprovações",
"approve": "Aprovar", "approve": "Aprovar",
"approved": "Aceito", "approved": "Aceito",
"denied": "Negado", "denied": "Negado",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Aumentar contagem de sites", "sitestCountIncrease": "Aumentar contagem de sites",
"idpManage": "Gerir Provedores de Identidade", "idpManage": "Gerir Provedores de Identidade",
"idpManageDescription": "Visualizar e gerir provedores de identidade no sistema", "idpManageDescription": "Visualizar e gerir provedores de identidade no sistema",
"idpGlobalModeBanner": "Provedores de identidade (Pds) por organização estão desabilitados neste servidor. Ele está usando IdPs globais (compartilhados entre todas as organizações). Gerencie IdPs no painel <adminPanelLink>admin</adminPanelLink>. Para habilitar IdPs por organização, edite a configuração do servidor e defina o modo IdP como org. <configDocsLink>Veja a documentação</configDocsLink>. Se quiser continuar usando IdPs globais e fazer isso desaparecer das configurações da organização, defina explicitamente o modo como global na configuração.",
"idpGlobalModeBannerUpgradeRequired": "Os provedores de identidade (IdPs) por organização estão desativados neste servidor. Ele está usando IdPs globais (compartilhados entre todas as organizações). Gerencie os IdPs globais no <adminPanelLink>painel administrativo</adminPanelLink>. Para usar provedores de identidade por organização, você deve atualizar para a edição Enterprise.",
"idpGlobalModeBannerLicenseRequired": "Os provedores de identidade (IdPs) por organização estão desativados neste servidor. Ele está usando IdPs globais (compartilhados entre todas as organizações). Gerencie os IdPs globais no <adminPanelLink>painel administrativo</adminPanelLink>. Para usar provedores de identidade por organização, é necessário uma licença Enterprise.",
"idpDeletedDescription": "Provedor de identidade eliminado com sucesso", "idpDeletedDescription": "Provedor de identidade eliminado com sucesso",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Tem certeza que deseja eliminar permanentemente o provedor de identidade?", "idpQuestionRemove": "Tem certeza que deseja eliminar permanentemente o provedor de identidade?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Configuração - Pangolin", "pangolinSetup": "Configuração - Pangolin",
"orgNameRequired": "O nome da organização é obrigatório", "orgNameRequired": "O nome da organização é obrigatório",
"orgIdRequired": "O ID da organização é obrigatório", "orgIdRequired": "O ID da organização é obrigatório",
"orgIdMaxLength": "ID da organização deve ter no máximo 32 caracteres",
"orgErrorCreate": "Ocorreu um erro ao criar a organização", "orgErrorCreate": "Ocorreu um erro ao criar a organização",
"pageNotFound": "Página Não Encontrada", "pageNotFound": "Página Não Encontrada",
"pageNotFoundDescription": "Ops! A página que você está procurando não existe.", "pageNotFoundDescription": "Ops! A página que você está procurando não existe.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Visualizar registros", "actionViewLogs": "Visualizar registros",
"noneSelected": "Nenhum selecionado", "noneSelected": "Nenhum selecionado",
"orgNotFound2": "Nenhuma organização encontrada.", "orgNotFound2": "Nenhuma organização encontrada.",
"searchProgress": "Pesquisar...", "searchPlaceholder": "Buscar...",
"emptySearchOptions": "Nenhuma opção encontrada",
"create": "Criar", "create": "Criar",
"orgs": "Organizações", "orgs": "Organizações",
"loginError": "Ocorreu um erro inesperado. Por favor, tente novamente.", "loginError": "Ocorreu um erro inesperado. Por favor, tente novamente.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Registo & Análise", "sidebarLogAndAnalytics": "Registo & Análise",
"sidebarBluePrints": "Diagramas", "sidebarBluePrints": "Diagramas",
"sidebarOrganization": "Organização", "sidebarOrganization": "Organização",
"sidebarBillingAndLicenses": "Faturamento e Licenças",
"sidebarLogsAnalytics": "Análises", "sidebarLogsAnalytics": "Análises",
"blueprints": "Diagramas", "blueprints": "Diagramas",
"blueprintsDescription": "Aplicar configurações declarativas e ver execuções anteriores", "blueprintsDescription": "Aplicar configurações declarativas e ver execuções anteriores",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Visão Geral dos Limites de Uso", "billingUsageLimitsOverview": "Visão Geral dos Limites de Uso",
"billingMonitorUsage": "Monitore seu uso em relação aos limites configurados. Se precisar aumentar esses limites, entre em contato conosco support@pangolin.net.", "billingMonitorUsage": "Monitore seu uso em relação aos limites configurados. Se precisar aumentar esses limites, entre em contato conosco support@pangolin.net.",
"billingDataUsage": "Uso de Dados", "billingDataUsage": "Uso de Dados",
"billingOnlineTime": "Tempo Online do Site", "billingSites": "sites",
"billingUsers": "Usuários Ativos", "billingUsers": "Utilizadores",
"billingDomains": "Domínios Ativos", "billingDomains": "Domínios",
"billingRemoteExitNodes": "Nodos Auto-Hospedados Ativos", "billingOrganizations": "Órgãos",
"billingRemoteExitNodes": "Nós remotos",
"billingNoLimitConfigured": "Nenhum limite configurado", "billingNoLimitConfigured": "Nenhum limite configurado",
"billingEstimatedPeriod": "Período Estimado de Cobrança", "billingEstimatedPeriod": "Período Estimado de Cobrança",
"billingIncludedUsage": "Uso Incluído", "billingIncludedUsage": "Uso Incluído",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Falha ao obter URL do portal", "billingFailedToGetPortalUrl": "Falha ao obter URL do portal",
"billingPortalError": "Erro do Portal", "billingPortalError": "Erro do Portal",
"billingDataUsageInfo": "Você é cobrado por todos os dados transferidos através de seus túneis seguros quando conectado à nuvem. Isso inclui o tráfego de entrada e saída em todos os seus sites. Quando você atingir o seu limite, seus sites desconectarão até que você atualize seu plano ou reduza o uso. Os dados não serão cobrados ao usar os nós.", "billingDataUsageInfo": "Você é cobrado por todos os dados transferidos através de seus túneis seguros quando conectado à nuvem. Isso inclui o tráfego de entrada e saída em todos os seus sites. Quando você atingir o seu limite, seus sites desconectarão até que você atualize seu plano ou reduza o uso. Os dados não serão cobrados ao usar os nós.",
"billingOnlineTimeInfo": "Cobrança de acordo com o tempo em que seus sites permanecem conectados à nuvem. Por exemplo, 44,640 minutos é igual a um site que roda 24/7 para um mês inteiro. Quando você atinge o seu limite, seus sites desconectarão até que você faça o upgrade do seu plano ou reduza o uso. O tempo não é cobrado ao usar nós.", "billingSInfo": "Quantos sites você pode usar",
"billingUsersInfo": "A cobrança é feita por cada usuário na organização. A cobrança é feita diariamente com base no número de contas de usuário ativas na sua organização.", "billingUsersInfo": "Quantos usuários você pode usar",
"billingDomainInfo": "A cobrança é feita por cada domínio da organização. A cobrança é feita diariamente com base no número de contas de domínio ativas na sua organização.", "billingDomainInfo": "Quantos domínios você pode usar",
"billingRemoteExitNodesInfo": "Você é cobrado por cada nó gerenciado na organização. A cobrança é calculada diariamente com base no número de nós gerenciados ativos em sua organização.", "billingRemoteExitNodesInfo": "Quantos nós remotos você pode usar",
"billingLicenseKeys": "Chaves de Licença",
"billingLicenseKeysDescription": "Gerenciar suas subscrições de chave de licença",
"billingLicenseSubscription": "Assinatura de Licença",
"billingInactive": "Inativo",
"billingLicenseItem": "Item de Licença",
"billingQuantity": "Quantidade",
"billingTotal": "total:",
"billingModifyLicenses": "Modificar assinatura de licença",
"domainNotFound": "Domínio Não Encontrado", "domainNotFound": "Domínio Não Encontrado",
"domainNotFoundDescription": "Este recurso está desativado porque o domínio não existe mais em nosso sistema. Defina um novo domínio para este recurso.", "domainNotFoundDescription": "Este recurso está desativado porque o domínio não existe mais em nosso sistema. Defina um novo domínio para este recurso.",
"failed": "Falhou", "failed": "Falhou",
"createNewOrgDescription": "Crie uma nova organização", "createNewOrgDescription": "Crie uma nova organização",
"organization": "Organização", "organization": "Organização",
"primary": "Primário",
"port": "Porta", "port": "Porta",
"securityKeyManage": "Gerir chaves de segurança", "securityKeyManage": "Gerir chaves de segurança",
"securityKeyDescription": "Adicionar ou remover chaves de segurança para autenticação sem senha", "securityKeyDescription": "Adicionar ou remover chaves de segurança para autenticação sem senha",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Número da porta é obrigatório para recursos não-HTTP", "resourcePortRequired": "Número da porta é obrigatório para recursos não-HTTP",
"resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP", "resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP",
"billingPricingCalculatorLink": "Calculadora de Preços", "billingPricingCalculatorLink": "Calculadora de Preços",
"billingYourPlan": "Seu plano",
"billingViewOrModifyPlan": "Ver ou modificar seu plano atual",
"billingViewPlanDetails": "Ver detalhes do plano",
"billingUsageAndLimits": "Uso e Limites",
"billingViewUsageAndLimits": "Ver os limites do seu plano e o uso atual",
"billingCurrentUsage": "Uso atual",
"billingMaximumLimits": "Limite Máximo",
"billingRemoteNodes": "Nós remotos",
"billingUnlimited": "Ilimitado",
"billingPaidLicenseKeys": "Chaves de licença paga",
"billingManageLicenseSubscription": "Gerencie sua assinatura para as chaves de licenças auto-hospedadas pagas",
"billingCurrentKeys": "Chaves atuais",
"billingModifyCurrentPlan": "Modificar o Plano Atual",
"billingConfirmUpgrade": "Confirmar a atualização",
"billingConfirmDowngrade": "Confirmar downgrade",
"billingConfirmUpgradeDescription": "Você está prestes a atualizar seu plano. Revise os novos limites e preços abaixo.",
"billingConfirmDowngradeDescription": "Você está prestes a fazer o downgrade do seu plano. Revise os novos limites e preços abaixo.",
"billingPlanIncludes": "Plano Inclui",
"billingProcessing": "Processandochar@@0",
"billingConfirmUpgradeButton": "Confirmar a atualização",
"billingConfirmDowngradeButton": "Confirmar downgrade",
"billingLimitViolationWarning": "Uso excede novos limites de plano",
"billingLimitViolationDescription": "Seu uso atual excede os limites deste plano. Após desclassificação, todas as ações serão desabilitadas até que você reduza o uso dentro dos novos limites. Por favor, reveja os recursos abaixo que atualmente estão acima dos limites. Limites de violação:",
"billingFeatureLossWarning": "Aviso de disponibilidade de recursos",
"billingFeatureLossDescription": "Ao fazer o downgrading, recursos não disponíveis no novo plano serão desativados automaticamente. Algumas configurações e configurações podem ser perdidas. Por favor, revise a matriz de preços para entender quais características não estarão mais disponíveis.",
"billingUsageExceedsLimit": "Uso atual ({current}) excede o limite ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Concordo com", "IAgreeToThe": "Concordo com",
"termsOfService": "os termos de serviço", "termsOfService": "os termos de serviço",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Tem certeza de que deseja remover a marcação das Páginas de Autenticação?", "authPageBrandingQuestionRemove": "Tem certeza de que deseja remover a marcação das Páginas de Autenticação?",
"authPageBrandingDeleteConfirm": "Confirmar Exclusão de Marca", "authPageBrandingDeleteConfirm": "Confirmar Exclusão de Marca",
"brandingLogoURL": "URL do Logo", "brandingLogoURL": "URL do Logo",
"brandingLogoURLOrPath": "URL ou caminho do logotipo",
"brandingLogoPathDescription": "Insira uma URL ou um caminho local.",
"brandingLogoURLDescription": "Digite uma URL publicamente acessível para a sua imagem do logotipo.",
"brandingPrimaryColor": "Cor Primária", "brandingPrimaryColor": "Cor Primária",
"brandingLogoWidth": "Largura (px)", "brandingLogoWidth": "Largura (px)",
"brandingLogoHeight": "Altura (px)", "brandingLogoHeight": "Altura (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Voltar para entrada padrão", "orgAuthBackToSignIn": "Voltar para entrada padrão",
"orgAuthNoAccount": "Não possui uma conta?", "orgAuthNoAccount": "Não possui uma conta?",
"subscriptionRequiredToUse": "Uma assinatura é necessária para usar esse recurso.", "subscriptionRequiredToUse": "Uma assinatura é necessária para usar esse recurso.",
"mustUpgradeToUse": "Você deve atualizar sua assinatura para usar este recurso.",
"subscriptionRequiredTierToUse": "Esta função requer <tierLink>{tier}</tierLink> ou superior.",
"upgradeToTierToUse": "Atualize para <tierLink>{tier}</tierLink> ou superior para usar este recurso.",
"subscriptionTierTier1": "Residencial",
"subscriptionTierTier2": "Equipe",
"subscriptionTierTier3": "Empresas",
"subscriptionTierEnterprise": "Empresa",
"idpDisabled": "Provedores de identidade estão desabilitados.", "idpDisabled": "Provedores de identidade estão desabilitados.",
"orgAuthPageDisabled": "A página de autenticação da organização está desativada.", "orgAuthPageDisabled": "A página de autenticação da organização está desativada.",
"domainRestartedDescription": "Verificação de domínio reiniciado com sucesso", "domainRestartedDescription": "Verificação de domínio reiniciado com sucesso",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Obtenha uma licença",
"description": "Escolha um plano e nos diga como você planeja usar o Pangolin.",
"chooseTier": "Escolha seu plano",
"viewPricingLink": "Veja os preços, recursos e limites",
"tiers": {
"starter": {
"title": "Iniciante",
"description": "Recursos de empresa, 25 usuários, 25 sites e apoio da comunidade."
},
"scale": {
"title": "Escala",
"description": "Recursos de empresa, 50 usuários, 50 sites e apoio prioritário."
}
},
"personalUseOnly": "Apenas uso pessoal (licença gratuita — sem check-out)",
"buttons": {
"continueToCheckout": "Continuar com checkout"
},
"toasts": {
"checkoutError": {
"title": "Erro no check-out",
"description": "Não foi possível iniciar o checkout. Por favor, tente novamente."
}
}
},
"priority": "Prioridade", "priority": "Prioridade",
"priorityDescription": "Rotas de alta prioridade são avaliadas primeiro. Prioridade = 100 significa ordem automática (decisões do sistema). Use outro número para aplicar prioridade manual.", "priorityDescription": "Rotas de alta prioridade são avaliadas primeiro. Prioridade = 100 significa ordem automática (decisões do sistema). Use outro número para aplicar prioridade manual.",
"instanceName": "Nome da Instância", "instanceName": "Nome da Instância",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Fim do ano seguinte", "logRetentionEndOfFollowingYear": "Fim do ano seguinte",
"actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização", "actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização",
"accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização", "accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização",
"licenseRequiredToUse": "É necessária uma licença empresarial para usar esse recurso.", "licenseRequiredToUse": "Uma licença <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> é necessária para usar este recurso. Este recurso também está disponível no <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "O <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> é necessário para usar este recurso. Este recurso também está disponível no <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Resolvedor de Certificado", "certResolver": "Resolvedor de Certificado",
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.", "certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
"selectCertResolver": "Selecionar solucionador de certificado", "selectCertResolver": "Selecionar solucionador de certificado",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Firewall habilitado", "firewallEnabled": "Firewall habilitado",
"autoUpdatesEnabled": "Atualizações Automáticas Habilitadas", "autoUpdatesEnabled": "Atualizações Automáticas Habilitadas",
"tpmAvailable": "TPM disponível", "tpmAvailable": "TPM disponível",
"windowsAntivirusEnabled": "Antivírus habilitado",
"macosSipEnabled": "Proteção da Integridade do Sistema (SIP)", "macosSipEnabled": "Proteção da Integridade do Sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo do Firewall", "macosFirewallStealthMode": "Modo Furtivo do Firewall",

View File

@@ -18,6 +18,8 @@
"componentsMember": "Вы состоите в {count, plural, =0 {0 организациях} one {# организации} few {# организациях} many {# организациях} other {# организациях}}.", "componentsMember": "Вы состоите в {count, plural, =0 {0 организациях} one {# организации} few {# организациях} many {# организациях} other {# организациях}}.",
"componentsInvalidKey": "Обнаружены недействительные или просроченные лицензионные ключи. Соблюдайте условия лицензии для использования всех функций.", "componentsInvalidKey": "Обнаружены недействительные или просроченные лицензионные ключи. Соблюдайте условия лицензии для использования всех функций.",
"dismiss": "Отменить", "dismiss": "Отменить",
"subscriptionViolationMessage": "Вы превысили лимиты для вашего текущего плана. Исправьте проблему, удалив сайты, пользователей или другие ресурсы, чтобы остаться в пределах вашего плана.",
"subscriptionViolationViewBilling": "Просмотр биллинга",
"componentsLicenseViolation": "Нарушение лицензии: Сервер использует {usedSites} сайтов, что превышает лицензионный лимит в {maxSites} сайтов. Соблюдайте условия лицензии для использования всех функций.", "componentsLicenseViolation": "Нарушение лицензии: Сервер использует {usedSites} сайтов, что превышает лицензионный лимит в {maxSites} сайтов. Соблюдайте условия лицензии для использования всех функций.",
"componentsSupporterMessage": "Спасибо за поддержку Pangolin в качестве {tier}!", "componentsSupporterMessage": "Спасибо за поддержку Pangolin в качестве {tier}!",
"inviteErrorNotValid": "Извините, но это приглашение не было принято или срок его действия истёк.", "inviteErrorNotValid": "Извините, но это приглашение не было принято или срок его действия истёк.",
@@ -199,6 +201,7 @@
"protocolSelect": "Выберите протокол", "protocolSelect": "Выберите протокол",
"resourcePortNumber": "Номер порта", "resourcePortNumber": "Номер порта",
"resourcePortNumberDescription": "Внешний номер порта для проксирования запросов.", "resourcePortNumberDescription": "Внешний номер порта для проксирования запросов.",
"back": "Назад",
"cancel": "Отмена", "cancel": "Отмена",
"resourceConfig": "Фрагменты конфигурации", "resourceConfig": "Фрагменты конфигурации",
"resourceConfigDescription": "Скопируйте и вставьте эти сниппеты для настройки TCP/UDP ресурса", "resourceConfigDescription": "Скопируйте и вставьте эти сниппеты для настройки TCP/UDP ресурса",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Произошла ошибка при удалении организации.", "orgErrorDeleteMessage": "Произошла ошибка при удалении организации.",
"orgDeleted": "Организация удалена", "orgDeleted": "Организация удалена",
"orgDeletedMessage": "Организация и её данные были удалены.", "orgDeletedMessage": "Организация и её данные были удалены.",
"deleteAccount": "Удалить аккаунт",
"deleteAccountDescription": "Окончательно удалить учетную запись, все организации, которые вы владеете, и все данные этих организаций не могут быть отменены.",
"deleteAccountButton": "Удалить аккаунт",
"deleteAccountConfirmTitle": "Удалить аккаунт",
"deleteAccountConfirmMessage": "Это очистит ваш аккаунт, все организации, которым вы владеете, и все данные этих организаций не могут быть отменены.",
"deleteAccountConfirmString": "удалить аккаунт",
"deleteAccountSuccess": "Учетная запись удалена",
"deleteAccountSuccessMessage": "Ваша учетная запись удалена.",
"deleteAccountError": "Не удалось удалить аккаунт",
"deleteAccountPreviewAccount": "Ваша учетная запись",
"deleteAccountPreviewOrgs": "Организации, которые вы владеете (и все их данные)",
"orgMissing": "Отсутствует ID организации", "orgMissing": "Отсутствует ID организации",
"orgMissingMessage": "Невозможно восстановить приглашение без ID организации.", "orgMissingMessage": "Невозможно восстановить приглашение без ID организации.",
"accessUsersManage": "Управление пользователями", "accessUsersManage": "Управление пользователями",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Фильтр по состоянию утверждения", "filterByApprovalState": "Фильтр по состоянию утверждения",
"approvalListEmpty": "Нет утверждений", "approvalListEmpty": "Нет утверждений",
"approvalState": "Состояние одобрения", "approvalState": "Состояние одобрения",
"approvalLoadMore": "Загрузить еще",
"loadingApprovals": "Загрузка утверждений",
"approve": "Одобрить", "approve": "Одобрить",
"approved": "Одобрено", "approved": "Одобрено",
"denied": "Отказано", "denied": "Отказано",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Увеличить количество сайтов", "sitestCountIncrease": "Увеличить количество сайтов",
"idpManage": "Управление поставщиками удостоверений", "idpManage": "Управление поставщиками удостоверений",
"idpManageDescription": "Просмотр и управление поставщиками удостоверений в системе", "idpManageDescription": "Просмотр и управление поставщиками удостоверений в системе",
"idpGlobalModeBanner": "Поставщики удостоверений (IdP) для каждой организации отключены на этом сервере. Используются глобальные IdP (общие для всех организаций). Управляйте глобальными IdP в <adminPanelLink>админ-панели</adminPanelLink>. Чтобы включить IdP для каждой организации, отредактируйте конфигурацию сервера и установите режим IdP в org. <configDocsLink>См. документацию</configDocsLink>. Если вы хотите продолжать использовать глобальные IdP и скрыть это из настроек организации, явно установите режим в глобальном конфиге.",
"idpGlobalModeBannerUpgradeRequired": "Поставщики удостоверений (IdP) для каждой организации отключены на этом сервере. Используются глобальные IdP (общие для всех организаций). Управляйте глобальными IdP в <adminPanelLink>админ-панели</adminPanelLink>. Чтобы использовать поставщиков удостоверений для каждой организации, необходимо обновить систему до версии Enterprise.",
"idpGlobalModeBannerLicenseRequired": "Поставщики удостоверений (IdP) для каждой организации отключены на этом сервере. Используются глобальные IdP (общие для всех организаций). Управляйте глобальными IdP в <adminPanelLink>админ-панели</adminPanelLink>. Для использования поставщиков удостоверений на организацию требуется лицензия Enterprise.",
"idpDeletedDescription": "Поставщик удостоверений успешно удалён", "idpDeletedDescription": "Поставщик удостоверений успешно удалён",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Вы уверены, что хотите навсегда удалить поставщика удостоверений?", "idpQuestionRemove": "Вы уверены, что хотите навсегда удалить поставщика удостоверений?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Настройка - Pangolin", "pangolinSetup": "Настройка - Pangolin",
"orgNameRequired": "Название организации обязательно", "orgNameRequired": "Название организации обязательно",
"orgIdRequired": "ID организации обязателен", "orgIdRequired": "ID организации обязателен",
"orgIdMaxLength": "ID организации должен быть не более 32 символов",
"orgErrorCreate": "Произошла ошибка при создании организации", "orgErrorCreate": "Произошла ошибка при создании организации",
"pageNotFound": "Страница не найдена", "pageNotFound": "Страница не найдена",
"pageNotFoundDescription": "Упс! Страница, которую вы ищете, не существует.", "pageNotFoundDescription": "Упс! Страница, которую вы ищете, не существует.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Просмотр журналов", "actionViewLogs": "Просмотр журналов",
"noneSelected": "Ничего не выбрано", "noneSelected": "Ничего не выбрано",
"orgNotFound2": "Организации не найдены.", "orgNotFound2": "Организации не найдены.",
"searchProgress": "Поиск...", "searchPlaceholder": "Поиск...",
"emptySearchOptions": "Опции не найдены",
"create": "Создать", "create": "Создать",
"orgs": "Организации", "orgs": "Организации",
"loginError": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз.", "loginError": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Журнал и аналитика", "sidebarLogAndAnalytics": "Журнал и аналитика",
"sidebarBluePrints": "Чертежи", "sidebarBluePrints": "Чертежи",
"sidebarOrganization": "Организация", "sidebarOrganization": "Организация",
"sidebarBillingAndLicenses": "Биллинг и лицензии",
"sidebarLogsAnalytics": "Статистика", "sidebarLogsAnalytics": "Статистика",
"blueprints": "Чертежи", "blueprints": "Чертежи",
"blueprintsDescription": "Применить декларирующие конфигурации и просмотреть предыдущие запуски", "blueprintsDescription": "Применить декларирующие конфигурации и просмотреть предыдущие запуски",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Обзор лимитов использования", "billingUsageLimitsOverview": "Обзор лимитов использования",
"billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@pangolin.net.", "billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@pangolin.net.",
"billingDataUsage": "Использование данных", "billingDataUsage": "Использование данных",
"billingOnlineTime": "Время работы сайта", "billingSites": "Сайты",
"billingUsers": "Активные пользователи", "billingUsers": "Пользователи",
"billingDomains": "Активные домены", "billingDomains": "Домены",
"billingRemoteExitNodes": "Активные самоуправляемые узлы", "billingOrganizations": "Орги",
"billingRemoteExitNodes": "Удаленные узлы",
"billingNoLimitConfigured": "Лимит не установлен", "billingNoLimitConfigured": "Лимит не установлен",
"billingEstimatedPeriod": "Предполагаемый период выставления счетов", "billingEstimatedPeriod": "Предполагаемый период выставления счетов",
"billingIncludedUsage": "Включенное использование", "billingIncludedUsage": "Включенное использование",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Не удалось получить URL-адрес портала", "billingFailedToGetPortalUrl": "Не удалось получить URL-адрес портала",
"billingPortalError": "Ошибка портала", "billingPortalError": "Ошибка портала",
"billingDataUsageInfo": "Вы несете ответственность за все данные, переданные через безопасные туннели при подключении к облаку. Это включает как входящий, так и исходящий трафик на всех ваших сайтах. При достижении лимита ваши сайты будут отключаться до тех пор, пока вы не обновите план или не уменьшите его использование. При использовании узлов не взимается плата.", "billingDataUsageInfo": "Вы несете ответственность за все данные, переданные через безопасные туннели при подключении к облаку. Это включает как входящий, так и исходящий трафик на всех ваших сайтах. При достижении лимита ваши сайты будут отключаться до тех пор, пока вы не обновите план или не уменьшите его использование. При использовании узлов не взимается плата.",
"billingOnlineTimeInfo": "Вы тарифицируете на то, как долго ваши сайты будут подключены к облаку. Например, 44 640 минут равны одному сайту, работающему круглосуточно за весь месяц. Когда вы достигните лимита, ваши сайты будут отключаться до тех пор, пока вы не обновите тарифный план или не сократите нагрузку. При использовании узлов не тарифицируется.", "billingSInfo": "Сколько сайтов вы можете использовать",
"billingUsersInfo": "Вы оплачиваете за каждого пользователя в организации. Платеж рассчитывается ежедневно в зависимости от количества активных учетных записей в вашем органе.", "billingUsersInfo": "Сколько пользователей вы можете использовать",
"billingDomainInfo": "Вы платите за каждый домен в организации. Платеж рассчитывается ежедневно в зависимости от количества активных доменных аккаунтов в вашем органе.", "billingDomainInfo": "Сколько доменов вы можете использовать",
"billingRemoteExitNodesInfo": "Вы платите за каждый управляемый узел организации. Платёж рассчитывается ежедневно на основе количества активных управляемых узлов в вашем органе.", "billingRemoteExitNodesInfo": "Сколько удаленных узлов вы можете использовать",
"billingLicenseKeys": "Лицензионные ключи",
"billingLicenseKeysDescription": "Управление подписками на лицензионные ключи",
"billingLicenseSubscription": "Лицензионное соглашение",
"billingInactive": "Неактивный",
"billingLicenseItem": "Элемент лицензии",
"billingQuantity": "Количество",
"billingTotal": "итого",
"billingModifyLicenses": "Изменить лицензию подписки",
"domainNotFound": "Домен не найден", "domainNotFound": "Домен не найден",
"domainNotFoundDescription": "Этот ресурс отключен, так как домен больше не существует в нашей системе. Пожалуйста, установите новый домен для этого ресурса.", "domainNotFoundDescription": "Этот ресурс отключен, так как домен больше не существует в нашей системе. Пожалуйста, установите новый домен для этого ресурса.",
"failed": "Ошибка", "failed": "Ошибка",
"createNewOrgDescription": "Создать новую организацию", "createNewOrgDescription": "Создать новую организацию",
"organization": "Организация", "organization": "Организация",
"primary": "Первичный",
"port": "Порт", "port": "Порт",
"securityKeyManage": "Управление ключами безопасности", "securityKeyManage": "Управление ключами безопасности",
"securityKeyDescription": "Добавить или удалить ключи безопасности для аутентификации без пароля", "securityKeyDescription": "Добавить или удалить ключи безопасности для аутентификации без пароля",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "Номер порта необходим для не-HTTP ресурсов", "resourcePortRequired": "Номер порта необходим для не-HTTP ресурсов",
"resourcePortNotAllowed": "Номер порта не должен быть установлен для HTTP ресурсов", "resourcePortNotAllowed": "Номер порта не должен быть установлен для HTTP ресурсов",
"billingPricingCalculatorLink": "Калькулятор расценок", "billingPricingCalculatorLink": "Калькулятор расценок",
"billingYourPlan": "Ваш план",
"billingViewOrModifyPlan": "Просмотреть или изменить ваш текущий тариф",
"billingViewPlanDetails": "Подробности плана",
"billingUsageAndLimits": "Использование и ограничения",
"billingViewUsageAndLimits": "Просмотр лимитов и текущего использования вашего плана",
"billingCurrentUsage": "Текущее использование",
"billingMaximumLimits": "Максимальные ограничения",
"billingRemoteNodes": "Удаленные узлы",
"billingUnlimited": "Неограниченный",
"billingPaidLicenseKeys": "Платные лицензионные ключи",
"billingManageLicenseSubscription": "Управление подпиской на платные лицензионные ключи собственного хостинга",
"billingCurrentKeys": "Текущие ключи",
"billingModifyCurrentPlan": "Изменить текущий план",
"billingConfirmUpgrade": "Подтвердить обновление",
"billingConfirmDowngrade": "Подтверждение понижения",
"billingConfirmUpgradeDescription": "Вы собираетесь обновить тарифный план. Проверьте новые лимиты и цены ниже.",
"billingConfirmDowngradeDescription": "Вы собираетесь понизить тарифный план. Проверьте новые ограничения и цены ниже.",
"billingPlanIncludes": "Включает план",
"billingProcessing": "Обработка...",
"billingConfirmUpgradeButton": "Подтвердить обновление",
"billingConfirmDowngradeButton": "Подтверждение понижения",
"billingLimitViolationWarning": "Превышено количество новых лимитов плана",
"billingLimitViolationDescription": "Ваше текущее использование превышает лимиты этого плана. После понижения значения все действия будут отключены до уменьшения использования в пределах новых лимитов. Пожалуйста, ознакомьтесь с функциями, которые в настоящее время превышают лимиты. Ограничения:",
"billingFeatureLossWarning": "Уведомление о доступности функций",
"billingFeatureLossDescription": "При переходе на другой тарифный план функции не будут автоматически отключены. Некоторые настройки и конфигурации могут быть потеряны. Пожалуйста, ознакомьтесь с матрицей ценообразования, чтобы понять, какие функции больше не будут доступны.",
"billingUsageExceedsLimit": "Текущее использование ({current}) превышает предел ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Я согласен с", "IAgreeToThe": "Я согласен с",
"termsOfService": "условия использования", "termsOfService": "условия использования",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Вы уверены, что хотите удалить брендирование для страниц аутентификации?", "authPageBrandingQuestionRemove": "Вы уверены, что хотите удалить брендирование для страниц аутентификации?",
"authPageBrandingDeleteConfirm": "Подтвердить удаление брендирования", "authPageBrandingDeleteConfirm": "Подтвердить удаление брендирования",
"brandingLogoURL": "URL логотипа", "brandingLogoURL": "URL логотипа",
"brandingLogoURLOrPath": "URL логотипа или путь",
"brandingLogoPathDescription": "Введите URL или локальный путь.",
"brandingLogoURLDescription": "Введите публичный URL для изображения вашего логотипа.",
"brandingPrimaryColor": "Основной цвет", "brandingPrimaryColor": "Основной цвет",
"brandingLogoWidth": "Ширина (px)", "brandingLogoWidth": "Ширина (px)",
"brandingLogoHeight": "Высота (px)", "brandingLogoHeight": "Высота (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Вернуться к стандартному входу", "orgAuthBackToSignIn": "Вернуться к стандартному входу",
"orgAuthNoAccount": "Нет учётной записи?", "orgAuthNoAccount": "Нет учётной записи?",
"subscriptionRequiredToUse": "Для использования этой функции требуется подписка.", "subscriptionRequiredToUse": "Для использования этой функции требуется подписка.",
"mustUpgradeToUse": "Вы должны обновить подписку, чтобы использовать эту функцию.",
"subscriptionRequiredTierToUse": "Эта функция требует <tierLink>{tier}</tierLink> или выше.",
"upgradeToTierToUse": "Обновитесь до <tierLink>{tier}</tierLink> или выше, чтобы использовать эту функцию.",
"subscriptionTierTier1": "Главная",
"subscriptionTierTier2": "Команда",
"subscriptionTierTier3": "Бизнес",
"subscriptionTierEnterprise": "Предприятие",
"idpDisabled": "Провайдеры идентификации отключены.", "idpDisabled": "Провайдеры идентификации отключены.",
"orgAuthPageDisabled": "Страница авторизации организации отключена.", "orgAuthPageDisabled": "Страница авторизации организации отключена.",
"domainRestartedDescription": "Проверка домена успешно перезапущена", "domainRestartedDescription": "Проверка домена успешно перезапущена",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Получить лицензию",
"description": "Выберите план и расскажите нам, как вы планируете использовать Панголин.",
"chooseTier": "Выберите ваш план",
"viewPricingLink": "Смотрите цены, возможности и ограничения",
"tiers": {
"starter": {
"title": "Старт",
"description": "Функции предприятия, 25 пользователей, 25 сайтов, и поддержка сообщества."
},
"scale": {
"title": "Масштаб",
"description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка."
}
},
"personalUseOnly": "Только для личного пользования (бесплатная лицензия — без оформления)",
"buttons": {
"continueToCheckout": "Продолжить оформление заказа"
},
"toasts": {
"checkoutError": {
"title": "Ошибка оформления заказа",
"description": "Не удалось начать оформление заказа. Пожалуйста, попробуйте еще раз."
}
}
},
"priority": "Приоритет", "priority": "Приоритет",
"priorityDescription": "Маршруты с более высоким приоритетом оцениваются первым. Приоритет = 100 означает автоматическое упорядочение (решение системы). Используйте другой номер для обеспечения ручного приоритета.", "priorityDescription": "Маршруты с более высоким приоритетом оцениваются первым. Приоритет = 100 означает автоматическое упорядочение (решение системы). Используйте другой номер для обеспечения ручного приоритета.",
"instanceName": "Имя экземпляра", "instanceName": "Имя экземпляра",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Конец следующего года", "logRetentionEndOfFollowingYear": "Конец следующего года",
"actionLogsDescription": "Просмотр истории действий, выполненных в этой организации", "actionLogsDescription": "Просмотр истории действий, выполненных в этой организации",
"accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации", "accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации",
"licenseRequiredToUse": "Для использования этой функции требуется лицензия предприятия.", "licenseRequiredToUse": "Лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"ossEnterpriseEditionRequired": "Для использования этой функции требуется <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink>. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"certResolver": "Резольвер сертификата", "certResolver": "Резольвер сертификата",
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.", "certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
"selectCertResolver": "Выберите резолвер сертификата", "selectCertResolver": "Выберите резолвер сертификата",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Брандмауэр включен", "firewallEnabled": "Брандмауэр включен",
"autoUpdatesEnabled": "Автоматические обновления включены", "autoUpdatesEnabled": "Автоматические обновления включены",
"tpmAvailable": "Доступно TPM", "tpmAvailable": "Доступно TPM",
"windowsAntivirusEnabled": "Антивирус включен",
"macosSipEnabled": "Защита целостности системы (SIP)", "macosSipEnabled": "Защита целостности системы (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Стилс-режим брандмауэра", "macosFirewallStealthMode": "Стилс-режим брандмауэра",

View File

@@ -18,6 +18,8 @@
"componentsMember": "{count, plural, =0 {hiçbir organizasyon} one {bir organizasyon} other {# organizasyon}} üyesisiniz.", "componentsMember": "{count, plural, =0 {hiçbir organizasyon} one {bir organizasyon} other {# organizasyon}} üyesisiniz.",
"componentsInvalidKey": "Geçersiz veya süresi dolmuş lisans anahtarları tespit edildi. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.", "componentsInvalidKey": "Geçersiz veya süresi dolmuş lisans anahtarları tespit edildi. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.",
"dismiss": "Kapat", "dismiss": "Kapat",
"subscriptionViolationMessage": "Geçerli planınız için limitlerinizi aştınız. Planınız dahilinde kalmak için siteleri, kullanıcıları veya diğer kaynakları kaldırarak sorunu düzeltin.",
"subscriptionViolationViewBilling": "Faturalamayı görüntüle",
"componentsLicenseViolation": "Lisans İhlali: Bu sunucu, lisanslı sınırı olan {maxSites} sitesini aşarak {usedSites} site kullanmaktadır. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.", "componentsLicenseViolation": "Lisans İhlali: Bu sunucu, lisanslı sınırı olan {maxSites} sitesini aşarak {usedSites} site kullanmaktadır. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.",
"componentsSupporterMessage": "Pangolin'e {tier} olarak destek olduğunuz için teşekkür ederiz!", "componentsSupporterMessage": "Pangolin'e {tier} olarak destek olduğunuz için teşekkür ederiz!",
"inviteErrorNotValid": "Üzgünüz, ancak erişmeye çalıştığınız davet kabul edilmemiş veya artık geçerli değil gibi görünüyor.", "inviteErrorNotValid": "Üzgünüz, ancak erişmeye çalıştığınız davet kabul edilmemiş veya artık geçerli değil gibi görünüyor.",
@@ -199,6 +201,7 @@
"protocolSelect": "Bir protokol seçin", "protocolSelect": "Bir protokol seçin",
"resourcePortNumber": "Port Numarası", "resourcePortNumber": "Port Numarası",
"resourcePortNumberDescription": "Vekil istekler için harici port numarası.", "resourcePortNumberDescription": "Vekil istekler için harici port numarası.",
"back": "Geri",
"cancel": "İptal", "cancel": "İptal",
"resourceConfig": "Yapılandırma Parçaları", "resourceConfig": "Yapılandırma Parçaları",
"resourceConfigDescription": "TCP/UDP kaynağınızı kurmak için bu yapılandırma parçalarını kopyalayıp yapıştırın", "resourceConfigDescription": "TCP/UDP kaynağınızı kurmak için bu yapılandırma parçalarını kopyalayıp yapıştırın",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "Organizasyon silinirken bir hata oluştu.", "orgErrorDeleteMessage": "Organizasyon silinirken bir hata oluştu.",
"orgDeleted": "Organizasyon silindi", "orgDeleted": "Organizasyon silindi",
"orgDeletedMessage": "Organizasyon ve verileri silindi.", "orgDeletedMessage": "Organizasyon ve verileri silindi.",
"deleteAccount": "Hesabı Sil",
"deleteAccountDescription": "Hesabınızı, sahip olduğunuz tüm organizasyonları ve bu organizasyonlardaki tüm verileri kalıcı olarak silin. Bu geri alınamaz.",
"deleteAccountButton": "Hesabı Sil",
"deleteAccountConfirmTitle": "Hesabı Sil",
"deleteAccountConfirmMessage": "Bu işlem, hesabınızı, sahip olduğunuz tüm organizasyonları ve bu organizasyonlardaki tüm verileri kalıcı olarak silecektir. Bu geri alınamaz.",
"deleteAccountConfirmString": "hesabı sil",
"deleteAccountSuccess": "Hesap Silindi",
"deleteAccountSuccessMessage": "Hesabınız silindi.",
"deleteAccountError": "Hesabı silme başarısız oldu",
"deleteAccountPreviewAccount": "Hesabınız",
"deleteAccountPreviewOrgs": "Sahip olduğunuz organizasyonlar (ve tüm verileri)",
"orgMissing": "Organizasyon Kimliği Eksik", "orgMissing": "Organizasyon Kimliği Eksik",
"orgMissingMessage": "Organizasyon kimliği olmadan daveti yeniden oluşturmanız mümkün değildir.", "orgMissingMessage": "Organizasyon kimliği olmadan daveti yeniden oluşturmanız mümkün değildir.",
"accessUsersManage": "Kullanıcıları Yönet", "accessUsersManage": "Kullanıcıları Yönet",
@@ -459,6 +473,8 @@
"filterByApprovalState": "Onay Durumuna Göre Filtrele", "filterByApprovalState": "Onay Durumuna Göre Filtrele",
"approvalListEmpty": "Onay yok", "approvalListEmpty": "Onay yok",
"approvalState": "Onay Durumu", "approvalState": "Onay Durumu",
"approvalLoadMore": "Daha fazla yükle",
"loadingApprovals": "Onaylar Yükleniyor",
"approve": "Onayla", "approve": "Onayla",
"approved": "Onaylandı", "approved": "Onaylandı",
"denied": "Reddedildi", "denied": "Reddedildi",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "Site sayısını artır", "sitestCountIncrease": "Site sayısını artır",
"idpManage": "Kimlik Sağlayıcılarını Yönet", "idpManage": "Kimlik Sağlayıcılarını Yönet",
"idpManageDescription": "Sistem içindeki kimlik sağlayıcıları görün ve yönetin", "idpManageDescription": "Sistem içindeki kimlik sağlayıcıları görün ve yönetin",
"idpGlobalModeBanner": "Bu sunucuda örgüt başına kimlik sağlayıcılar (IdP'ler) devre dışı bırakılmıştır. Tüm örgütler arasında paylaşılan küresel IdP'leri kullanıyor. Küresel IdP'leri <adminPanelLink> yönetici panelinde </adminPanelLink>yönetin. Örgüt başına IdP'leri etkinleştirmek için, sunucu yapılandırmasını düzenleyin ve IdP modunu 'org' olarak ayarlayın. <configDocsLink> Belgeleri inceleyin </configDocsLink>. Küresel IdP'leri kullanmaya devam etmek istiyorsanız ve bunun örgüt ayarlarından kaybolmasını istiyorsanız, yapılandırmada modu otomatik olarak 'global' olarak ayarlayın.",
"idpGlobalModeBannerUpgradeRequired": "Bu sunucuda örgüt başına kimlik sağlayıcılar (IdP'ler) devre dışı bırakılmıştır. Tüm örgütler arasında paylaşılan küresel IdP'leri kullanıyor. Küresel IdP'leri <adminPanelLink> yönetici panelinde </adminPanelLink>yönetin. Örgüt başına kimlik sağlayıcılar kullanmak için, Enterprise sürümüne yükseltmeniz gerekmektedir.",
"idpGlobalModeBannerLicenseRequired": "Bu sunucuda örgüt başına kimlik sağlayıcılar (IdP'ler) devre dışı bırakılmıştır. Tüm örgütler arasında paylaşılan küresel IdP'leri kullanıyor. Küresel IdP'leri <adminPanelLink> yönetici panelinde </adminPanelLink>yönetin. Örgüt başına kimlik sağlayıcılar kullanmak için Enterprise lisansı gereklidir.",
"idpDeletedDescription": "Kimlik sağlayıcı başarıyla silindi", "idpDeletedDescription": "Kimlik sağlayıcı başarıyla silindi",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Kimlik sağlayıcısını kalıcı olarak silmek istediğinizden emin misiniz?", "idpQuestionRemove": "Kimlik sağlayıcısını kalıcı olarak silmek istediğinizden emin misiniz?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "Kurulum - Pangolin", "pangolinSetup": "Kurulum - Pangolin",
"orgNameRequired": "Kuruluş adı gereklidir", "orgNameRequired": "Kuruluş adı gereklidir",
"orgIdRequired": "Kuruluş ID gereklidir", "orgIdRequired": "Kuruluş ID gereklidir",
"orgIdMaxLength": "Organizasyon kimliği en fazla 32 karakter olmalıdır",
"orgErrorCreate": "Kuruluş oluşturulurken bir hata oluştu", "orgErrorCreate": "Kuruluş oluşturulurken bir hata oluştu",
"pageNotFound": "Sayfa Bulunamadı", "pageNotFound": "Sayfa Bulunamadı",
"pageNotFoundDescription": "Oops! Aradığınız sayfa mevcut değil.", "pageNotFoundDescription": "Oops! Aradığınız sayfa mevcut değil.",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "Kayıtları Görüntüle", "actionViewLogs": "Kayıtları Görüntüle",
"noneSelected": "Hiçbiri seçili değil", "noneSelected": "Hiçbiri seçili değil",
"orgNotFound2": "Hiçbir organizasyon bulunamadı.", "orgNotFound2": "Hiçbir organizasyon bulunamadı.",
"searchProgress": "Ara...", "searchPlaceholder": "Ara...",
"emptySearchOptions": "Seçenek bulunamadı",
"create": "Oluştur", "create": "Oluştur",
"orgs": "Organizasyonlar", "orgs": "Organizasyonlar",
"loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.", "loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "Kayıt & Analiz", "sidebarLogAndAnalytics": "Kayıt & Analiz",
"sidebarBluePrints": "Planlar", "sidebarBluePrints": "Planlar",
"sidebarOrganization": "Organizasyon", "sidebarOrganization": "Organizasyon",
"sidebarBillingAndLicenses": "Faturalandırma & Lisanslar",
"sidebarLogsAnalytics": "Analitik", "sidebarLogsAnalytics": "Analitik",
"blueprints": "Planlar", "blueprints": "Planlar",
"blueprintsDescription": "Deklaratif yapılandırmaları uygulayın ve önceki çalışmaları görüntüleyin", "blueprintsDescription": "Deklaratif yapılandırmaları uygulayın ve önceki çalışmaları görüntüleyin",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "Kullanım Limitleri Genel Görünümü", "billingUsageLimitsOverview": "Kullanım Limitleri Genel Görünümü",
"billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@pangolin.net adresinden bizimle iletişime geçin.", "billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@pangolin.net adresinden bizimle iletişime geçin.",
"billingDataUsage": "Veri Kullanımı", "billingDataUsage": "Veri Kullanımı",
"billingOnlineTime": "Site Çevrimiçi Süresi", "billingSites": "Siteler",
"billingUsers": "Aktif Kullanıcılar", "billingUsers": "Kullanıcılar",
"billingDomains": "Aktif Alanlar", "billingDomains": "Alan Adları",
"billingRemoteExitNodes": "Aktif Öz-Host Düğümleri", "billingOrganizations": "Organizasyonlar",
"billingRemoteExitNodes": "Uzak Düğümler",
"billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı", "billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı",
"billingEstimatedPeriod": "Tahmini Fatura Dönemi", "billingEstimatedPeriod": "Tahmini Fatura Dönemi",
"billingIncludedUsage": "Dahil Kullanım", "billingIncludedUsage": "Dahil Kullanım",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "Portal URL'si alınamadı", "billingFailedToGetPortalUrl": "Portal URL'si alınamadı",
"billingPortalError": "Portal Hatası", "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.", "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.", "billingSInfo": "Kaç tane site kullanabileceğiniz",
"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.", "billingUsersInfo": "Kaç tane kullanıcı kullanabileceğiniz",
"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.", "billingDomainInfo": "Kaç tane alan adı kullanabileceğiniz",
"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.", "billingRemoteExitNodesInfo": "Kaç tane uzaktan düğüm kullanabileceğiniz",
"billingLicenseKeys": "Lisans Anahtarları",
"billingLicenseKeysDescription": "Lisans anahtarı aboneliklerinizi yönetin",
"billingLicenseSubscription": "Lisans Aboneliği",
"billingInactive": "Pasif",
"billingLicenseItem": "Lisans Öğesi",
"billingQuantity": "Miktar",
"billingTotal": "toplam",
"billingModifyLicenses": "Lisans Aboneliğini Düzenle",
"domainNotFound": "Alan Adı Bulunamadı", "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.", "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", "failed": "Başarısız",
"createNewOrgDescription": "Yeni bir organizasyon oluşturun", "createNewOrgDescription": "Yeni bir organizasyon oluşturun",
"organization": "Kuruluş", "organization": "Kuruluş",
"primary": "Birincil",
"port": "Bağlantı Noktası", "port": "Bağlantı Noktası",
"securityKeyManage": "Güvenlik Anahtarlarını Yönet", "securityKeyManage": "Güvenlik Anahtarlarını Yönet",
"securityKeyDescription": "Şifresiz kimlik doğrulama için güvenlik anahtarları ekleyin veya kaldırın", "securityKeyDescription": "Şifresiz kimlik doğrulama için güvenlik anahtarları ekleyin veya kaldırın",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir", "resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir",
"resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı", "resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı",
"billingPricingCalculatorLink": "Fiyat Hesaplayıcı", "billingPricingCalculatorLink": "Fiyat Hesaplayıcı",
"billingYourPlan": "Planınız",
"billingViewOrModifyPlan": "Mevcut planınızı görüntüleyin veya düzenleyin",
"billingViewPlanDetails": "Plan Detaylarını Görüntüle",
"billingUsageAndLimits": "Kullanım ve Sınırlar",
"billingViewUsageAndLimits": "Planınızın limitlerini ve mevcut kullanım durumunu görüntüleyin",
"billingCurrentUsage": "Mevcut Kullanım",
"billingMaximumLimits": "Maksimum Sınırlar",
"billingRemoteNodes": "Uzak Düğümler",
"billingUnlimited": "Sınırsız",
"billingPaidLicenseKeys": "Ücretli Lisans Anahtarları",
"billingManageLicenseSubscription": "Kendi barındırdığınız ücretli lisans anahtarları için aboneliğinizi yönetin",
"billingCurrentKeys": "Mevcut Anahtarlar",
"billingModifyCurrentPlan": "Mevcut Planı Düzenle",
"billingConfirmUpgrade": "Yükseltmeyi Onayla",
"billingConfirmDowngrade": "Düşürmeyi Onayla",
"billingConfirmUpgradeDescription": "Planınızı yükseltmek üzeresiniz. Yeni limitleri ve fiyatları aşağıda inceleyin.",
"billingConfirmDowngradeDescription": "Planınızı düşürmek üzeresiniz. Yeni limitleri ve fiyatları aşağıda inceleyin.",
"billingPlanIncludes": "Plan İçerikleri",
"billingProcessing": "İşleniyor...",
"billingConfirmUpgradeButton": "Yükseltmeyi Onayla",
"billingConfirmDowngradeButton": "Düşürmeyi Onayla",
"billingLimitViolationWarning": "Kullanım Yeni Plan Sınırlarınııyor",
"billingLimitViolationDescription": "Mevcut kullanımınız bu planın sınırlarınııyor. Düzeltmelerden sonra, yeni sınırlar içinde kalana kadar tüm işlemler devre dışı bırakılacak. Lütfen şu anda limitlerin üzerinde olan özellikleri inceleyin. İhlal edilen sınırlar:",
"billingFeatureLossWarning": "Özellik Kullanılabilirlik Bildirimi",
"billingFeatureLossDescription": "Plan düşürüldüğünde, yeni planda mevcut olmayan özellikler otomatik olarak devre dışı bırakılacaktır. Bazı ayarlar ve yapılar kaybolabilir. Hangi özelliklerin artık mevcut olmayacağını anlamak için fiyat tablosunu inceleyiniz.",
"billingUsageExceedsLimit": "Mevcut kullanım ({current}) limitleri ({limit}) aşıyor",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Kabul ediyorum", "IAgreeToThe": "Kabul ediyorum",
"termsOfService": "hizmet şartları", "termsOfService": "hizmet şartları",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?", "authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?",
"authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla", "authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logo URL veya Yol",
"brandingLogoPathDescription": "Bir URL veya yerel bir yol girin.",
"brandingLogoURLDescription": "Logo resminiz için genel olarak erişilebilir bir URL girin.",
"brandingPrimaryColor": "Ana Renk", "brandingPrimaryColor": "Ana Renk",
"brandingLogoWidth": "Genişlik (px)", "brandingLogoWidth": "Genişlik (px)",
"brandingLogoHeight": "Yükseklik (px)", "brandingLogoHeight": "Yükseklik (px)",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "Standart girişe geri dön", "orgAuthBackToSignIn": "Standart girişe geri dön",
"orgAuthNoAccount": "Hesabınız yok mu?", "orgAuthNoAccount": "Hesabınız yok mu?",
"subscriptionRequiredToUse": "Bu özelliği kullanmak için abonelik gerekmektedir.", "subscriptionRequiredToUse": "Bu özelliği kullanmak için abonelik gerekmektedir.",
"mustUpgradeToUse": "Bu özelliği kullanmak için aboneliğinizi yükseltmelisiniz.",
"subscriptionRequiredTierToUse": "Bu özellik <tierLink>{tier}</tierLink> veya daha üstünü gerektirir.",
"upgradeToTierToUse": "Bu özelliği kullanmak için <tierLink>{tier}</tierLink> veya daha üst bir seviyeye yükseltin.",
"subscriptionTierTier1": "Ana Sayfa",
"subscriptionTierTier2": "Takım",
"subscriptionTierTier3": "İşletme",
"subscriptionTierEnterprise": "Kurumsal",
"idpDisabled": "Kimlik sağlayıcılar devre dışı bırakılmıştır.", "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.", "orgAuthPageDisabled": "Kuruluş kimlik doğrulama sayfası devre dışı bırakılmıştır.",
"domainRestartedDescription": "Alan doğrulaması başarıyla yeniden başlatıldı", "domainRestartedDescription": "Alan doğrulaması başarıyla yeniden başlatıldı",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "Bir lisans alın",
"description": "Bir plan seçin ve Pangolin'i nasıl kullanmayı planladığınızı anlatın.",
"chooseTier": "Planınızı seçin",
"viewPricingLink": "Fiyatları, özellikleri ve limitleri görüntüleyin",
"tiers": {
"starter": {
"title": "Başlangıç",
"description": "Kurumsal özellikler, 25 kullanıcı, 25 site ve topluluk desteği."
},
"scale": {
"title": "Ölçek",
"description": "Kurumsal özellikler, 50 kullanıcı, 50 site ve öncelikli destek."
}
},
"personalUseOnly": "Yalnızca kişisel kullanım (ücretsiz lisans — ödeme yapılmaz)",
"buttons": {
"continueToCheckout": "Ödemeye Devam Et"
},
"toasts": {
"checkoutError": {
"title": "Ödeme Hatası",
"description": "Ödeme işlemi başlatılamadı. Lütfen tekrar deneyin."
}
}
},
"priority": "Öncelik", "priority": "Öncelik",
"priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.", "priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.",
"instanceName": "Örnek İsmi", "instanceName": "Örnek İsmi",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu", "logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu",
"actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin", "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", "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.", "licenseRequiredToUse": "Bu özelliği kullanmak için bir <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisansı gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>'da da mevcuttur.",
"ossEnterpriseEditionRequired": "Bu özelliği kullanmak için <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>'da da mevcuttur.",
"certResolver": "Sertifika Çözücü", "certResolver": "Sertifika Çözücü",
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.", "certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
"selectCertResolver": "Sertifika Çözücü Seçin", "selectCertResolver": "Sertifika Çözücü Seçin",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "Güvenlik Duvarı Etkin", "firewallEnabled": "Güvenlik Duvarı Etkin",
"autoUpdatesEnabled": "Otomatik Güncellemeler Etkin", "autoUpdatesEnabled": "Otomatik Güncellemeler Etkin",
"tpmAvailable": "TPM Mevcut", "tpmAvailable": "TPM Mevcut",
"windowsAntivirusEnabled": "Antivirüs Etkinleştirildi",
"macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)", "macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu", "macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu",

View File

@@ -18,6 +18,8 @@
"componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。", "componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。",
"componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。", "componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。",
"dismiss": "忽略", "dismiss": "忽略",
"subscriptionViolationMessage": "您的当前计划超出了您的限制。通过移除站点、用户或其他资源以保持在您的计划范围内来纠正问题。",
"subscriptionViolationViewBilling": "查看计费",
"componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。", "componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。",
"componentsSupporterMessage": "感谢您的支持!您现在是 Pangolin 的 {tier} 用户。", "componentsSupporterMessage": "感谢您的支持!您现在是 Pangolin 的 {tier} 用户。",
"inviteErrorNotValid": "很抱歉,但看起来你试图访问的邀请尚未被接受或不再有效。", "inviteErrorNotValid": "很抱歉,但看起来你试图访问的邀请尚未被接受或不再有效。",
@@ -199,6 +201,7 @@
"protocolSelect": "选择协议", "protocolSelect": "选择协议",
"resourcePortNumber": "端口号", "resourcePortNumber": "端口号",
"resourcePortNumberDescription": "代理请求的外部端口号。", "resourcePortNumberDescription": "代理请求的外部端口号。",
"back": "后退",
"cancel": "取消", "cancel": "取消",
"resourceConfig": "配置片段", "resourceConfig": "配置片段",
"resourceConfigDescription": "复制并粘贴这些配置片段以设置 TCP/UDP 资源", "resourceConfigDescription": "复制并粘贴这些配置片段以设置 TCP/UDP 资源",
@@ -244,6 +247,17 @@
"orgErrorDeleteMessage": "删除组织时出错。", "orgErrorDeleteMessage": "删除组织时出错。",
"orgDeleted": "组织已删除", "orgDeleted": "组织已删除",
"orgDeletedMessage": "组织及其数据已被删除。", "orgDeletedMessage": "组织及其数据已被删除。",
"deleteAccount": "删除帐户",
"deleteAccountDescription": "永久删除您的帐户、您拥有的所有组织以及这些组织中的所有数据。此操作无法撤消。",
"deleteAccountButton": "删除帐户",
"deleteAccountConfirmTitle": "删除帐户",
"deleteAccountConfirmMessage": "这将永久擦除您的帐户、您拥有的所有组织以及这些组织中的所有数据。这不能撤消。",
"deleteAccountConfirmString": "删除帐户",
"deleteAccountSuccess": "账户已删除",
"deleteAccountSuccessMessage": "您的帐户已被删除。",
"deleteAccountError": "删除帐户失败",
"deleteAccountPreviewAccount": "您的帐户",
"deleteAccountPreviewOrgs": "您拥有的组织 (和所有数据)",
"orgMissing": "缺少组织 ID", "orgMissing": "缺少组织 ID",
"orgMissingMessage": "没有组织ID无法重新生成邀请。", "orgMissingMessage": "没有组织ID无法重新生成邀请。",
"accessUsersManage": "管理用户", "accessUsersManage": "管理用户",
@@ -459,6 +473,8 @@
"filterByApprovalState": "按批准状态过滤", "filterByApprovalState": "按批准状态过滤",
"approvalListEmpty": "无批准", "approvalListEmpty": "无批准",
"approvalState": "审批状态", "approvalState": "审批状态",
"approvalLoadMore": "加载更多",
"loadingApprovals": "正在加载批准",
"approve": "批准", "approve": "批准",
"approved": "已批准", "approved": "已批准",
"denied": "被拒绝", "denied": "被拒绝",
@@ -789,6 +805,9 @@
"sitestCountIncrease": "增加站点数量", "sitestCountIncrease": "增加站点数量",
"idpManage": "管理身份提供商", "idpManage": "管理身份提供商",
"idpManageDescription": "查看和管理系统中的身份提供商", "idpManageDescription": "查看和管理系统中的身份提供商",
"idpGlobalModeBanner": "此服务器上禁用了每个组织的身份提供商(Idps)。 它正在使用全局IdP(所有组织共享)。在 <adminPanelLink>管理面板</adminPanelLink>中管理全局IdP。 要启用每个组织的 IdP请编辑服务器配置并将 IdP 模式设置为 org。 <configDocsLink>请参阅文档</configDocsLink>。 如果您想要继续使用全局IdP并使其从组织设置中消失请在配置中将模式设置为全局模式。",
"idpGlobalModeBannerUpgradeRequired": "此服务器上禁用了每个组织的身份提供商(Idps)。它正在使用全局身份提供商(所有组织共享)。 在 <adminPanelLink>管理面板</adminPanelLink>管理全局身份。要使用每个组织的身份提供者,您必须升级到企业版本。",
"idpGlobalModeBannerLicenseRequired": "此服务器上禁用了每个组织的身份提供商(Idps)。它正在使用全局身份提供商(所有组织共享)。 在 <adminPanelLink>管理面板</adminPanelLink>管理全局身份。要使用每个组织的身份提供者,需要企业许可证。",
"idpDeletedDescription": "身份提供商删除成功", "idpDeletedDescription": "身份提供商删除成功",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "您确定要永久删除身份提供者吗?", "idpQuestionRemove": "您确定要永久删除身份提供者吗?",
@@ -1012,6 +1031,7 @@
"pangolinSetup": "认证 - Pangolin", "pangolinSetup": "认证 - Pangolin",
"orgNameRequired": "组织名称是必需的", "orgNameRequired": "组织名称是必需的",
"orgIdRequired": "组织ID是必需的", "orgIdRequired": "组织ID是必需的",
"orgIdMaxLength": "组织 ID 必须至少 32 个字符",
"orgErrorCreate": "创建组织时出错", "orgErrorCreate": "创建组织时出错",
"pageNotFound": "找不到页面", "pageNotFound": "找不到页面",
"pageNotFoundDescription": "哎呀!您正在查找的页面不存在。", "pageNotFoundDescription": "哎呀!您正在查找的页面不存在。",
@@ -1164,7 +1184,8 @@
"actionViewLogs": "查看日志", "actionViewLogs": "查看日志",
"noneSelected": "未选择", "noneSelected": "未选择",
"orgNotFound2": "未找到组织。", "orgNotFound2": "未找到组织。",
"searchProgress": "搜索...", "searchPlaceholder": "搜索...",
"emptySearchOptions": "未找到选项",
"create": "创建", "create": "创建",
"orgs": "组织", "orgs": "组织",
"loginError": "发生意外错误。请重试。", "loginError": "发生意外错误。请重试。",
@@ -1246,6 +1267,7 @@
"sidebarLogAndAnalytics": "日志与分析", "sidebarLogAndAnalytics": "日志与分析",
"sidebarBluePrints": "蓝图", "sidebarBluePrints": "蓝图",
"sidebarOrganization": "组织", "sidebarOrganization": "组织",
"sidebarBillingAndLicenses": "帐单和许可证",
"sidebarLogsAnalytics": "分析", "sidebarLogsAnalytics": "分析",
"blueprints": "蓝图", "blueprints": "蓝图",
"blueprintsDescription": "应用声明配置并查看先前运行的", "blueprintsDescription": "应用声明配置并查看先前运行的",
@@ -1404,10 +1426,11 @@
"billingUsageLimitsOverview": "使用限制概览", "billingUsageLimitsOverview": "使用限制概览",
"billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@pangolin.net。", "billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@pangolin.net。",
"billingDataUsage": "数据使用情况", "billingDataUsage": "数据使用情况",
"billingOnlineTime": "站点在线时间", "billingSites": "站点",
"billingUsers": "活跃用户", "billingUsers": "用户",
"billingDomains": "活跃域", "billingDomains": "域",
"billingRemoteExitNodes": "活跃自托管节点", "billingOrganizations": "球队",
"billingRemoteExitNodes": "远程节点",
"billingNoLimitConfigured": "未配置限制", "billingNoLimitConfigured": "未配置限制",
"billingEstimatedPeriod": "估计结算周期", "billingEstimatedPeriod": "估计结算周期",
"billingIncludedUsage": "包含的使用量", "billingIncludedUsage": "包含的使用量",
@@ -1432,15 +1455,24 @@
"billingFailedToGetPortalUrl": "无法获取门户网址", "billingFailedToGetPortalUrl": "无法获取门户网址",
"billingPortalError": "门户错误", "billingPortalError": "门户错误",
"billingDataUsageInfo": "当连接到云端时,您将为通过安全隧道传输的所有数据收取费用。 这包括您所有站点的进出流量。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取数据。", "billingDataUsageInfo": "当连接到云端时,您将为通过安全隧道传输的所有数据收取费用。 这包括您所有站点的进出流量。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取数据。",
"billingOnlineTimeInfo": "您要根据您的网站连接到云端的时间长短收取费用。 例如44,640分钟等于一个24/7全月运行的网站。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取费用。", "billingSInfo": "您可以使用多少站点",
"billingUsersInfo": "您为组织中的每个用户收取费用。每日计费是根据您组织中活跃用户帐户的数量计算的。", "billingUsersInfo": "您可以使用多少用户",
"billingDomainInfo": "您在组织中的每个域都要收取费用。每日计费是根据您组织中的活动域帐户数计算的。", "billingDomainInfo": "您可以使用多少域",
"billingRemoteExitNodesInfo": "您为组织中的每个管理节点收取费用。计费是每日根据您组织中活跃的管理节点数计算的。", "billingRemoteExitNodesInfo": "您可以使用多少远程节点",
"billingLicenseKeys": "许可证密钥",
"billingLicenseKeysDescription": "管理您的许可证密钥订阅",
"billingLicenseSubscription": "许可订阅",
"billingInactive": "未激活",
"billingLicenseItem": "许可证项目",
"billingQuantity": "数量",
"billingTotal": "总计",
"billingModifyLicenses": "修改许可订阅",
"domainNotFound": "域未找到", "domainNotFound": "域未找到",
"domainNotFoundDescription": "此资源已禁用,因为该域不再在我们的系统中存在。请为此资源设置一个新域。", "domainNotFoundDescription": "此资源已禁用,因为该域不再在我们的系统中存在。请为此资源设置一个新域。",
"failed": "失败", "failed": "失败",
"createNewOrgDescription": "创建一个新组织", "createNewOrgDescription": "创建一个新组织",
"organization": "组织", "organization": "组织",
"primary": "主要的",
"port": "端口", "port": "端口",
"securityKeyManage": "管理安全密钥", "securityKeyManage": "管理安全密钥",
"securityKeyDescription": "添加或删除用于无密码认证的安全密钥", "securityKeyDescription": "添加或删除用于无密码认证的安全密钥",
@@ -1512,6 +1544,32 @@
"resourcePortRequired": "非 HTTP 资源必须输入端口号", "resourcePortRequired": "非 HTTP 资源必须输入端口号",
"resourcePortNotAllowed": "HTTP 资源不应设置端口号", "resourcePortNotAllowed": "HTTP 资源不应设置端口号",
"billingPricingCalculatorLink": "价格计算器", "billingPricingCalculatorLink": "价格计算器",
"billingYourPlan": "您的计划",
"billingViewOrModifyPlan": "查看或修改您当前的计划",
"billingViewPlanDetails": "查看计划详细信息",
"billingUsageAndLimits": "用法和限制",
"billingViewUsageAndLimits": "查看您的计划限制和当前使用情况",
"billingCurrentUsage": "当前使用情况",
"billingMaximumLimits": "最大限制",
"billingRemoteNodes": "远程节点",
"billingUnlimited": "无限制",
"billingPaidLicenseKeys": "付费许可证密钥",
"billingManageLicenseSubscription": "管理您对付费的自托管许可证密钥的订阅",
"billingCurrentKeys": "当前密钥",
"billingModifyCurrentPlan": "修改当前计划",
"billingConfirmUpgrade": "确认升级",
"billingConfirmDowngrade": "确认降级",
"billingConfirmUpgradeDescription": "您即将升级您的计划。请检查下面的新限额和定价。",
"billingConfirmDowngradeDescription": "您即将降级计划。请检查下面的新限额和定价。",
"billingPlanIncludes": "计划包含",
"billingProcessing": "正在处理...",
"billingConfirmUpgradeButton": "确认升级",
"billingConfirmDowngradeButton": "确认降级",
"billingLimitViolationWarning": "超出新计划限制",
"billingLimitViolationDescription": "您当前的使用量超过了此计划的限制。降级后,所有操作都将被禁用,直到您在新的限制范围内减少使用量。 请查看以下当前超出限制的特性:",
"billingFeatureLossWarning": "功能可用通知",
"billingFeatureLossDescription": "如果降级,新计划中不可用的功能将被自动禁用。一些设置和配置可能会丢失。 请查看定价矩阵以了解哪些功能将不再可用。",
"billingUsageExceedsLimit": "当前使用量 ({current}) 超出限制 ({limit})",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "我同意", "IAgreeToThe": "我同意",
"termsOfService": "服务条款", "termsOfService": "服务条款",
@@ -1877,6 +1935,9 @@
"authPageBrandingQuestionRemove": "您确定要移除授权页面的品牌吗?", "authPageBrandingQuestionRemove": "您确定要移除授权页面的品牌吗?",
"authPageBrandingDeleteConfirm": "确认删除品牌", "authPageBrandingDeleteConfirm": "确认删除品牌",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "徽标URL或路径",
"brandingLogoPathDescription": "输入网址或本地路径。",
"brandingLogoURLDescription": "请在您的徽标图片中输入一个可公开访问的 URL。",
"brandingPrimaryColor": "主要颜色", "brandingPrimaryColor": "主要颜色",
"brandingLogoWidth": "宽度px", "brandingLogoWidth": "宽度px",
"brandingLogoHeight": "高度px", "brandingLogoHeight": "高度px",
@@ -1926,6 +1987,13 @@
"orgAuthBackToSignIn": "返回标准登录", "orgAuthBackToSignIn": "返回标准登录",
"orgAuthNoAccount": "没有账户?", "orgAuthNoAccount": "没有账户?",
"subscriptionRequiredToUse": "需要订阅才能使用此功能。", "subscriptionRequiredToUse": "需要订阅才能使用此功能。",
"mustUpgradeToUse": "您必须升级您的订阅才能使用此功能。",
"subscriptionRequiredTierToUse": "此功能需要 <tierLink>{tier}</tierLink> 或更高级别。",
"upgradeToTierToUse": "升级到 <tierLink>{tier}</tierLink> 或更高级别以使用此功能。",
"subscriptionTierTier1": "首页",
"subscriptionTierTier2": "团队",
"subscriptionTierTier3": "业务",
"subscriptionTierEnterprise": "企业",
"idpDisabled": "身份提供者已禁用。", "idpDisabled": "身份提供者已禁用。",
"orgAuthPageDisabled": "组织认证页面已禁用。", "orgAuthPageDisabled": "组织认证页面已禁用。",
"domainRestartedDescription": "域验证重新启动成功", "domainRestartedDescription": "域验证重新启动成功",
@@ -2113,6 +2181,32 @@
} }
} }
}, },
"newPricingLicenseForm": {
"title": "获取许可证",
"description": "选择一个计划,告诉我们你计划如何使用 Pangolin。",
"chooseTier": "选择您的计划",
"viewPricingLink": "查看价格、特征和限制",
"tiers": {
"starter": {
"title": "启动器",
"description": "企业特征25个用户25个站点和社区支持。"
},
"scale": {
"title": "缩放比例",
"description": "企业特征、50个用户、50个站点和优先支持。"
}
},
"personalUseOnly": "仅供个人使用 (免费许可证-无签出)",
"buttons": {
"continueToCheckout": "继续签出"
},
"toasts": {
"checkoutError": {
"title": "签出错误",
"description": "无法启动结帐。请重试。"
}
}
},
"priority": "优先权", "priority": "优先权",
"priorityDescription": "先评估更高优先级线路。优先级 = 100意味着自动排序(系统决定). 使用另一个数字强制执行手动优先级。", "priorityDescription": "先评估更高优先级线路。优先级 = 100意味着自动排序(系统决定). 使用另一个数字强制执行手动优先级。",
"instanceName": "实例名称", "instanceName": "实例名称",
@@ -2211,7 +2305,8 @@
"logRetentionEndOfFollowingYear": "下一年结束", "logRetentionEndOfFollowingYear": "下一年结束",
"actionLogsDescription": "查看此机构执行的操作历史", "actionLogsDescription": "查看此机构执行的操作历史",
"accessLogsDescription": "查看此机构资源的访问认证请求", "accessLogsDescription": "查看此机构资源的访问认证请求",
"licenseRequiredToUse": "需要企业许可才能使用此功能。", "licenseRequiredToUse": "需要 <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> 许可才能使用此功能。此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> 中使用。",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> 需要使用此功能。此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> 中使用。",
"certResolver": "证书解决器", "certResolver": "证书解决器",
"certResolverDescription": "选择用于此资源的证书解析器。", "certResolverDescription": "选择用于此资源的证书解析器。",
"selectCertResolver": "选择证书解析", "selectCertResolver": "选择证书解析",
@@ -2510,6 +2605,7 @@
"firewallEnabled": "防火墙已启用", "firewallEnabled": "防火墙已启用",
"autoUpdatesEnabled": "启用自动更新", "autoUpdatesEnabled": "启用自动更新",
"tpmAvailable": "TPM 可用", "tpmAvailable": "TPM 可用",
"windowsAntivirusEnabled": "抗病毒已启用",
"macosSipEnabled": "系统完整性保护 (SIP)", "macosSipEnabled": "系统完整性保护 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "防火墙隐形模式", "macosFirewallStealthMode": "防火墙隐形模式",

4127
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,10 +13,9 @@
"scripts": { "scripts": {
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts", "dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
"dev:check": "npx tsc --noEmit && npm run format:check", "dev:check": "npx tsc --noEmit && npm run format:check",
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:generate && npm run db:sqlite:push", "dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push",
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts", "db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
"db:pg:push": "npx tsx server/db/pg/migrate.ts", "db:push": "npx tsx server/db/migrate.ts",
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts", "db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
"db:clear-migrations": "rm -rf server/migrations", "db:clear-migrations": "rm -rf server/migrations",
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json", "set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
@@ -34,8 +33,8 @@
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "8.4.0", "@asteasolutions/zod-to-openapi": "8.4.0",
"@aws-sdk/client-s3": "3.971.0", "@aws-sdk/client-s3": "3.989.0",
"@faker-js/faker": "10.2.0", "@faker-js/faker": "10.3.0",
"@headlessui/react": "2.2.9", "@headlessui/react": "2.2.9",
"@hookform/resolvers": "5.2.2", "@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
@@ -60,67 +59,66 @@
"@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-tooltip": "1.2.8",
"@react-email/components": "1.0.2", "@react-email/components": "1.0.7",
"@react-email/render": "2.0.0", "@react-email/render": "2.0.4",
"@react-email/tailwind": "2.0.2", "@react-email/tailwind": "2.0.4",
"@simplewebauthn/browser": "13.2.2", "@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2", "@simplewebauthn/server": "13.2.2",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.90.12", "@tanstack/react-query": "5.90.21",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"arctic": "3.7.0", "arctic": "3.7.0",
"axios": "1.13.2", "axios": "1.13.5",
"better-sqlite3": "11.9.1", "better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4", "canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
"clsx": "2.1.1", "clsx": "2.1.1",
"cmdk": "1.1.1", "cmdk": "1.1.1",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"cors": "2.8.5", "cors": "2.8.6",
"crypto-js": "4.2.0", "crypto-js": "4.2.0",
"d3": "7.9.0", "d3": "7.9.0",
"date-fns": "4.1.0",
"drizzle-orm": "0.45.1", "drizzle-orm": "0.45.1",
"eslint": "9.39.2",
"eslint-config-next": "16.1.0",
"express": "5.2.1", "express": "5.2.1",
"express-rate-limit": "8.2.1", "express-rate-limit": "8.2.1",
"glob": "13.0.0", "glob": "13.0.3",
"helmet": "8.1.0", "helmet": "8.1.0",
"http-errors": "2.0.1", "http-errors": "2.0.1",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "5.9.2", "ioredis": "5.9.3",
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
"lucide-react": "0.562.0", "lucide-react": "0.563.0",
"maxmind": "5.0.1", "maxmind": "5.0.5",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.5.9", "next": "15.5.12",
"next-intl": "4.7.0", "next-intl": "4.8.2",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "3.9.17", "nextjs-toploader": "3.9.17",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"nodemailer": "7.0.11", "nodemailer": "8.0.1",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.17.1", "pg": "8.18.0",
"posthog-node": "5.23.0", "posthog-node": "5.24.15",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.3", "react": "19.2.4",
"react-day-picker": "9.13.0", "react-day-picker": "9.13.2",
"react-dom": "19.2.3", "react-dom": "19.2.4",
"react-easy-sort": "1.8.0", "react-easy-sort": "1.8.0",
"react-hook-form": "7.71.1", "react-hook-form": "7.71.1",
"react-icons": "5.5.0", "react-icons": "5.5.0",
"recharts": "2.15.4", "recharts": "2.15.4",
"reodotdev": "1.0.0", "reodotdev": "1.0.0",
"resend": "6.8.0", "resend": "6.9.2",
"semver": "7.7.3", "semver": "7.7.4",
"stripe": "20.2.0", "sshpk": "^1.18.0",
"stripe": "20.3.1",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.4.0", "tailwind-merge": "3.4.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
"tw-animate-css": "1.4.0", "tw-animate-css": "1.4.0",
"use-debounce": "^10.1.0",
"uuid": "13.0.0", "uuid": "13.0.0",
"vaul": "1.1.2", "vaul": "1.1.2",
"visionscarto-world-atlas": "1.0.0", "visionscarto-world-atlas": "1.0.0",
@@ -129,14 +127,15 @@
"ws": "8.19.0", "ws": "8.19.0",
"yaml": "2.8.2", "yaml": "2.8.2",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "4.3.5", "zod": "4.3.6",
"zod-validation-error": "5.0.0" "zod-validation-error": "5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.51.2", "@dotenvx/dotenvx": "1.52.0",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/preview-server": "5.2.8",
"@tailwindcss/postcss": "4.1.18", "@tailwindcss/postcss": "4.1.18",
"@tanstack/react-query-devtools": "5.91.1", "@tanstack/react-query-devtools": "5.91.3",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
@@ -145,30 +144,33 @@
"@types/express": "5.0.6", "@types/express": "5.0.6",
"@types/express-session": "1.18.2", "@types/express-session": "1.18.2",
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "24.10.2", "@types/node": "25.2.3",
"@types/nodemailer": "7.0.4", "@types/nodemailer": "7.0.9",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/pg": "8.16.0", "@types/pg": "8.16.0",
"@types/react": "19.2.7", "@types/react": "19.2.14",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/sshpk": "^1.17.4",
"@types/swagger-ui-express": "4.1.8", "@types/swagger-ui-express": "4.1.8",
"@types/topojson-client": "3.1.5", "@types/topojson-client": "3.1.5",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@types/yargs": "17.0.35", "@types/yargs": "17.0.35",
"@types/js-yaml": "4.0.9",
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.8", "drizzle-kit": "0.31.9",
"esbuild": "0.27.2", "esbuild": "0.27.3",
"esbuild-node-externals": "1.20.1", "esbuild-node-externals": "1.20.1",
"eslint": "9.39.2",
"eslint-config-next": "16.1.6",
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.8.0", "prettier": "3.8.1",
"react-email": "5.2.5", "react-email": "5.2.8",
"tailwindcss": "4.1.18", "tailwindcss": "4.1.18",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.53.1" "typescript-eslint": "8.55.0"
} }
} }

View File

@@ -131,7 +131,8 @@ export enum ActionsEnum {
viewLogs = "viewLogs", viewLogs = "viewLogs",
exportLogs = "exportLogs", exportLogs = "exportLogs",
listApprovals = "listApprovals", listApprovals = "listApprovals",
updateApprovals = "updateApprovals" updateApprovals = "updateApprovals",
signSshKey = "signSshKey"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(

View File

@@ -0,0 +1,45 @@
import { db } from "@server/db";
import { and, eq } from "drizzle-orm";
import { roleSiteResources, userSiteResources } from "@server/db";
export async function canUserAccessSiteResource({
userId,
resourceId,
roleId
}: {
userId: string;
resourceId: number;
roleId: number;
}): Promise<boolean> {
const roleResourceAccess = await db
.select()
.from(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, resourceId),
eq(roleSiteResources.roleId, roleId)
)
)
.limit(1);
if (roleResourceAccess.length > 0) {
return true;
}
const userResourceAccess = await db
.select()
.from(userSiteResources)
.where(
and(
eq(userSiteResources.userId, userId),
eq(userSiteResources.siteResourceId, resourceId)
)
)
.limit(1);
if (userResourceAccess.length > 0) {
return true;
}
return false;
}

View File

@@ -56,15 +56,15 @@ Ensure drizzle-kit is installed.
You must have a connection string in your config file, as shown above. You must have a connection string in your config file, as shown above.
```bash ```bash
npm run db:pg:generate npm run db:generate
npm run db:pg:push npm run db:push
``` ```
### SQLite ### SQLite
```bash ```bash
npm run db:sqlite:generate npm run db:generate
npm run db:sqlite:push npm run db:push
``` ```
## Build Time ## Build Time

3
server/db/migrate.ts Normal file
View File

@@ -0,0 +1,3 @@
import { runMigrations } from "./";
await runMigrations();

View File

@@ -1,3 +1,4 @@
export * from "./driver"; export * from "./driver";
export * from "./schema/schema"; export * from "./schema/schema";
export * from "./schema/privateSchema"; export * from "./schema/privateSchema";
export * from "./migrate";

View File

@@ -4,7 +4,7 @@ import path from "path";
const migrationsFolder = path.join("server/migrations"); const migrationsFolder = path.join("server/migrations");
const runMigrations = async () => { export const runMigrations = async () => {
console.log("Running migrations..."); console.log("Running migrations...");
try { try {
await migrate(db as any, { await migrate(db as any, {
@@ -17,5 +17,3 @@ const runMigrations = async () => {
process.exit(1); process.exit(1);
} }
}; };
runMigrations();

View File

@@ -82,11 +82,14 @@ export const subscriptions = pgTable("subscriptions", {
canceledAt: bigint("canceledAt", { mode: "number" }), canceledAt: bigint("canceledAt", { mode: "number" }),
createdAt: bigint("createdAt", { mode: "number" }).notNull(), createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }), updatedAt: bigint("updatedAt", { mode: "number" }),
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }) version: integer("version"),
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
type: varchar("type", { length: 50 }) // tier1, tier2, tier3, or license
}); });
export const subscriptionItems = pgTable("subscriptionItems", { export const subscriptionItems = pgTable("subscriptionItems", {
subscriptionItemId: serial("subscriptionItemId").primaryKey(), subscriptionItemId: serial("subscriptionItemId").primaryKey(),
stripeSubscriptionItemId: varchar("stripeSubscriptionItemId", { length: 255 }),
subscriptionId: varchar("subscriptionId", { length: 255 }) subscriptionId: varchar("subscriptionId", { length: 255 })
.notNull() .notNull()
.references(() => subscriptions.subscriptionId, { .references(() => subscriptions.subscriptionId, {
@@ -94,6 +97,7 @@ export const subscriptionItems = pgTable("subscriptionItems", {
}), }),
planId: varchar("planId", { length: 255 }).notNull(), planId: varchar("planId", { length: 255 }).notNull(),
priceId: varchar("priceId", { length: 255 }), priceId: varchar("priceId", { length: 255 }),
featureId: varchar("featureId", { length: 255 }),
meterId: varchar("meterId", { length: 255 }), meterId: varchar("meterId", { length: 255 }),
unitAmount: real("unitAmount"), unitAmount: real("unitAmount"),
tiers: text("tiers"), tiers: text("tiers"),
@@ -136,6 +140,7 @@ export const limits = pgTable("limits", {
}) })
.notNull(), .notNull(),
value: real("value"), value: real("value"),
override: boolean("override").default(false),
description: text("description") description: text("description")
}); });

View File

@@ -1,18 +1,16 @@
import {
pgTable,
serial,
varchar,
boolean,
integer,
bigint,
real,
text,
index,
uniqueIndex
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { alias } from "yargs"; import { InferSelectModel } from "drizzle-orm";
import {
bigint,
boolean,
index,
integer,
pgTable,
real,
serial,
text,
varchar
} from "drizzle-orm/pg-core";
export const domains = pgTable("domains", { export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(), domainId: varchar("domainId").primaryKey(),
@@ -55,7 +53,11 @@ export const orgs = pgTable("orgs", {
.default(0), .default(0),
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull() .notNull()
.default(0) .default(0),
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: boolean("isBillingOrg"),
billingOrgId: varchar("billingOrgId")
}); });
export const orgDomains = pgTable("orgDomains", { export const orgDomains = pgTable("orgDomains", {
@@ -142,7 +144,8 @@ export const resources = pgTable("resources", {
}).default("forced"), // "forced" = always show, "automatic" = only when down }).default("forced"), // "forced" = always show, "automatic" = only when down
maintenanceTitle: text("maintenanceTitle"), maintenanceTitle: text("maintenanceTitle"),
maintenanceMessage: text("maintenanceMessage"), maintenanceMessage: text("maintenanceMessage"),
maintenanceEstimatedTime: text("maintenanceEstimatedTime") maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
postAuthPath: text("postAuthPath")
}); });
export const targets = pgTable("targets", { export const targets = pgTable("targets", {
@@ -187,7 +190,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
hcFollowRedirects: boolean("hcFollowRedirects").default(true), hcFollowRedirects: boolean("hcFollowRedirects").default(true),
hcMethod: varchar("hcMethod").default("GET"), hcMethod: varchar("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" hcHealth: text("hcHealth")
.$type<"unknown" | "healthy" | "unhealthy">()
.default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName") hcTlsServerName: text("hcTlsServerName")
}); });
@@ -217,7 +222,7 @@ export const siteResources = pgTable("siteResources", {
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: varchar("niceId").notNull(), niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(), name: varchar("name").notNull(),
mode: varchar("mode").notNull(), // "host" | "cidr" | "port" mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
protocol: varchar("protocol"), // only for port mode protocol: varchar("protocol"), // only for port mode
proxyPort: integer("proxyPort"), // only for port mode proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode
@@ -327,7 +332,8 @@ export const userOrgs = pgTable("userOrgs", {
.notNull() .notNull()
.references(() => roles.roleId), .references(() => roles.roleId),
isOwner: boolean("isOwner").notNull().default(false), isOwner: boolean("isOwner").notNull().default(false),
autoProvisioned: boolean("autoProvisioned").default(false) autoProvisioned: boolean("autoProvisioned").default(false),
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
}); });
export const emailVerificationCodes = pgTable("emailVerificationCodes", { export const emailVerificationCodes = pgTable("emailVerificationCodes", {
@@ -983,6 +989,16 @@ export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
}) })
}); });
export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
messageId: serial("messageId").primaryKey(),
wsClientId: varchar("clientId"),
messageType: varchar("messageType"),
sentAt: bigint("sentAt", { mode: "number" }).notNull(),
receivedAt: bigint("receivedAt", { mode: "number" }),
error: text("error"),
complete: boolean("complete").notNull().default(false)
});
export type Org = InferSelectModel<typeof orgs>; export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>; export type Site = InferSelectModel<typeof sites>;
@@ -1043,3 +1059,4 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>; export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>; export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>; export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
export type RoundTripMessageTracker = InferSelectModel<typeof roundTripMessageTracker>;

View File

@@ -1,3 +1,4 @@
export * from "./driver"; export * from "./driver";
export * from "./schema/schema"; export * from "./schema/schema";
export * from "./schema/privateSchema"; export * from "./schema/privateSchema";
export * from "./migrate";

View File

@@ -4,7 +4,7 @@ import path from "path";
const migrationsFolder = path.join("server/migrations"); const migrationsFolder = path.join("server/migrations");
const runMigrations = async () => { export const runMigrations = async () => {
console.log("Running migrations..."); console.log("Running migrations...");
try { try {
migrate(db as any, { migrate(db as any, {
@@ -16,5 +16,3 @@ const runMigrations = async () => {
process.exit(1); process.exit(1);
} }
}; };
runMigrations();

View File

@@ -70,13 +70,16 @@ export const subscriptions = sqliteTable("subscriptions", {
canceledAt: integer("canceledAt"), canceledAt: integer("canceledAt"),
createdAt: integer("createdAt").notNull(), createdAt: integer("createdAt").notNull(),
updatedAt: integer("updatedAt"), updatedAt: integer("updatedAt"),
billingCycleAnchor: integer("billingCycleAnchor") version: integer("version"),
billingCycleAnchor: integer("billingCycleAnchor"),
type: text("type") // tier1, tier2, tier3, or license
}); });
export const subscriptionItems = sqliteTable("subscriptionItems", { export const subscriptionItems = sqliteTable("subscriptionItems", {
subscriptionItemId: integer("subscriptionItemId").primaryKey({ subscriptionItemId: integer("subscriptionItemId").primaryKey({
autoIncrement: true autoIncrement: true
}), }),
stripeSubscriptionItemId: text("stripeSubscriptionItemId"),
subscriptionId: text("subscriptionId") subscriptionId: text("subscriptionId")
.notNull() .notNull()
.references(() => subscriptions.subscriptionId, { .references(() => subscriptions.subscriptionId, {
@@ -84,6 +87,7 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
}), }),
planId: text("planId").notNull(), planId: text("planId").notNull(),
priceId: text("priceId"), priceId: text("priceId"),
featureId: text("featureId"),
meterId: text("meterId"), meterId: text("meterId"),
unitAmount: real("unitAmount"), unitAmount: real("unitAmount"),
tiers: text("tiers"), tiers: text("tiers"),
@@ -126,6 +130,7 @@ export const limits = sqliteTable("limits", {
}) })
.notNull(), .notNull(),
value: real("value"), value: real("value"),
override: integer("override", { mode: "boolean" }).default(false),
description: text("description") description: text("description")
}); });

View File

@@ -1,13 +1,6 @@
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { InferSelectModel } from "drizzle-orm"; import { InferSelectModel } from "drizzle-orm";
import { import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
sqliteTable,
text,
integer,
index,
uniqueIndex
} from "drizzle-orm/sqlite-core";
import { no } from "zod/v4/locales";
export const domains = sqliteTable("domains", { export const domains = sqliteTable("domains", {
domainId: text("domainId").primaryKey(), domainId: text("domainId").primaryKey(),
@@ -52,7 +45,11 @@ export const orgs = sqliteTable("orgs", {
.default(0), .default(0),
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull() .notNull()
.default(0) .default(0),
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
billingOrgId: text("billingOrgId")
}); });
export const userDomains = sqliteTable("userDomains", { export const userDomains = sqliteTable("userDomains", {
@@ -162,7 +159,8 @@ export const resources = sqliteTable("resources", {
}).default("forced"), // "forced" = always show, "automatic" = only when down }).default("forced"), // "forced" = always show, "automatic" = only when down
maintenanceTitle: text("maintenanceTitle"), maintenanceTitle: text("maintenanceTitle"),
maintenanceMessage: text("maintenanceMessage"), maintenanceMessage: text("maintenanceMessage"),
maintenanceEstimatedTime: text("maintenanceEstimatedTime") maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
postAuthPath: text("postAuthPath")
}); });
export const targets = sqliteTable("targets", { export const targets = sqliteTable("targets", {
@@ -213,7 +211,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
}).default(true), }).default(true),
hcMethod: text("hcMethod").default("GET"), hcMethod: text("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" hcHealth: text("hcHealth")
.$type<"unknown" | "healthy" | "unhealthy">()
.default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName") hcTlsServerName: text("hcTlsServerName")
}); });
@@ -245,7 +245,7 @@ export const siteResources = sqliteTable("siteResources", {
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: text("niceId").notNull(), niceId: text("niceId").notNull(),
name: text("name").notNull(), name: text("name").notNull(),
mode: text("mode").notNull(), // "host" | "cidr" | "port" mode: text("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
protocol: text("protocol"), // only for port mode protocol: text("protocol"), // only for port mode
proxyPort: integer("proxyPort"), // only for port mode proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode
@@ -637,7 +637,8 @@ export const userOrgs = sqliteTable("userOrgs", {
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false), isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
autoProvisioned: integer("autoProvisioned", { autoProvisioned: integer("autoProvisioned", {
mode: "boolean" mode: "boolean"
}).default(false) }).default(false),
pamUsername: text("pamUsername") // cleaned username for ssh and such
}); });
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
@@ -1079,6 +1080,16 @@ export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
}) })
}); });
export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
messageId: integer("messageId").primaryKey({ autoIncrement: true }),
wsClientId: text("clientId"),
messageType: text("messageType"),
sentAt: integer("sentAt").notNull(),
receivedAt: integer("receivedAt"),
error: text("error"),
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
});
export type Org = InferSelectModel<typeof orgs>; export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>; export type Site = InferSelectModel<typeof sites>;
@@ -1140,3 +1151,6 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>; export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>; export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>; export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
export type RoundTripMessageTracker = InferSelectModel<
typeof roundTripMessageTracker
>;

View File

@@ -105,11 +105,13 @@ function getOpenApiDocumentation() {
servers: [{ url: "/v1" }] servers: [{ url: "/v1" }]
}); });
if (!process.env.DISABLE_GEN_OPENAPI) {
// convert to yaml and save to file // convert to yaml and save to file
const outputPath = path.join(APP_PATH, "openapi.yaml"); const outputPath = path.join(APP_PATH, "openapi.yaml");
const yamlOutput = yaml.dump(generated); const yamlOutput = yaml.dump(generated);
fs.writeFileSync(outputPath, yamlOutput, "utf8"); fs.writeFileSync(outputPath, yamlOutput, "utf8");
logger.info(`OpenAPI documentation saved to ${outputPath}`); logger.info(`OpenAPI documentation saved to ${outputPath}`);
}
return generated; return generated;
} }

View File

@@ -1,30 +1,44 @@
import Stripe from "stripe";
export enum FeatureId { export enum FeatureId {
SITE_UPTIME = "siteUptime",
USERS = "users", USERS = "users",
SITES = "sites",
EGRESS_DATA_MB = "egressDataMb", EGRESS_DATA_MB = "egressDataMb",
DOMAINS = "domains", DOMAINS = "domains",
REMOTE_EXIT_NODES = "remoteExitNodes" REMOTE_EXIT_NODES = "remoteExitNodes",
ORGINIZATIONS = "organizations",
TIER1 = "tier1"
} }
export const FeatureMeterIds: Record<FeatureId, string> = { export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> {
[FeatureId.SITE_UPTIME]: "mtr_61Srrej5wUJuiTWgo41D3Ee2Ir7WmDLU", switch (featureId) {
[FeatureId.USERS]: "mtr_61SrreISyIWpwUNGR41D3Ee2Ir7WmQro", case FeatureId.USERS:
[FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW", return "Users";
[FeatureId.DOMAINS]: "mtr_61Ss9nIKDNMw0LDRU41D3Ee2Ir7WmRPU", case FeatureId.SITES:
[FeatureId.REMOTE_EXIT_NODES]: "mtr_61T86UXnfxTVXy9sD41D3Ee2Ir7WmFTE" return "Sites";
case FeatureId.EGRESS_DATA_MB:
return "Egress Data (MB)";
case FeatureId.DOMAINS:
return "Domains";
case FeatureId.REMOTE_EXIT_NODES:
return "Remote Exit Nodes";
case FeatureId.ORGINIZATIONS:
return "Organizations";
case FeatureId.TIER1:
return "Home Lab";
default:
return featureId;
}
}
// this is from the old system
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
}; };
export const FeatureMeterIdsSandbox: Record<FeatureId, string> = { export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = {
[FeatureId.SITE_UPTIME]: "mtr_test_61Snh3cees4w60gv841DCpkOb237BDEu", // [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
[FeatureId.USERS]: "mtr_test_61Sn5fLtq1gSfRkyA41DCpkOb237B6au",
[FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ",
[FeatureId.DOMAINS]: "mtr_test_61SsA8qrdAlgPpFRQ41DCpkOb237BGts",
[FeatureId.REMOTE_EXIT_NODES]: "mtr_test_61T86Vqmwa3D9ra3341DCpkOb237B94K"
}; };
export function getFeatureMeterId(featureId: FeatureId): string { export function getFeatureMeterId(featureId: FeatureId): string | undefined {
if ( if (
process.env.ENVIRONMENT == "prod" && process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true" process.env.SANDBOX_MODE !== "true"
@@ -43,45 +57,81 @@ export function getFeatureIdByMetricId(
)?.[0]; )?.[0];
} }
export type FeaturePriceSet = { export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
[key in Exclude<FeatureId, FeatureId.DOMAINS>]: string;
} & { export const tier1FeaturePriceSet: FeaturePriceSet = {
[FeatureId.DOMAINS]?: string; // Optional since domains are not billed [FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
}; };
export const standardFeaturePriceSet: FeaturePriceSet = { export const tier1FeaturePriceSetSandbox: FeaturePriceSet = {
// Free tier matches the freeLimitSet [FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
[FeatureId.SITE_UPTIME]: "price_1RrQc4D3Ee2Ir7WmaJGZ3MtF",
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea",
[FeatureId.EGRESS_DATA_MB]: "price_1RrQXFD3Ee2Ir7WmvGDlgxQk",
// [FeatureId.DOMAINS]: "price_1Rz3tMD3Ee2Ir7Wm5qLeASzC",
[FeatureId.REMOTE_EXIT_NODES]: "price_1S46weD3Ee2Ir7Wm94KEHI4h"
}; };
export const standardFeaturePriceSetSandbox: FeaturePriceSet = { export function getTier1FeaturePriceSet(): FeaturePriceSet {
// Free tier matches the freeLimitSet
[FeatureId.SITE_UPTIME]: "price_1RefFBDCpkOb237BPrKZ8IEU",
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF",
[FeatureId.EGRESS_DATA_MB]: "price_1Rfp9LDCpkOb237BwuN5Oiu0",
// [FeatureId.DOMAINS]: "price_1Ryi88DCpkOb237B2D6DM80b",
[FeatureId.REMOTE_EXIT_NODES]: "price_1RyiZvDCpkOb237BXpmoIYJL"
};
export function getStandardFeaturePriceSet(): FeaturePriceSet {
if ( if (
process.env.ENVIRONMENT == "prod" && process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true" process.env.SANDBOX_MODE !== "true"
) { ) {
return standardFeaturePriceSet; return tier1FeaturePriceSet;
} else { } else {
return standardFeaturePriceSetSandbox; return tier1FeaturePriceSetSandbox;
} }
} }
export function getLineItems( export const tier2FeaturePriceSet: FeaturePriceSet = {
featurePriceSet: FeaturePriceSet [FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
): Stripe.Checkout.SessionCreateParams.LineItem[] { };
return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({
price: priceId export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
})); [FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
};
export function getTier2FeaturePriceSet(): FeaturePriceSet {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
) {
return tier2FeaturePriceSet;
} else {
return tier2FeaturePriceSetSandbox;
}
}
export const tier3FeaturePriceSet: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
};
export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
};
export function getTier3FeaturePriceSet(): FeaturePriceSet {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
) {
return tier3FeaturePriceSet;
} else {
return tier3FeaturePriceSetSandbox;
}
}
export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
// Check all feature price sets
const allPriceSets = [
getTier1FeaturePriceSet(),
getTier2FeaturePriceSet(),
getTier3FeaturePriceSet()
];
for (const priceSet of allPriceSets) {
const entry = (Object.entries(priceSet) as [FeatureId, string][]).find(
([_, price]) => price === priceId
);
if (entry) {
return entry[0];
}
}
return undefined;
} }

View File

@@ -0,0 +1,25 @@
import Stripe from "stripe";
import { FeatureId, FeaturePriceSet } from "./features";
import { usageService } from "./usageService";
export async function getLineItems(
featurePriceSet: FeaturePriceSet,
orgId: string,
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
const users = await usageService.getUsage(orgId, FeatureId.USERS);
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
let quantity: number | undefined;
if (featureId === FeatureId.USERS) {
quantity = users?.instantaneousValue || 1;
} else if (featureId === FeatureId.TIER1) {
quantity = 1;
}
return {
price: priceId,
quantity: quantity
};
});
}

View File

@@ -1,50 +1,70 @@
import { FeatureId } from "./features"; import { FeatureId } from "./features";
export type LimitSet = { export type LimitSet = Partial<{
[key in FeatureId]: { [key in FeatureId]: {
value: number | null; // null indicates no limit value: number | null; // null indicates no limit
description?: string; description?: string;
}; };
}; }>;
export const sandboxLimitSet: LimitSet = {
[FeatureId.SITE_UPTIME]: { value: 2880, description: "Sandbox limit" }, // 1 site up for 2 days
[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" }
};
export const freeLimitSet: LimitSet = { export const freeLimitSet: LimitSet = {
[FeatureId.SITE_UPTIME]: { value: 46080, description: "Free tier limit" }, // 1 site up for 32 days [FeatureId.SITES]: { value: 5, description: "Basic limit" },
[FeatureId.USERS]: { value: 3, description: "Free tier limit" }, [FeatureId.USERS]: { value: 5, description: "Basic limit" },
[FeatureId.EGRESS_DATA_MB]: { [FeatureId.DOMAINS]: { value: 5, description: "Basic limit" },
value: 25000, [FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" },
description: "Free tier limit" [FeatureId.ORGINIZATIONS]: { value: 1, description: "Basic limit" },
}, // 25 GB
[FeatureId.DOMAINS]: { value: 3, description: "Free tier limit" },
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Free tier limit" }
}; };
export const subscribedLimitSet: LimitSet = { export const tier1LimitSet: LimitSet = {
[FeatureId.SITE_UPTIME]: { [FeatureId.USERS]: { value: 7, description: "Home limit" },
value: 2232000, [FeatureId.SITES]: { value: 10, description: "Home limit" },
description: "Contact us to increase soft limit." [FeatureId.DOMAINS]: { value: 10, description: "Home limit" },
}, // 50 sites up for 31 days [FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" },
[FeatureId.ORGINIZATIONS]: { value: 1, description: "Home limit" },
};
export const tier2LimitSet: LimitSet = {
[FeatureId.USERS]: { [FeatureId.USERS]: {
value: 150, value: 100,
description: "Contact us to increase soft limit." description: "Team limit"
},
[FeatureId.SITES]: {
value: 50,
description: "Team limit"
}, },
[FeatureId.EGRESS_DATA_MB]: {
value: 12000000,
description: "Contact us to increase soft limit."
}, // 12000 GB
[FeatureId.DOMAINS]: { [FeatureId.DOMAINS]: {
value: 250, value: 50,
description: "Contact us to increase soft limit." description: "Team limit"
}, },
[FeatureId.REMOTE_EXIT_NODES]: { [FeatureId.REMOTE_EXIT_NODES]: {
value: 5, value: 3,
description: "Contact us to increase soft limit." description: "Team limit"
},
[FeatureId.ORGINIZATIONS]: {
value: 1,
description: "Team limit"
} }
}; };
export const tier3LimitSet: LimitSet = {
[FeatureId.USERS]: {
value: 500,
description: "Business limit"
},
[FeatureId.SITES]: {
value: 250,
description: "Business limit"
},
[FeatureId.DOMAINS]: {
value: 100,
description: "Business limit"
},
[FeatureId.REMOTE_EXIT_NODES]: {
value: 20,
description: "Business limit"
},
[FeatureId.ORGINIZATIONS]: {
value: 5,
description: "Business limit"
},
};

View File

@@ -2,6 +2,7 @@ import { db, limits } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { LimitSet } from "./limitSet"; import { LimitSet } from "./limitSet";
import { FeatureId } from "./features"; import { FeatureId } from "./features";
import logger from "@server/logger";
class LimitService { class LimitService {
async applyLimitSetToOrg(orgId: string, limitSet: LimitSet): Promise<void> { async applyLimitSetToOrg(orgId: string, limitSet: LimitSet): Promise<void> {
@@ -13,6 +14,21 @@ class LimitService {
for (const [featureId, entry] of limitEntries) { for (const [featureId, entry] of limitEntries) {
const limitId = `${orgId}-${featureId}`; const limitId = `${orgId}-${featureId}`;
const { value, description } = entry; const { value, description } = entry;
// get the limit first
const [limit] = await trx
.select()
.from(limits)
.where(eq(limits.limitId, limitId))
.limit(1);
// check if its overriden
if (limit && limit.override) {
logger.debug(
`Skipping limit ${limitId} for org ${orgId} since it is overridden...`
);
continue;
}
await trx await trx
.insert(limits) .insert(limits)
.values({ limitId, orgId, featureId, value, description }); .values({ limitId, orgId, featureId, value, description });

View File

@@ -0,0 +1,52 @@
import { Tier } from "@server/types/Tiers";
export enum TierFeature {
OrgOidc = "orgOidc",
LoginPageDomain = "loginPageDomain", // handle downgrade by removing custom domain
DeviceApprovals = "deviceApprovals", // handle downgrade by disabling device approvals
LoginPageBranding = "loginPageBranding", // handle downgrade by setting to default branding
LogExport = "logExport",
AccessLogs = "accessLogs", // set the retention period to none on downgrade
ActionLogs = "actionLogs", // set the retention period to none on downgrade
RotateCredentials = "rotateCredentials",
MaintencePage = "maintencePage", // handle downgrade
DevicePosture = "devicePosture",
TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
SshPam = "sshPam"
}
export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
[TierFeature.LoginPageBranding]: ["tier1", "tier3", "enterprise"],
[TierFeature.LogExport]: ["tier3", "enterprise"],
[TierFeature.AccessLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
[TierFeature.TwoFactorEnforcement]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.SessionDurationPolicies]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.PasswordExpirationPolicies]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
[TierFeature.SshPam]: ["enterprise"]
};

View File

@@ -1,34 +0,0 @@
export enum TierId {
STANDARD = "standard"
}
export type TierPriceSet = {
[key in TierId]: string;
};
export const tierPriceSet: TierPriceSet = {
// Free tier matches the freeLimitSet
[TierId.STANDARD]: "price_1RrQ9cD3Ee2Ir7Wmqdy3KBa0"
};
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"
};
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

@@ -1,74 +1,32 @@
import { eq, sql, and } from "drizzle-orm"; import { eq, sql, and } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import * as fs from "fs/promises";
import * as path from "path";
import { import {
db, db,
usage, usage,
customers, customers,
sites,
newts,
limits, limits,
Usage, Usage,
Limit, Limit,
Transaction Transaction,
orgs
} from "@server/db"; } from "@server/db";
import { FeatureId, getFeatureMeterId } from "./features"; import { FeatureId, getFeatureMeterId } from "./features";
import logger from "@server/logger"; import logger from "@server/logger";
import { sendToClient } from "#dynamic/routers/ws";
import { build } from "@server/build"; 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;
timestamp: number;
event_name: string;
payload: {
value: number;
stripe_customer_id: string;
};
}
export function noop() { export function noop() {
if ( if (build !== "saas") {
build !== "saas" ||
!process.env.S3_BUCKET ||
!process.env.LOCAL_FILE_PATH
) {
return true; return true;
} }
return false; return false;
} }
export class UsageService { export class UsageService {
private bucketName: string | undefined;
private currentEventFile: string | null = null;
private currentFileStartTime: number = 0;
private eventsDir: string | undefined;
private uploadingFiles: Set<string> = new Set();
constructor() { constructor() {
if (noop()) { if (noop()) {
return; return;
} }
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
// this.eventsDir = privateConfig.getRawPrivateConfig().stripe?.localFilePath;
this.bucketName = process.env.S3_BUCKET || undefined;
this.eventsDir = process.env.LOCAL_FILE_PATH || undefined;
// Ensure events directory exists
this.initializeEventsDirectory().then(() => {
this.uploadPendingEventFilesOnStartup();
});
// Periodically check for old event files to upload
setInterval(() => {
this.uploadOldEventFiles().catch((err) => {
logger.error("Error in periodic event file upload:", err);
});
}, 30000); // every 30 seconds
} }
/** /**
@@ -78,85 +36,6 @@ export class UsageService {
return Math.round(value * 100000000000) / 100000000000; // 11 decimal places return Math.round(value * 100000000000) / 100000000000; // 11 decimal places
} }
private async initializeEventsDirectory(): Promise<void> {
if (!this.eventsDir) {
logger.warn(
"Stripe local file path is not configured, skipping events directory initialization."
);
return;
}
try {
await fs.mkdir(this.eventsDir, { recursive: true });
} catch (error) {
logger.error("Failed to create events directory:", error);
}
}
private async uploadPendingEventFilesOnStartup(): Promise<void> {
if (!this.eventsDir || !this.bucketName) {
logger.warn(
"Stripe local file path or bucket name is not configured, skipping leftover event file upload."
);
return;
}
try {
const files = await fs.readdir(this.eventsDir);
for (const file of files) {
if (file.endsWith(".json")) {
const filePath = path.join(this.eventsDir, file);
try {
const fileContent = await fs.readFile(
filePath,
"utf-8"
);
const events = JSON.parse(fileContent);
if (Array.isArray(events) && events.length > 0) {
// Upload to S3
const uploadCommand = new PutObjectCommand({
Bucket: this.bucketName,
Key: file,
Body: fileContent,
ContentType: "application/json"
});
await s3Client.send(uploadCommand);
// Check if file still exists before unlinking
try {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
logger.debug(
`Startup file ${file} was already deleted`
);
}
logger.info(
`Uploaded leftover event file ${file} to S3 with ${events.length} events`
);
} else {
// Remove empty file
try {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
logger.debug(
`Empty startup file ${file} was already deleted`
);
}
}
} catch (err) {
logger.error(
`Error processing leftover event file ${file}:`,
err
);
}
}
}
} catch (error) {
logger.error("Failed to scan for leftover event files");
}
}
public async add( public async add(
orgId: string, orgId: string,
featureId: FeatureId, featureId: FeatureId,
@@ -167,6 +46,8 @@ export class UsageService {
return null; return null;
} }
let orgIdToUse = await this.getBillingOrg(orgId, transaction);
// Truncate value to 11 decimal places // Truncate value to 11 decimal places
value = this.truncateValue(value); value = this.truncateValue(value);
@@ -176,20 +57,10 @@ export class UsageService {
while (attempt <= maxRetries) { while (attempt <= maxRetries) {
try { try {
// Get subscription data for this org (with caching)
const customerId = await this.getCustomerId(orgId, featureId);
if (!customerId) {
logger.warn(
`No subscription data found for org ${orgId} and feature ${featureId}`
);
return null;
}
let usage; let usage;
if (transaction) { if (transaction) {
usage = await this.internalAddUsage( usage = await this.internalAddUsage(
orgId, orgIdToUse,
featureId, featureId,
value, value,
transaction transaction
@@ -197,7 +68,7 @@ export class UsageService {
} else { } else {
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
usage = await this.internalAddUsage( usage = await this.internalAddUsage(
orgId, orgIdToUse,
featureId, featureId,
value, value,
trx trx
@@ -205,9 +76,6 @@ export class UsageService {
}); });
} }
// Log event for Stripe
await this.logStripeEvent(featureId, value, customerId);
return usage || null; return usage || null;
} catch (error: any) { } catch (error: any) {
// Check if this is a deadlock error // Check if this is a deadlock error
@@ -224,7 +92,7 @@ export class UsageService {
const delay = baseDelay + jitter; const delay = baseDelay + jitter;
logger.warn( logger.warn(
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms` `Deadlock detected for ${orgIdToUse}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
); );
await new Promise((resolve) => setTimeout(resolve, delay)); await new Promise((resolve) => setTimeout(resolve, delay));
@@ -232,7 +100,7 @@ export class UsageService {
} }
logger.error( logger.error(
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`, `Failed to add usage for ${orgIdToUse}/${featureId} after ${attempt} attempts:`,
error error
); );
break; break;
@@ -243,7 +111,7 @@ export class UsageService {
} }
private async internalAddUsage( private async internalAddUsage(
orgId: string, orgId: string, // here the orgId is the billing org already resolved by getBillingOrg in updateCount
featureId: FeatureId, featureId: FeatureId,
value: number, value: number,
trx: Transaction trx: Transaction
@@ -262,17 +130,22 @@ export class UsageService {
featureId, featureId,
orgId, orgId,
meterId, meterId,
latestValue: value, instantaneousValue: value || 0,
latestValue: value || 0,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: usage.usageId, target: usage.usageId,
set: { set: {
latestValue: sql`${usage.latestValue} + ${value}` instantaneousValue: sql`COALESCE(${usage.instantaneousValue}, 0) + ${value}`
} }
}) })
.returning(); .returning();
logger.debug(
`Added usage for org ${orgId} feature ${featureId}: +${value}, new instantaneousValue: ${returnUsage.instantaneousValue}`
);
return returnUsage; return returnUsage;
} }
@@ -286,7 +159,7 @@ export class UsageService {
return new Date(date * 1000).toISOString().split("T")[0]; return new Date(date * 1000).toISOString().split("T")[0];
} }
async updateDaily( async updateCount(
orgId: string, orgId: string,
featureId: FeatureId, featureId: FeatureId,
value?: number, value?: number,
@@ -295,30 +168,20 @@ export class UsageService {
if (noop()) { if (noop()) {
return; return;
} }
try {
if (!customerId) {
customerId =
(await this.getCustomerId(orgId, featureId)) || undefined;
if (!customerId) {
logger.warn(
`No subscription data found for org ${orgId} and feature ${featureId}`
);
return;
}
}
let orgIdToUse = await this.getBillingOrg(orgId);
try {
// Truncate value to 11 decimal places if provided // Truncate value to 11 decimal places if provided
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
value = this.truncateValue(value); value = this.truncateValue(value);
} }
const today = this.getTodayDateString();
let currentUsage: Usage | null = null; let currentUsage: Usage | null = null;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// Get existing meter record // Get existing meter record
const usageId = `${orgId}-${featureId}`; const usageId = `${orgIdToUse}-${featureId}`;
// Get current usage record // Get current usage record
[currentUsage] = await trx [currentUsage] = await trx
.select() .select()
@@ -327,66 +190,34 @@ export class UsageService {
.limit(1); .limit(1);
if (currentUsage) { if (currentUsage) {
const lastUpdateDate = this.getDateString(
currentUsage.updatedAt
);
const currentRunningTotal = currentUsage.latestValue;
const lastDailyValue = currentUsage.instantaneousValue || 0;
if (value == undefined || value === null) {
value = currentUsage.instantaneousValue || 0;
}
if (lastUpdateDate === today) {
// Same day update: replace the daily value
// Remove old daily value from running total, add new value
const newRunningTotal = this.truncateValue(
currentRunningTotal - lastDailyValue + value
);
await trx await trx
.update(usage) .update(usage)
.set({ .set({
latestValue: newRunningTotal,
instantaneousValue: value, instantaneousValue: value,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
}) })
.where(eq(usage.usageId, usageId)); .where(eq(usage.usageId, usageId));
} else {
// New day: add to running total
const newRunningTotal = this.truncateValue(
currentRunningTotal + value
);
await trx
.update(usage)
.set({
latestValue: newRunningTotal,
instantaneousValue: value,
updatedAt: Math.floor(Date.now() / 1000)
})
.where(eq(usage.usageId, usageId));
}
} else { } else {
// First record for this meter // First record for this meter
const meterId = getFeatureMeterId(featureId); const meterId = getFeatureMeterId(featureId);
const truncatedValue = this.truncateValue(value || 0);
await trx.insert(usage).values({ await trx.insert(usage).values({
usageId, usageId,
featureId, featureId,
orgId, orgId: orgIdToUse,
meterId, meterId,
instantaneousValue: truncatedValue, instantaneousValue: value || 0,
latestValue: truncatedValue, latestValue: value || 0,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
}); });
} }
}); });
await this.logStripeEvent(featureId, value || 0, customerId); // if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
// await this.logStripeEvent(featureId, value || 0, customerId);
// }
} catch (error) { } catch (error) {
logger.error( logger.error(
`Failed to update daily usage for ${orgId}/${featureId}:`, `Failed to update count usage for ${orgIdToUse}/${featureId}:`,
error error
); );
} }
@@ -396,7 +227,9 @@ export class UsageService {
orgId: string, orgId: string,
featureId: FeatureId featureId: FeatureId
): Promise<string | null> { ): Promise<string | null> {
const cacheKey = `customer_${orgId}_${featureId}`; let orgIdToUse = await this.getBillingOrg(orgId);
const cacheKey = `customer_${orgIdToUse}_${featureId}`;
const cached = cache.get<string>(cacheKey); const cached = cache.get<string>(cacheKey);
if (cached) { if (cached) {
@@ -410,7 +243,7 @@ export class UsageService {
customerId: customers.customerId customerId: customers.customerId
}) })
.from(customers) .from(customers)
.where(eq(customers.orgId, orgId)) .where(eq(customers.orgId, orgIdToUse))
.limit(1); .limit(1);
if (!customer) { if (!customer) {
@@ -425,189 +258,13 @@ export class UsageService {
return customerId; return customerId;
} catch (error) { } catch (error) {
logger.error( logger.error(
`Failed to get subscription data for ${orgId}/${featureId}:`, `Failed to get subscription data for ${orgIdToUse}/${featureId}:`,
error error
); );
return null; return null;
} }
} }
private async logStripeEvent(
featureId: FeatureId,
value: number,
customerId: string
): Promise<void> {
// Truncate value to 11 decimal places before sending to Stripe
const truncatedValue = this.truncateValue(value);
const event: StripeEvent = {
identifier: uuidv4(),
timestamp: Math.floor(new Date().getTime() / 1000),
event_name: featureId,
payload: {
value: truncatedValue,
stripe_customer_id: customerId
}
};
await this.writeEventToFile(event);
await this.checkAndUploadFile();
}
private async writeEventToFile(event: StripeEvent): Promise<void> {
if (!this.eventsDir || !this.bucketName) {
logger.warn(
"Stripe local file path or bucket name is not configured, skipping event file write."
);
return;
}
if (!this.currentEventFile) {
this.currentEventFile = this.generateEventFileName();
this.currentFileStartTime = Date.now();
}
const filePath = path.join(this.eventsDir, this.currentEventFile);
try {
let events: StripeEvent[] = [];
// Try to read existing file
try {
const fileContent = await fs.readFile(filePath, "utf-8");
events = JSON.parse(fileContent);
} catch (error) {
// File doesn't exist or is empty, start with empty array
events = [];
}
// Add new event
events.push(event);
// Write back to file
await fs.writeFile(filePath, JSON.stringify(events, null, 2));
} catch (error) {
logger.error("Failed to write event to file:", error);
}
}
private async checkAndUploadFile(): Promise<void> {
if (!this.currentEventFile) {
return;
}
const now = Date.now();
const fileAge = now - this.currentFileStartTime;
// Check if file is at least 1 minute old
if (fileAge >= 60000) {
// 60 seconds
await this.uploadFileToS3();
}
}
private async uploadFileToS3(): Promise<void> {
if (!this.bucketName || !this.eventsDir) {
logger.warn(
"Stripe local file path or bucket name is not configured, skipping S3 upload."
);
return;
}
if (!this.currentEventFile) {
return;
}
const fileName = this.currentEventFile;
const filePath = path.join(this.eventsDir, fileName);
// Check if this file is already being uploaded
if (this.uploadingFiles.has(fileName)) {
logger.debug(
`File ${fileName} is already being uploaded, skipping`
);
return;
}
// Mark file as being uploaded
this.uploadingFiles.add(fileName);
try {
// Check if file exists before trying to read it
try {
await fs.access(filePath);
} catch (error) {
logger.debug(
`File ${fileName} does not exist, may have been already processed`
);
this.uploadingFiles.delete(fileName);
// Reset current file if it was this file
if (this.currentEventFile === fileName) {
this.currentEventFile = null;
this.currentFileStartTime = 0;
}
return;
}
// Check if file exists and has content
const fileContent = await fs.readFile(filePath, "utf-8");
const events = JSON.parse(fileContent);
if (events.length === 0) {
// No events to upload, just clean up
try {
await fs.unlink(filePath);
} catch (unlinkError) {
// File may have been already deleted
logger.debug(
`File ${fileName} was already deleted during cleanup`
);
}
this.currentEventFile = null;
this.uploadingFiles.delete(fileName);
return;
}
// Upload to S3
const uploadCommand = new PutObjectCommand({
Bucket: this.bucketName,
Key: fileName,
Body: fileContent,
ContentType: "application/json"
});
await s3Client.send(uploadCommand);
// Clean up local file - check if it still exists before unlinking
try {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
// File may have been already deleted by another process
logger.debug(
`File ${fileName} was already deleted during upload`
);
}
logger.info(
`Uploaded ${fileName} to S3 with ${events.length} events`
);
// Reset for next file
this.currentEventFile = null;
this.currentFileStartTime = 0;
} catch (error) {
logger.error(`Failed to upload ${fileName} to S3:`, error);
} finally {
// Always remove from uploading set
this.uploadingFiles.delete(fileName);
}
}
private generateEventFileName(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const uuid = uuidv4().substring(0, 8);
return `events-${timestamp}-${uuid}.json`;
}
public async getUsage( public async getUsage(
orgId: string, orgId: string,
featureId: FeatureId, featureId: FeatureId,
@@ -617,7 +274,9 @@ export class UsageService {
return null; return null;
} }
const usageId = `${orgId}-${featureId}`; let orgIdToUse = await this.getBillingOrg(orgId, trx);
const usageId = `${orgIdToUse}-${featureId}`;
try { try {
const [result] = await trx const [result] = await trx
@@ -629,7 +288,7 @@ export class UsageService {
if (!result) { if (!result) {
// Lets create one if it doesn't exist using upsert to handle race conditions // Lets create one if it doesn't exist using upsert to handle race conditions
logger.info( logger.info(
`Creating new usage record for ${orgId}/${featureId}` `Creating new usage record for ${orgIdToUse}/${featureId}`
); );
const meterId = getFeatureMeterId(featureId); const meterId = getFeatureMeterId(featureId);
@@ -639,7 +298,7 @@ export class UsageService {
.values({ .values({
usageId, usageId,
featureId, featureId,
orgId, orgId: orgIdToUse,
meterId, meterId,
latestValue: 0, latestValue: 0,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
@@ -661,7 +320,7 @@ export class UsageService {
} catch (insertError) { } catch (insertError) {
// Fallback: try to fetch existing record in case of any insert issues // Fallback: try to fetch existing record in case of any insert issues
logger.warn( logger.warn(
`Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`, `Insert failed for ${orgIdToUse}/${featureId}, attempting to fetch existing record:`,
insertError insertError
); );
const [existingUsage] = await trx const [existingUsage] = await trx
@@ -676,136 +335,45 @@ export class UsageService {
return result; return result;
} catch (error) { } catch (error) {
logger.error( logger.error(
`Failed to get usage for ${orgId}/${featureId}:`, `Failed to get usage for ${orgIdToUse}/${featureId}:`,
error error
); );
throw error; throw error;
} }
} }
public async getUsageDaily( public async getBillingOrg(
orgId: string, orgId: string,
featureId: FeatureId trx: Transaction | typeof db = db
): Promise<Usage | null> { ): Promise<string> {
if (noop()) { let orgIdToUse = orgId;
return null;
} // get the org
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated const [org] = await trx
return this.getUsage(orgId, featureId); .select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (!org) {
throw new Error(`Organization with ID ${orgId} not found`);
} }
public async forceUpload(): Promise<void> { if (!org.isBillingOrg) {
await this.uploadFileToS3(); if (org.billingOrgId) {
} orgIdToUse = org.billingOrgId;
/**
* Scan the events directory for files older than 1 minute and upload them if not empty.
*/
private async uploadOldEventFiles(): Promise<void> {
if (!this.eventsDir || !this.bucketName) {
logger.warn(
"Stripe local file path or bucket name is not configured, skipping old event file upload."
);
return;
}
try {
const files = await fs.readdir(this.eventsDir);
const now = Date.now();
for (const file of files) {
if (!file.endsWith(".json")) continue;
// Skip files that are already being uploaded
if (this.uploadingFiles.has(file)) {
logger.debug(
`Skipping file ${file} as it's already being uploaded`
);
continue;
}
const filePath = path.join(this.eventsDir, file);
try {
// Check if file still exists before processing
try {
await fs.access(filePath);
} catch (accessError) {
logger.debug(`File ${file} does not exist, skipping`);
continue;
}
const stat = await fs.stat(filePath);
const age = now - stat.mtimeMs;
if (age >= 90000) {
// 1.5 minutes - Mark as being uploaded
this.uploadingFiles.add(file);
try {
const fileContent = await fs.readFile(
filePath,
"utf-8"
);
const events = JSON.parse(fileContent);
if (Array.isArray(events) && events.length > 0) {
// Upload to S3
const uploadCommand = new PutObjectCommand({
Bucket: this.bucketName,
Key: file,
Body: fileContent,
ContentType: "application/json"
});
await s3Client.send(uploadCommand);
// Check if file still exists before unlinking
try {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
logger.debug(
`File ${file} was already deleted during interval upload`
);
}
logger.info(
`Interval: Uploaded event file ${file} to S3 with ${events.length} events`
);
// If this was the current event file, reset it
if (this.currentEventFile === file) {
this.currentEventFile = null;
this.currentFileStartTime = 0;
}
} else { } else {
// Remove empty file throw new Error(
try { `Organization ${orgId} is not a billing org and does not have a billingOrgId set`
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
logger.debug(
`Empty file ${file} was already deleted`
); );
} }
} }
} finally {
// Always remove from uploading set return orgIdToUse;
this.uploadingFiles.delete(file);
}
}
} catch (err) {
logger.error(
`Interval: Error processing event file ${file}:`,
err
);
// Remove from uploading set on error
this.uploadingFiles.delete(file);
}
}
} catch (err) {
logger.error("Interval: Failed to scan for event files:", err);
}
} }
public async checkLimitSet( public async checkLimitSet(
orgId: string, orgId: string,
kickSites = false,
featureId?: FeatureId, featureId?: FeatureId,
usage?: Usage, usage?: Usage,
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
@@ -813,6 +381,9 @@ export class UsageService {
if (noop()) { if (noop()) {
return false; return false;
} }
let orgIdToUse = await this.getBillingOrg(orgId, trx);
// This method should check the current usage against the limits set for the organization // This method should check the current usage against the limits set for the organization
// and kick out all of the sites on the org // and kick out all of the sites on the org
let hasExceededLimits = false; let hasExceededLimits = false;
@@ -826,7 +397,7 @@ export class UsageService {
.from(limits) .from(limits)
.where( .where(
and( and(
eq(limits.orgId, orgId), eq(limits.orgId, orgIdToUse),
eq(limits.featureId, featureId) eq(limits.featureId, featureId)
) )
); );
@@ -835,11 +406,11 @@ export class UsageService {
orgLimits = await trx orgLimits = await trx
.select() .select()
.from(limits) .from(limits)
.where(eq(limits.orgId, orgId)); .where(eq(limits.orgId, orgIdToUse));
} }
if (orgLimits.length === 0) { if (orgLimits.length === 0) {
logger.debug(`No limits set for org ${orgId}`); logger.debug(`No limits set for org ${orgIdToUse}`);
return false; return false;
} }
@@ -850,7 +421,7 @@ export class UsageService {
currentUsage = usage; currentUsage = usage;
} else { } else {
currentUsage = await this.getUsage( currentUsage = await this.getUsage(
orgId, orgIdToUse,
limit.featureId as FeatureId, limit.featureId as FeatureId,
trx trx
); );
@@ -861,10 +432,10 @@ export class UsageService {
currentUsage?.latestValue || currentUsage?.latestValue ||
0; 0;
logger.debug( logger.debug(
`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}` `Current usage for org ${orgIdToUse} on feature ${limit.featureId}: ${usageValue}`
); );
logger.debug( logger.debug(
`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}` `Limit for org ${orgIdToUse} on feature ${limit.featureId}: ${limit.value}`
); );
if ( if (
currentUsage && currentUsage &&
@@ -872,67 +443,15 @@ export class UsageService {
usageValue > limit.value usageValue > limit.value
) { ) {
logger.debug( logger.debug(
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` + `Org ${orgIdToUse} has exceeded limit for ${limit.featureId}: ` +
`${usageValue} > ${limit.value}` `${usageValue} > ${limit.value}`
); );
hasExceededLimits = true; hasExceededLimits = true;
break; // Exit early if any limit is exceeded break; // Exit early if any limit is exceeded
} }
} }
// If any limits are exceeded, disconnect all sites for this organization
if (hasExceededLimits && kickSites) {
logger.warn(
`Disconnecting all sites for org ${orgId} due to exceeded limits`
);
// Get all sites for this organization
const orgSites = await trx
.select()
.from(sites)
.where(eq(sites.orgId, orgId));
// Mark all sites as offline and send termination messages
const siteUpdates = orgSites.map((site) => site.siteId);
if (siteUpdates.length > 0) {
// Send termination messages to newt sites
for (const site of orgSites) {
if (site.type === "newt") {
const [newt] = await trx
.select()
.from(newts)
.where(eq(newts.siteId, site.siteId))
.limit(1);
if (newt) {
const payload = {
type: `newt/wg/terminate`,
data: {
reason: "Usage limits exceeded"
}
};
// Don't await to prevent blocking
await sendToClient(newt.newtId, payload).catch(
(error: any) => {
logger.error(
`Failed to send termination message to newt ${newt.newtId}:`,
error
);
}
);
}
}
}
logger.info(
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
);
}
}
} catch (error) { } catch (error) {
logger.error(`Error checking limits for org ${orgId}:`, error); logger.error(`Error checking limits for org ${orgIdToUse}:`, error);
} }
return hasExceededLimits; return hasExceededLimits;

View File

@@ -32,7 +32,7 @@ import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { build } from "@server/build"; import { tierMatrix } from "../billing/tierMatrix";
export type ProxyResourcesResults = { export type ProxyResourcesResults = {
proxyResource: Resource; proxyResource: Resource;
@@ -212,7 +212,7 @@ export async function updateProxyResources(
} else { } else {
// Update existing resource // Update existing resource
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage);
if (!isLicensed) { if (!isLicensed) {
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
} }
@@ -648,7 +648,7 @@ export async function updateProxyResources(
); );
} }
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage);
if (!isLicensed) { if (!isLicensed) {
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
} }

View File

@@ -20,6 +20,7 @@ import { sendTerminateClient } from "@server/routers/client/terminate";
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm"; import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations"; import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
import { OlmErrorCodes } from "@server/routers/olm/error"; import { OlmErrorCodes } from "@server/routers/olm/error";
import { tierMatrix } from "./billing/tierMatrix";
export async function calculateUserClientsForOrgs( export async function calculateUserClientsForOrgs(
userId: string, userId: string,
@@ -189,7 +190,8 @@ export async function calculateUserClientsForOrgs(
const niceId = await getUniqueClientName(orgId); const niceId = await getUniqueClientName(orgId);
const isOrgLicensed = await isLicensedOrSubscribed( const isOrgLicensed = await isLicensedOrSubscribed(
userOrg.orgId userOrg.orgId,
tierMatrix.deviceApprovals
); );
const requireApproval = const requireApproval =
build !== "oss" && build !== "oss" &&

View File

@@ -107,6 +107,11 @@ export class Config {
process.env.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path; process.env.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path;
} }
process.env.DISABLE_ENTERPRISE_FEATURES = parsedConfig.flags
?.disable_enterprise_features
? "true"
: "false";
this.rawConfig = parsedConfig; this.rawConfig = parsedConfig;
} }

View File

@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // This is a placeholder value replaced by the build process
export const APP_VERSION = "1.15.0"; export const APP_VERSION = "1.15.4";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -1,197 +0,0 @@
import { isValidCIDR } from "@server/lib/validators";
import { getNextAvailableOrgSubnet } from "@server/lib/ip";
import {
actions,
apiKeyOrg,
apiKeys,
db,
domains,
Org,
orgDomains,
orgs,
roleActions,
roles,
userOrgs
} from "@server/db";
import { eq } from "drizzle-orm";
import { defaultRoleAllowedActions } from "@server/routers/role";
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
import { createCustomer } from "#dynamic/lib/billing";
import { usageService } from "@server/lib/billing/usageService";
import config from "@server/lib/config";
export async function createUserAccountOrg(
userId: string,
userEmail: string
): Promise<{
success: boolean;
org?: {
orgId: string;
name: string;
subnet: string;
};
error?: string;
}> {
// const subnet = await getNextAvailableOrgSubnet();
const orgId = "org_" + userId;
const name = `${userEmail}'s Organization`;
// if (!isValidCIDR(subnet)) {
// return {
// success: false,
// error: "Invalid subnet format. Please provide a valid CIDR notation."
// };
// }
// // make sure the subnet is unique
// const subnetExists = await db
// .select()
// .from(orgs)
// .where(eq(orgs.subnet, subnet))
// .limit(1);
// if (subnetExists.length > 0) {
// return { success: false, error: `Subnet ${subnet} already exists` };
// }
// make sure the orgId is unique
const orgExists = await db
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (orgExists.length > 0) {
return {
success: false,
error: `Organization with ID ${orgId} already exists`
};
}
let error = "";
let org: Org | null = null;
await db.transaction(async (trx) => {
const allDomains = await trx
.select()
.from(domains)
.where(eq(domains.configManaged, true));
const utilitySubnet = config.getRawConfig().orgs.utility_subnet_group;
const newOrg = await trx
.insert(orgs)
.values({
orgId,
name,
// subnet
subnet: "100.90.128.0/24", // TODO: this should not be hardcoded - or can it be the same in all orgs?
utilitySubnet: utilitySubnet,
createdAt: new Date().toISOString()
})
.returning();
if (newOrg.length === 0) {
error = "Failed to create organization";
trx.rollback();
return;
}
org = newOrg[0];
// Create admin role within the same transaction
const [insertedRole] = await trx
.insert(roles)
.values({
orgId: newOrg[0].orgId,
isAdmin: true,
name: "Admin",
description: "Admin role with the most permissions"
})
.returning({ roleId: roles.roleId });
if (!insertedRole || !insertedRole.roleId) {
error = "Failed to create Admin role";
trx.rollback();
return;
}
const roleId = insertedRole.roleId;
// Get all actions and create role actions
const actionIds = await trx.select().from(actions).execute();
if (actionIds.length > 0) {
await trx.insert(roleActions).values(
actionIds.map((action) => ({
roleId,
actionId: action.actionId,
orgId: newOrg[0].orgId
}))
);
}
if (allDomains.length) {
await trx.insert(orgDomains).values(
allDomains.map((domain) => ({
orgId: newOrg[0].orgId,
domainId: domain.domainId
}))
);
}
await trx.insert(userOrgs).values({
userId,
orgId: newOrg[0].orgId,
roleId: roleId,
isOwner: true
});
const memberRole = await trx
.insert(roles)
.values({
name: "Member",
description: "Members can only view resources",
orgId
})
.returning();
await trx.insert(roleActions).values(
defaultRoleAllowedActions.map((action) => ({
roleId: memberRole[0].roleId,
actionId: action,
orgId
}))
);
});
await limitsService.applyLimitSetToOrg(orgId, sandboxLimitSet);
if (!org) {
return { success: false, error: "Failed to create org" };
}
if (error) {
return {
success: false,
error: `Failed to create org: ${error}`
};
}
// make sure we have the stripe customer
const customerId = await createCustomer(orgId, userEmail);
if (customerId) {
await usageService.updateDaily(orgId, FeatureId.USERS, 1, customerId); // Only 1 because we are crating the org
}
return {
org: {
orgId,
name,
// subnet
subnet: "100.90.128.0/24"
},
success: true
};
}

245
server/lib/deleteOrg.ts Normal file
View File

@@ -0,0 +1,245 @@
import {
clients,
clientSiteResourcesAssociationsCache,
clientSitesAssociationsCache,
db,
domains,
exitNodeOrgs,
exitNodes,
olms,
orgDomains,
orgs,
remoteExitNodes,
resources,
sites,
userOrgs
} from "@server/db";
import { newts, newtSessions } from "@server/db";
import { eq, and, inArray, sql, count, countDistinct } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { sendToClient } from "#dynamic/routers/ws";
import { deletePeer } from "@server/routers/gerbil/peers";
import { OlmErrorCodes } from "@server/routers/olm/error";
import { sendTerminateClient } from "@server/routers/client/terminate";
import { usageService } from "./billing/usageService";
import { FeatureId } from "./billing";
export type DeleteOrgByIdResult = {
deletedNewtIds: string[];
olmsToTerminate: string[];
};
/**
* Deletes one organization and its related data. Returns ids for termination
* messages; caller should call sendTerminationMessages with the result.
* Throws if org not found.
*/
export async function deleteOrgById(
orgId: string
): Promise<DeleteOrgByIdResult> {
const [org] = await db
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (!org) {
throw createHttpError(
HttpCode.NOT_FOUND,
`Organization with ID ${orgId} not found`
);
}
const orgSites = await db
.select()
.from(sites)
.where(eq(sites.orgId, orgId))
.limit(1);
const orgClients = await db
.select()
.from(clients)
.where(eq(clients.orgId, orgId));
const deletedNewtIds: string[] = [];
const olmsToTerminate: string[] = [];
let domainCount: number | null = null;
let siteCount: number | null = null;
let userCount: number | null = null;
let remoteExitNodeCount: number | null = null;
await db.transaction(async (trx) => {
for (const site of orgSites) {
if (site.pubKey) {
if (site.type == "wireguard") {
await deletePeer(site.exitNodeId!, site.pubKey);
} else if (site.type == "newt") {
const [deletedNewt] = await trx
.delete(newts)
.where(eq(newts.siteId, site.siteId))
.returning();
if (deletedNewt) {
deletedNewtIds.push(deletedNewt.newtId);
await trx
.delete(newtSessions)
.where(
eq(newtSessions.newtId, deletedNewt.newtId)
);
}
}
}
logger.info(`Deleting site ${site.siteId}`);
await trx.delete(sites).where(eq(sites.siteId, site.siteId));
}
for (const client of orgClients) {
const [olm] = await trx
.select()
.from(olms)
.where(eq(olms.clientId, client.clientId))
.limit(1);
if (olm) {
olmsToTerminate.push(olm.olmId);
}
logger.info(`Deleting client ${client.clientId}`);
await trx
.delete(clients)
.where(eq(clients.clientId, client.clientId));
await trx
.delete(clientSiteResourcesAssociationsCache)
.where(
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
)
);
await trx
.delete(clientSitesAssociationsCache)
.where(
eq(clientSitesAssociationsCache.clientId, client.clientId)
);
}
const allOrgDomains = await trx
.select()
.from(orgDomains)
.innerJoin(domains, eq(domains.domainId, orgDomains.domainId))
.where(
and(
eq(orgDomains.orgId, orgId),
eq(domains.configManaged, false)
)
);
const domainIdsToDelete: string[] = [];
for (const orgDomain of allOrgDomains) {
const domainId = orgDomain.domains.domainId;
const orgCount = await trx
.select({ count: sql<number>`count(*)` })
.from(orgDomains)
.where(eq(orgDomains.domainId, domainId));
if (orgCount[0].count === 1) {
domainIdsToDelete.push(domainId);
}
}
if (domainIdsToDelete.length > 0) {
await trx
.delete(domains)
.where(inArray(domains.domainId, domainIdsToDelete));
}
await trx.delete(resources).where(eq(resources.orgId, orgId));
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
await trx.delete(orgs).where(eq(orgs.orgId, orgId));
if (org.billingOrgId) {
const billingOrgs = await trx
.select()
.from(orgs)
.where(eq(orgs.billingOrgId, org.billingOrgId));
if (billingOrgs.length > 0) {
const billingOrgIds = billingOrgs.map((org) => org.orgId);
const [domainCountRes] = await trx
.select({ count: count() })
.from(orgDomains)
.where(inArray(orgDomains.orgId, billingOrgIds));
domainCount = domainCountRes.count;
const [siteCountRes] = await trx
.select({ count: count() })
.from(sites)
.where(inArray(sites.orgId, billingOrgIds));
siteCount = siteCountRes.count;
const [userCountRes] = await trx
.select({ count: countDistinct(userOrgs.userId) })
.from(userOrgs)
.where(inArray(userOrgs.orgId, billingOrgIds));
userCount = userCountRes.count;
const [remoteExitNodeCountRes] = await trx
.select({ count: countDistinct(exitNodeOrgs.exitNodeId) })
.from(exitNodeOrgs)
.where(inArray(exitNodeOrgs.orgId, billingOrgIds));
remoteExitNodeCount = remoteExitNodeCountRes.count;
}
}
});
if (org.billingOrgId) {
usageService.updateCount(
org.billingOrgId,
FeatureId.DOMAINS,
domainCount ?? 0
);
usageService.updateCount(
org.billingOrgId,
FeatureId.SITES,
siteCount ?? 0
);
usageService.updateCount(
org.billingOrgId,
FeatureId.USERS,
userCount ?? 0
);
usageService.updateCount(
org.billingOrgId,
FeatureId.REMOTE_EXIT_NODES,
remoteExitNodeCount ?? 0
);
}
return { deletedNewtIds, olmsToTerminate };
}
export function sendTerminationMessages(result: DeleteOrgByIdResult): void {
for (const newtId of result.deletedNewtIds) {
sendToClient(newtId, { type: `newt/wg/terminate`, data: {} }).catch(
(error) => {
logger.error(
"Failed to send termination message to newt:",
error
);
}
);
}
for (const olmId of result.olmsToTerminate) {
sendTerminateClient(
0,
OlmErrorCodes.TERMINATED_REKEYED,
olmId
).catch((error) => {
logger.error(
"Failed to send termination message to olm:",
error
);
});
}
}

View File

@@ -1,3 +1,8 @@
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> { import { Tier } from "@server/types/Tiers";
export async function isLicensedOrSubscribed(
orgId: string,
tiers: Tier[]
): Promise<boolean> {
return false; return false;
} }

View File

@@ -0,0 +1,8 @@
import { Tier } from "@server/types/Tiers";
export async function isSubscribed(
orgId: string,
tiers: Tier[]
): Promise<boolean> {
return false;
}

View File

@@ -0,0 +1,18 @@
/**
* Normalizes a post-authentication path for safe use when building redirect URLs.
* Returns a path that starts with / and does not allow open redirects (no //, no :).
*/
export function normalizePostAuthPath(path: string | null | undefined): string | null {
if (path == null || typeof path !== "string") {
return null;
}
const trimmed = path.trim();
if (trimmed === "") {
return null;
}
// Reject protocol-relative (//) or scheme (:) to avoid open redirect
if (trimmed.includes("//") || trimmed.includes(":")) {
return null;
}
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
}

View File

@@ -331,7 +331,8 @@ export const configSchema = z
disable_local_sites: z.boolean().optional(), disable_local_sites: z.boolean().optional(),
disable_basic_wireguard_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(),
disable_config_managed_domains: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(),
disable_product_help_banners: z.boolean().optional() disable_product_help_banners: z.boolean().optional(),
disable_enterprise_features: z.boolean().optional()
}) })
.optional(), .optional(),
dns: z dns: z

142
server/lib/userOrg.ts Normal file
View File

@@ -0,0 +1,142 @@
import {
db,
Org,
orgs,
resources,
siteResources,
sites,
Transaction,
UserOrg,
userOrgs,
userResources,
userSiteResources,
userSites
} from "@server/db";
import { eq, and, inArray, ne, exists } from "drizzle-orm";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
export async function assignUserToOrg(
org: Org,
values: typeof userOrgs.$inferInsert,
trx: Transaction | typeof db = db
) {
const [userOrg] = await trx.insert(userOrgs).values(values).returning();
// calculate if the user is in any other of the orgs before we count it as an add to the billing org
if (org.billingOrgId) {
const otherBillingOrgs = await trx
.select()
.from(orgs)
.where(
and(
eq(orgs.billingOrgId, org.billingOrgId),
ne(orgs.orgId, org.orgId)
)
);
const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
const orgsInBillingDomainThatTheUserIsStillIn = await trx
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userOrg.userId),
inArray(userOrgs.orgId, billingOrgIds)
)
);
if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) {
await usageService.add(org.orgId, FeatureId.USERS, 1, trx);
}
}
}
export async function removeUserFromOrg(
org: Org,
userId: string,
trx: Transaction | typeof db = db
) {
await trx
.delete(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId)));
await trx.delete(userResources).where(
and(
eq(userResources.userId, userId),
exists(
trx
.select()
.from(resources)
.where(
and(
eq(resources.resourceId, userResources.resourceId),
eq(resources.orgId, org.orgId)
)
)
)
)
);
await trx.delete(userSiteResources).where(
and(
eq(userSiteResources.userId, userId),
exists(
trx
.select()
.from(siteResources)
.where(
and(
eq(
siteResources.siteResourceId,
userSiteResources.siteResourceId
),
eq(siteResources.orgId, org.orgId)
)
)
)
)
);
await trx.delete(userSites).where(
and(
eq(userSites.userId, userId),
exists(
db
.select()
.from(sites)
.where(
and(
eq(sites.siteId, userSites.siteId),
eq(sites.orgId, org.orgId)
)
)
)
)
);
// calculate if the user is in any other of the orgs before we count it as an remove to the billing org
if (org.billingOrgId) {
const billingOrgs = await trx
.select()
.from(orgs)
.where(eq(orgs.billingOrgId, org.billingOrgId));
const billingOrgIds = billingOrgs.map((o) => o.orgId);
const orgsInBillingDomainThatTheUserIsStillIn = await trx
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
inArray(userOrgs.orgId, billingOrgIds)
)
);
if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) {
await usageService.add(org.orgId, FeatureId.USERS, -1, trx);
}
}
}

View File

@@ -29,3 +29,4 @@ export * from "./verifyUserIsOrgOwner";
export * from "./verifySiteResourceAccess"; export * from "./verifySiteResourceAccess";
export * from "./logActionAudit"; export * from "./logActionAudit";
export * from "./verifyOlmAccess"; export * from "./verifyOlmAccess";
export * from "./verifyLimits";

View File

@@ -4,7 +4,6 @@ import { apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
export async function verifyApiKeyOrgAccess( export async function verifyApiKeyOrgAccess(
req: Request, req: Request,

View File

@@ -0,0 +1,43 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
export async function verifyLimits(
req: Request,
res: Response,
next: NextFunction
) {
if (build != "saas") {
return next();
}
const orgId = req.userOrgId || req.apiKeyOrg?.orgId || req.params.orgId;
if (!orgId) {
return next(); // its fine if we silently fail here because this is not critical to operation or security and its better user experience if we dont fail
}
try {
const reject = await usageService.checkLimitSet(orgId);
if (reject) {
return next(
createHttpError(
HttpCode.PAYMENT_REQUIRED,
"Organization has exceeded its usage limits. Please upgrade your plan or contact support."
)
);
}
return next();
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error checking limits"
)
);
}
}

View File

@@ -16,5 +16,6 @@ export enum OpenAPITags {
Client = "Client", Client = "Client",
ApiKey = "API Key", ApiKey = "API Key",
Domain = "Domain", Domain = "Domain",
Blueprint = "Blueprint" Blueprint = "Blueprint",
Ssh = "SSH"
} }

View File

@@ -11,46 +11,83 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { getTierPriceSet } from "@server/lib/billing/tiers";
import { getOrgSubscriptionsData } from "@server/private/routers/billing/getOrgSubscriptions";
import { build } from "@server/build"; import { build } from "@server/build";
import { db, customers, subscriptions, orgs } from "@server/db";
import logger from "@server/logger";
import { Tier } from "@server/types/Tiers";
import { eq, and, ne } from "drizzle-orm";
export async function getOrgTierData( export async function getOrgTierData(
orgId: string orgId: string
): Promise<{ tier: string | null; active: boolean }> { ): Promise<{ tier: Tier | null; active: boolean }> {
let tier = null; let tier: Tier | null = null;
let active = false; let active = false;
if (build !== "saas") { if (build !== "saas") {
return { tier, active }; return { tier, active };
} }
// TODO: THIS IS INEFFICIENT!!! WE SHOULD IMPROVE HOW WE STORE TIERS WITH SUBSCRIPTIONS AND RETRIEVE THEM try {
const [org] = await db
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (!org) {
return { tier, active };
}
let orgIdToUse = org.orgId;
if (!org.isBillingOrg) {
if (!org.billingOrgId) {
logger.warn(
`Org ${orgId} is not a billing org and does not have a billingOrgId`
);
return { tier, active };
}
orgIdToUse = org.billingOrgId;
}
// Get customer for org
const [customer] = await db
.select()
.from(customers)
.where(eq(customers.orgId, orgIdToUse))
.limit(1);
if (!customer) {
return { tier, active };
}
// Query for active subscriptions that are not license type
const [subscription] = await db
.select()
.from(subscriptions)
.where(
and(
eq(subscriptions.customerId, customer.customerId),
eq(subscriptions.status, "active"),
ne(subscriptions.type, "license")
)
)
.limit(1);
if (subscription) {
// Validate that subscription.type is one of the expected tier values
if (
subscription.type === "tier1" ||
subscription.type === "tier2" ||
subscription.type === "tier3"
) {
tier = subscription.type;
active = true;
}
}
} catch (error) {
// If org not found or error occurs, return null tier and inactive
// This is acceptable behavior as per the function signature
}
const subscriptionsWithItems = await getOrgSubscriptionsData(orgId);
for (const { subscription, items } of subscriptionsWithItems) {
if (items && items.length > 0) {
const tierPriceSet = getTierPriceSet();
// Iterate through tiers in order (earlier keys are higher tiers)
for (const [tierId, priceId] of Object.entries(tierPriceSet)) {
// Check if any subscription item matches this tier's price ID
const matchingItem = items.find((item) => item.priceId === priceId);
if (matchingItem) {
tier = tierId;
break;
}
}
}
if (subscription && subscription.status === "active") {
active = true;
}
// If we found a tier and active subscription, we can stop
if (tier && active) {
break;
}
}
return { tier, active }; return { tier, active };
} }

View File

@@ -13,8 +13,6 @@
import { build } from "@server/build"; import { build } from "@server/build";
import { db, Org, orgs, ResourceSession, sessions, users } from "@server/db"; import { db, Org, orgs, ResourceSession, sessions, users } from "@server/db";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import license from "#private/license/license"; import license from "#private/license/license";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
@@ -80,6 +78,8 @@ export async function checkOrgAccessPolicy(
} }
} }
// TODO: check that the org is subscribed
// get the needed data // get the needed data
if (!props.org) { if (!props.org) {

View File

@@ -65,6 +65,11 @@ export class PrivateConfig {
this.rawPrivateConfig.branding?.logo?.dark_path || undefined; this.rawPrivateConfig.branding?.logo?.dark_path || undefined;
} }
if (this.rawPrivateConfig.app.identity_provider_mode) {
process.env.IDENTITY_PROVIDER_MODE =
this.rawPrivateConfig.app.identity_provider_mode;
}
process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding
?.logo?.auth_page?.width ?.logo?.auth_page?.width
? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString() ? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString()
@@ -125,24 +130,10 @@ export class PrivateConfig {
this.rawPrivateConfig.server.reo_client_id; this.rawPrivateConfig.server.reo_client_id;
} }
if (this.rawPrivateConfig.stripe?.s3Bucket) {
process.env.S3_BUCKET = this.rawPrivateConfig.stripe.s3Bucket;
}
if (this.rawPrivateConfig.stripe?.localFilePath) {
process.env.LOCAL_FILE_PATH =
this.rawPrivateConfig.stripe.localFilePath;
}
if (this.rawPrivateConfig.stripe?.s3Region) {
process.env.S3_REGION = this.rawPrivateConfig.stripe.s3Region;
}
if (this.rawPrivateConfig.flags.use_pangolin_dns) { if (this.rawPrivateConfig.flags.use_pangolin_dns) {
process.env.USE_PANGOLIN_DNS = process.env.USE_PANGOLIN_DNS =
this.rawPrivateConfig.flags.use_pangolin_dns.toString(); this.rawPrivateConfig.flags.use_pangolin_dns.toString();
} }
if (this.rawPrivateConfig.flags.use_org_only_idp) {
process.env.USE_ORG_ONLY_IDP =
this.rawPrivateConfig.flags.use_org_only_idp.toString();
}
} }
public getRawPrivateConfig() { public getRawPrivateConfig() {

View File

@@ -13,17 +13,19 @@
import { build } from "@server/build"; import { build } from "@server/build";
import license from "#private/license/license"; import license from "#private/license/license";
import { getOrgTierData } from "#private/lib/billing"; import { isSubscribed } from "#private/lib/isSubscribed";
import { TierId } from "@server/lib/billing/tiers"; import { Tier } from "@server/types/Tiers";
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> { export async function isLicensedOrSubscribed(
orgId: string,
tiers: Tier[]
): Promise<boolean> {
if (build === "enterprise") { if (build === "enterprise") {
return await license.isUnlocked(); return await license.isUnlocked();
} }
if (build === "saas") { if (build === "saas") {
const { tier } = await getOrgTierData(orgId); return isSubscribed(orgId, tiers);
return tier === TierId.STANDARD;
} }
return false; return false;

View File

@@ -0,0 +1,29 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { Tier } from "@server/types/Tiers";
export async function isSubscribed(
orgId: string,
tiers: Tier[]
): Promise<boolean> {
if (build === "saas") {
const { tier, active } = await getOrgTierData(orgId);
const isTier = (tier && tiers.includes(tier)) || false;
return active && isTier;
}
return false;
}

View File

@@ -25,7 +25,8 @@ export const privateConfigSchema = z.object({
app: z app: z
.object({ .object({
region: z.string().optional().default("default"), region: z.string().optional().default("default"),
base_domain: z.string().optional() base_domain: z.string().optional(),
identity_provider_mode: z.enum(["global", "org"]).optional()
}) })
.optional() .optional()
.default({ .default({
@@ -95,7 +96,7 @@ export const privateConfigSchema = z.object({
.object({ .object({
enable_redis: z.boolean().optional().default(false), enable_redis: z.boolean().optional().default(false),
use_pangolin_dns: z.boolean().optional().default(false), use_pangolin_dns: z.boolean().optional().default(false),
use_org_only_idp: z.boolean().optional().default(false) use_org_only_idp: z.boolean().optional()
}) })
.optional() .optional()
.prefault({}), .prefault({}),
@@ -176,11 +177,33 @@ export const privateConfigSchema = z.object({
.string() .string()
.optional() .optional()
.transform(getEnvOrYaml("STRIPE_WEBHOOK_SECRET")), .transform(getEnvOrYaml("STRIPE_WEBHOOK_SECRET")),
s3Bucket: z.string(), // s3Bucket: z.string(),
s3Region: z.string().default("us-east-1"), // s3Region: z.string().default("us-east-1"),
localFilePath: z.string() // localFilePath: z.string().optional()
}) })
.optional() .optional()
})
.transform((data) => {
// this to maintain backwards compatibility with the old config file
const identityProviderMode = data.app?.identity_provider_mode;
const useOrgOnlyIdp = data.flags?.use_org_only_idp;
if (identityProviderMode !== undefined) {
return data;
}
if (useOrgOnlyIdp === true) {
return {
...data,
app: { ...data.app, identity_provider_mode: "org" as const }
};
}
if (useOrgOnlyIdp === false) {
return {
...data,
app: { ...data.app, identity_provider_mode: "global" as const }
};
}
return data;
}); });
export function readPrivateConfigFile() { export function readPrivateConfigFile() {

442
server/private/lib/sshCA.ts Normal file
View File

@@ -0,0 +1,442 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import * as crypto from "crypto";
/**
* SSH CA "Server" - Pure TypeScript Implementation
*
* This module provides basic SSH Certificate Authority functionality using
* only Node.js built-in crypto module. No external dependencies or subprocesses.
*
* Usage:
* 1. generateCA() - Creates a new CA key pair, returns CA info including the
* TrustedUserCAKeys line to add to servers
* 2. signPublicKey() - Signs a user's public key with the CA, returns a certificate
*/
// ============================================================================
// SSH Wire Format Helpers
// ============================================================================
/**
* Encode a string in SSH wire format (4-byte length prefix + data)
*/
function encodeString(data: Buffer | string): Buffer {
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
const len = Buffer.alloc(4);
len.writeUInt32BE(buf.length, 0);
return Buffer.concat([len, buf]);
}
/**
* Encode a uint32 in SSH wire format (big-endian)
*/
function encodeUInt32(value: number): Buffer {
const buf = Buffer.alloc(4);
buf.writeUInt32BE(value, 0);
return buf;
}
/**
* Encode a uint64 in SSH wire format (big-endian)
*/
function encodeUInt64(value: bigint): Buffer {
const buf = Buffer.alloc(8);
buf.writeBigUInt64BE(value, 0);
return buf;
}
/**
* Decode a string from SSH wire format at the given offset
* Returns the string buffer and the new offset
*/
function decodeString(data: Buffer, offset: number): { value: Buffer; newOffset: number } {
const len = data.readUInt32BE(offset);
const value = data.subarray(offset + 4, offset + 4 + len);
return { value, newOffset: offset + 4 + len };
}
// ============================================================================
// SSH Public Key Parsing/Encoding
// ============================================================================
/**
* Parse an OpenSSH public key line (e.g., "ssh-ed25519 AAAA... comment")
*/
function parseOpenSSHPublicKey(pubKeyLine: string): {
keyType: string;
keyData: Buffer;
comment: string;
} {
const parts = pubKeyLine.trim().split(/\s+/);
if (parts.length < 2) {
throw new Error("Invalid public key format");
}
const keyType = parts[0];
const keyData = Buffer.from(parts[1], "base64");
const comment = parts.slice(2).join(" ") || "";
// Verify the key type in the blob matches
const { value: blobKeyType } = decodeString(keyData, 0);
if (blobKeyType.toString("utf8") !== keyType) {
throw new Error(`Key type mismatch: ${blobKeyType.toString("utf8")} vs ${keyType}`);
}
return { keyType, keyData, comment };
}
/**
* Encode an Ed25519 public key in OpenSSH format
*/
function encodeEd25519PublicKey(publicKey: Buffer): Buffer {
return Buffer.concat([
encodeString("ssh-ed25519"),
encodeString(publicKey)
]);
}
/**
* Format a public key blob as an OpenSSH public key line
*/
function formatOpenSSHPublicKey(keyBlob: Buffer, comment: string = ""): string {
const { value: keyType } = decodeString(keyBlob, 0);
const base64 = keyBlob.toString("base64");
return `${keyType.toString("utf8")} ${base64}${comment ? " " + comment : ""}`;
}
// ============================================================================
// SSH Certificate Building
// ============================================================================
interface CertificateOptions {
/** Serial number for the certificate */
serial?: bigint;
/** Certificate type: 1 = user, 2 = host */
certType?: number;
/** Key ID (usually username or identifier) */
keyId: string;
/** List of valid principals (usernames the cert is valid for) */
validPrincipals: string[];
/** Valid after timestamp (seconds since epoch) */
validAfter?: bigint;
/** Valid before timestamp (seconds since epoch) */
validBefore?: bigint;
/** Critical options (usually empty for user certs) */
criticalOptions?: Map<string, string>;
/** Extensions to enable */
extensions?: string[];
}
/**
* Build the extensions section of the certificate
*/
function buildExtensions(extensions: string[]): Buffer {
// Extensions are a series of name-value pairs, sorted by name
// For boolean extensions, the value is empty
const sortedExtensions = [...extensions].sort();
const parts: Buffer[] = [];
for (const ext of sortedExtensions) {
parts.push(encodeString(ext));
parts.push(encodeString("")); // Empty value for boolean extensions
}
return encodeString(Buffer.concat(parts));
}
/**
* Build the critical options section
*/
function buildCriticalOptions(options: Map<string, string>): Buffer {
const sortedKeys = [...options.keys()].sort();
const parts: Buffer[] = [];
for (const key of sortedKeys) {
parts.push(encodeString(key));
parts.push(encodeString(encodeString(options.get(key)!)));
}
return encodeString(Buffer.concat(parts));
}
/**
* Build the valid principals section
*/
function buildPrincipals(principals: string[]): Buffer {
const parts: Buffer[] = [];
for (const principal of principals) {
parts.push(encodeString(principal));
}
return encodeString(Buffer.concat(parts));
}
/**
* Extract the raw Ed25519 public key from an OpenSSH public key blob
*/
function extractEd25519PublicKey(keyBlob: Buffer): Buffer {
const { newOffset } = decodeString(keyBlob, 0); // Skip key type
const { value: publicKey } = decodeString(keyBlob, newOffset);
return publicKey;
}
// ============================================================================
// CA Interface
// ============================================================================
export interface CAKeyPair {
/** CA private key in PEM format (keep this secret!) */
privateKeyPem: string;
/** CA public key in PEM format */
publicKeyPem: string;
/** CA public key in OpenSSH format (for TrustedUserCAKeys) */
publicKeyOpenSSH: string;
/** Raw CA public key bytes (Ed25519) */
publicKeyRaw: Buffer;
}
export interface SignedCertificate {
/** The certificate in OpenSSH format (save as id_ed25519-cert.pub or similar) */
certificate: string;
/** The certificate type string */
certType: string;
/** Serial number */
serial: bigint;
/** Key ID */
keyId: string;
/** Valid principals */
validPrincipals: string[];
/** Valid from timestamp */
validAfter: Date;
/** Valid until timestamp */
validBefore: Date;
}
// ============================================================================
// Main Functions
// ============================================================================
/**
* Generate a new SSH Certificate Authority key pair.
*
* Returns the CA keys and the line to add to /etc/ssh/sshd_config:
* TrustedUserCAKeys /etc/ssh/ca.pub
*
* Then save the publicKeyOpenSSH to /etc/ssh/ca.pub on the server.
*
* @param comment - Optional comment for the CA public key
* @returns CA key pair and configuration info
*/
export function generateCA(comment: string = "ssh-ca"): CAKeyPair {
// Generate Ed25519 key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" }
});
// Get raw public key bytes
const pubKeyObj = crypto.createPublicKey(publicKey);
const rawPubKey = pubKeyObj.export({ type: "spki", format: "der" });
// Ed25519 SPKI format: 12 byte header + 32 byte key
const ed25519PubKey = rawPubKey.subarray(rawPubKey.length - 32);
// Create OpenSSH format public key
const pubKeyBlob = encodeEd25519PublicKey(ed25519PubKey);
const publicKeyOpenSSH = formatOpenSSHPublicKey(pubKeyBlob, comment);
return {
privateKeyPem: privateKey,
publicKeyPem: publicKey,
publicKeyOpenSSH,
publicKeyRaw: ed25519PubKey
};
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Get and decrypt the SSH CA keys for an organization.
*
* @param orgId - Organization ID
* @param decryptionKey - Key to decrypt the CA private key (typically server.secret from config)
* @returns CA key pair or null if not found
*/
export async function getOrgCAKeys(
orgId: string,
decryptionKey: string
): Promise<CAKeyPair | null> {
const { db, orgs } = await import("@server/db");
const { eq } = await import("drizzle-orm");
const { decrypt } = await import("@server/lib/crypto");
const [org] = await db
.select({
sshCaPrivateKey: orgs.sshCaPrivateKey,
sshCaPublicKey: orgs.sshCaPublicKey
})
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (!org || !org.sshCaPrivateKey || !org.sshCaPublicKey) {
return null;
}
const privateKeyPem = decrypt(org.sshCaPrivateKey, decryptionKey);
// Extract raw public key from the OpenSSH format
const { keyData } = parseOpenSSHPublicKey(org.sshCaPublicKey);
const { newOffset } = decodeString(keyData, 0); // Skip key type
const { value: publicKeyRaw } = decodeString(keyData, newOffset);
// Get PEM format of public key
const pubKeyObj = crypto.createPublicKey({
key: privateKeyPem,
format: "pem"
});
const publicKeyPem = pubKeyObj.export({ type: "spki", format: "pem" }) as string;
return {
privateKeyPem,
publicKeyPem,
publicKeyOpenSSH: org.sshCaPublicKey,
publicKeyRaw
};
}
/**
* Sign a user's SSH public key with the CA, producing a certificate.
*
* The resulting certificate should be saved alongside the user's private key
* with a -cert.pub suffix. For example:
* - Private key: ~/.ssh/id_ed25519
* - Certificate: ~/.ssh/id_ed25519-cert.pub
*
* @param caPrivateKeyPem - CA private key in PEM format
* @param userPublicKeyLine - User's public key in OpenSSH format
* @param options - Certificate options (principals, validity, etc.)
* @returns Signed certificate
*/
export function signPublicKey(
caPrivateKeyPem: string,
userPublicKeyLine: string,
options: CertificateOptions
): SignedCertificate {
// Parse the user's public key
const { keyType, keyData } = parseOpenSSHPublicKey(userPublicKeyLine);
// Determine certificate type string
let certTypeString: string;
if (keyType === "ssh-ed25519") {
certTypeString = "ssh-ed25519-cert-v01@openssh.com";
} else if (keyType === "ssh-rsa") {
certTypeString = "ssh-rsa-cert-v01@openssh.com";
} else if (keyType === "ecdsa-sha2-nistp256") {
certTypeString = "ecdsa-sha2-nistp256-cert-v01@openssh.com";
} else if (keyType === "ecdsa-sha2-nistp384") {
certTypeString = "ecdsa-sha2-nistp384-cert-v01@openssh.com";
} else if (keyType === "ecdsa-sha2-nistp521") {
certTypeString = "ecdsa-sha2-nistp521-cert-v01@openssh.com";
} else {
throw new Error(`Unsupported key type: ${keyType}`);
}
// Get CA public key from private key
const caPrivKey = crypto.createPrivateKey(caPrivateKeyPem);
const caPubKey = crypto.createPublicKey(caPrivKey);
const caRawPubKey = caPubKey.export({ type: "spki", format: "der" });
const caEd25519PubKey = caRawPubKey.subarray(caRawPubKey.length - 32);
const caPubKeyBlob = encodeEd25519PublicKey(caEd25519PubKey);
// Set defaults
const serial = options.serial ?? BigInt(Date.now());
const certType = options.certType ?? 1; // 1 = user cert
const now = BigInt(Math.floor(Date.now() / 1000));
const validAfter = options.validAfter ?? (now - 60n); // 1 minute ago
const validBefore = options.validBefore ?? (now + 86400n * 365n); // 1 year from now
// Default extensions for user certificates
const defaultExtensions = [
"permit-X11-forwarding",
"permit-agent-forwarding",
"permit-port-forwarding",
"permit-pty",
"permit-user-rc"
];
const extensions = options.extensions ?? defaultExtensions;
const criticalOptions = options.criticalOptions ?? new Map();
// Generate nonce (random bytes)
const nonce = crypto.randomBytes(32);
// Extract the public key portion from the user's key blob
// For Ed25519: skip the key type string, get the public key (already encoded)
let userKeyPortion: Buffer;
if (keyType === "ssh-ed25519") {
// Skip the key type string, take the rest (which is encodeString(32-byte-key))
const { newOffset } = decodeString(keyData, 0);
userKeyPortion = keyData.subarray(newOffset);
} else {
// For other key types, extract everything after the key type
const { newOffset } = decodeString(keyData, 0);
userKeyPortion = keyData.subarray(newOffset);
}
// Build the certificate body (to be signed)
const certBody = Buffer.concat([
encodeString(certTypeString),
encodeString(nonce),
userKeyPortion,
encodeUInt64(serial),
encodeUInt32(certType),
encodeString(options.keyId),
buildPrincipals(options.validPrincipals),
encodeUInt64(validAfter),
encodeUInt64(validBefore),
buildCriticalOptions(criticalOptions),
buildExtensions(extensions),
encodeString(""), // reserved
encodeString(caPubKeyBlob) // signature key (CA public key)
]);
// Sign the certificate body
const signature = crypto.sign(null, certBody, caPrivKey);
// Build the full signature blob (algorithm + signature)
const signatureBlob = Buffer.concat([
encodeString("ssh-ed25519"),
encodeString(signature)
]);
// Build complete certificate
const certificate = Buffer.concat([
certBody,
encodeString(signatureBlob)
]);
// Format as OpenSSH certificate line
const certLine = `${certTypeString} ${certificate.toString("base64")} ${options.keyId}`;
return {
certificate: certLine,
certType: certTypeString,
serial,
keyId: options.keyId,
validPrincipals: options.validPrincipals,
validAfter: new Date(Number(validAfter) * 1000),
validBefore: new Date(Number(validBefore) * 1000)
};
}

View File

@@ -16,18 +16,24 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { build } from "@server/build"; import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "#private/lib/billing";
import { Tier } from "@server/types/Tiers";
export async function verifyValidSubscription( export function verifyValidSubscription(tiers: Tier[]) {
return async function (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ): Promise<any> {
try { try {
if (build != "saas") { if (build != "saas") {
return next(); return next();
} }
const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId; const orgId =
req.params.orgId ||
req.body.orgId ||
req.query.orgId ||
req.userOrgId;
if (!orgId) { if (!orgId) {
return next( return next(
@@ -38,9 +44,9 @@ export async function verifyValidSubscription(
); );
} }
const tier = await getOrgTierData(orgId); const { tier, active } = await getOrgTierData(orgId);
const isTier = tiers.includes(tier as Tier);
if (!tier.active) { if (!active) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
@@ -48,6 +54,14 @@ export async function verifyValidSubscription(
) )
); );
} }
if (!isTier) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Organization subscription tier does not have access to this feature"
)
);
}
return next(); return next();
} catch (e) { } catch (e) {
@@ -58,4 +72,5 @@ export async function verifyValidSubscription(
) )
); );
} }
};
} }

View File

@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
import { approvals, db, type Approval } from "@server/db"; import { approvals, db, type Approval } from "@server/db";
import { eq, sql, and } from "drizzle-orm"; import { eq, sql, and, inArray } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
@@ -88,7 +88,7 @@ export async function countApprovals(
.where( .where(
and( and(
eq(approvals.orgId, orgId), eq(approvals.orgId, orgId),
sql`${approvals.decision} in ${state}` inArray(approvals.decision, state)
) )
); );

View File

@@ -19,8 +19,6 @@ import { fromError } from "zod-validation-error";
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
import { build } from "@server/build"; import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { import {
approvals, approvals,
clients, clients,
@@ -30,7 +28,7 @@ import {
currentFingerprint, currentFingerprint,
type Approval type Approval
} from "@server/db"; } from "@server/db";
import { eq, isNull, sql, not, and, desc } from "drizzle-orm"; import { eq, isNull, sql, not, and, desc, gte, lte } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { getUserDeviceName } from "@server/db/names"; import { getUserDeviceName } from "@server/db/names";
@@ -39,18 +37,26 @@ const paramsSchema = z.strictObject({
}); });
const querySchema = z.strictObject({ const querySchema = z.strictObject({
limit: z limit: z.coerce
.string() .number<string>() // for prettier formatting
.int()
.positive()
.optional() .optional()
.default("1000") .catch(20)
.transform(Number) .default(20),
.pipe(z.int().nonnegative()), cursorPending: z.coerce // pending cursor
offset: z .number<string>()
.string() .int()
.max(1) // 0 means non pending
.min(0) // 1 means pending
.optional() .optional()
.default("0") .catch(undefined),
.transform(Number) cursorTimestamp: z.coerce
.pipe(z.int().nonnegative()), .number<string>()
.int()
.positive()
.optional()
.catch(undefined),
approvalState: z approvalState: z
.enum(["pending", "approved", "denied", "all"]) .enum(["pending", "approved", "denied", "all"])
.optional() .optional()
@@ -63,13 +69,21 @@ const querySchema = z.strictObject({
.pipe(z.number().int().positive().optional()) .pipe(z.number().int().positive().optional())
}); });
async function queryApprovals( async function queryApprovals({
orgId: string, orgId,
limit: number, limit,
offset: number, approvalState,
approvalState: z.infer<typeof querySchema>["approvalState"], cursorPending,
clientId?: number cursorTimestamp,
) { clientId
}: {
orgId: string;
limit: number;
approvalState: z.infer<typeof querySchema>["approvalState"];
cursorPending?: number;
cursorTimestamp?: number;
clientId?: number;
}) {
let state: Array<Approval["decision"]> = []; let state: Array<Approval["decision"]> = [];
switch (approvalState) { switch (approvalState) {
case "pending": case "pending":
@@ -85,6 +99,26 @@ async function queryApprovals(
state = ["approved", "denied", "pending"]; state = ["approved", "denied", "pending"];
} }
const conditions = [
eq(approvals.orgId, orgId),
sql`${approvals.decision} in ${state}`
];
if (clientId) {
conditions.push(eq(approvals.clientId, clientId));
}
const pendingSortKey = sql`CASE ${approvals.decision} WHEN 'pending' THEN 1 ELSE 0 END`;
if (cursorPending != null && cursorTimestamp != null) {
// https://stackoverflow.com/a/79720298/10322846
// composite cursor, next data means (pending, timestamp) <= cursor
conditions.push(
lte(pendingSortKey, cursorPending),
lte(approvals.timestamp, cursorTimestamp)
);
}
const res = await db const res = await db
.select({ .select({
approvalId: approvals.approvalId, approvalId: approvals.approvalId,
@@ -107,7 +141,8 @@ async function queryApprovals(
fingerprintArch: currentFingerprint.arch, fingerprintArch: currentFingerprint.arch,
fingerprintSerialNumber: currentFingerprint.serialNumber, fingerprintSerialNumber: currentFingerprint.serialNumber,
fingerprintUsername: currentFingerprint.username, fingerprintUsername: currentFingerprint.username,
fingerprintHostname: currentFingerprint.hostname fingerprintHostname: currentFingerprint.hostname,
timestamp: approvals.timestamp
}) })
.from(approvals) .from(approvals)
.innerJoin(users, and(eq(approvals.userId, users.userId))) .innerJoin(users, and(eq(approvals.userId, users.userId)))
@@ -120,22 +155,12 @@ async function queryApprovals(
) )
.leftJoin(olms, eq(clients.clientId, olms.clientId)) .leftJoin(olms, eq(clients.clientId, olms.clientId))
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId)) .leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
.where( .where(and(...conditions))
and( .orderBy(desc(pendingSortKey), desc(approvals.timestamp))
eq(approvals.orgId, orgId), .limit(limit + 1); // the `+1` is used for the cursor
sql`${approvals.decision} in ${state}`,
...(clientId ? [eq(approvals.clientId, clientId)] : [])
)
)
.orderBy(
sql`CASE ${approvals.decision} WHEN 'pending' THEN 0 ELSE 1 END`,
desc(approvals.timestamp)
)
.limit(limit)
.offset(offset);
// Process results to format device names and build fingerprint objects // Process results to format device names and build fingerprint objects
return res.map((approval) => { const approvalsList = res.slice(0, limit).map((approval) => {
const model = approval.deviceModel || null; const model = approval.deviceModel || null;
const deviceName = approval.clientName const deviceName = approval.clientName
? getUserDeviceName(model, approval.clientName) ? getUserDeviceName(model, approval.clientName)
@@ -154,14 +179,14 @@ async function queryApprovals(
const fingerprint = hasFingerprintData const fingerprint = hasFingerprintData
? { ? {
platform: approval.fingerprintPlatform || null, platform: approval.fingerprintPlatform ?? null,
osVersion: approval.fingerprintOsVersion || null, osVersion: approval.fingerprintOsVersion ?? null,
kernelVersion: approval.fingerprintKernelVersion || null, kernelVersion: approval.fingerprintKernelVersion ?? null,
arch: approval.fingerprintArch || null, arch: approval.fingerprintArch ?? null,
deviceModel: approval.deviceModel || null, deviceModel: approval.deviceModel ?? null,
serialNumber: approval.fingerprintSerialNumber || null, serialNumber: approval.fingerprintSerialNumber ?? null,
username: approval.fingerprintUsername || null, username: approval.fingerprintUsername ?? null,
hostname: approval.fingerprintHostname || null hostname: approval.fingerprintHostname ?? null
} }
: null; : null;
@@ -185,11 +210,30 @@ async function queryApprovals(
niceId: approval.niceId || null niceId: approval.niceId || null
}; };
}); });
let nextCursorPending: number | null = null;
let nextCursorTimestamp: number | null = null;
if (res.length > limit) {
const lastItem = res[limit];
nextCursorPending = lastItem.decision === "pending" ? 1 : 0;
nextCursorTimestamp = lastItem.timestamp;
}
return {
approvalsList,
nextCursorPending,
nextCursorTimestamp
};
} }
export type ListApprovalsResponse = { export type ListApprovalsResponse = {
approvals: NonNullable<Awaited<ReturnType<typeof queryApprovals>>>; approvals: NonNullable<
pagination: { total: number; limit: number; offset: number }; Awaited<ReturnType<typeof queryApprovals>>
>["approvalsList"];
pagination: {
total: number;
limit: number;
cursorPending: number | null;
cursorTimestamp: number | null;
};
}; };
export async function listApprovals( export async function listApprovals(
@@ -217,30 +261,25 @@ export async function listApprovals(
) )
); );
} }
const { limit, offset, approvalState, clientId } = parsedQuery.data; const {
limit,
cursorPending,
cursorTimestamp,
approvalState,
clientId
} = parsedQuery.data;
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
if (build === "saas") { const { approvalsList, nextCursorPending, nextCursorTimestamp } =
const { tier } = await getOrgTierData(orgId); await queryApprovals({
const subscribed = tier === TierId.STANDARD; orgId: orgId.toString(),
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const approvalsList = await queryApprovals(
orgId.toString(),
limit, limit,
offset, cursorPending,
cursorTimestamp,
approvalState, approvalState,
clientId clientId
); });
const [{ count }] = await db const [{ count }] = await db
.select({ count: sql<number>`count(*)` }) .select({ count: sql<number>`count(*)` })
@@ -252,7 +291,8 @@ export async function listApprovals(
pagination: { pagination: {
total: count, total: count,
limit, limit,
offset cursorPending: nextCursorPending,
cursorTimestamp: nextCursorTimestamp
} }
}, },
success: true, success: true,

View File

@@ -17,10 +17,7 @@ import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { build } from "@server/build";
import { approvals, clients, db, orgs, type Approval } from "@server/db"; import { approvals, clients, db, orgs, type Approval } from "@server/db";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { and, eq, type InferInsertModel } from "drizzle-orm"; import { and, eq, type InferInsertModel } from "drizzle-orm";
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
@@ -64,20 +61,6 @@ export async function processPendingApproval(
} }
const { orgId, approvalId } = parsedParams.data; const { orgId, approvalId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const updateData = parsedBody.data; const updateData = parsedBody.data;
const approval = await db const approval = await db

View File

@@ -13,4 +13,3 @@
export * from "./transferSession"; export * from "./transferSession";
export * from "./getSessionTransferToken"; export * from "./getSessionTransferToken";
export * from "./quickStart";

View File

@@ -1,585 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { NextFunction, Request, Response } from "express";
import {
account,
db,
domainNamespaces,
domains,
exitNodes,
newts,
newtSessions,
orgs,
passwordResetTokens,
Resource,
resourcePassword,
resourcePincode,
resources,
resourceWhitelist,
roleResources,
roles,
roleSites,
sites,
targetHealthCheck,
targets,
userResources,
userSites
} from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { z } from "zod";
import { users } from "@server/db";
import { fromError } from "zod-validation-error";
import createHttpError from "http-errors";
import response from "@server/lib/response";
import { SqliteError } from "better-sqlite3";
import { eq, and, sql } from "drizzle-orm";
import moment from "moment";
import { generateId } from "@server/auth/sessions/app";
import config from "@server/lib/config";
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
import { UserType } from "@server/types/UserTypes";
import { createUserAccountOrg } from "@server/lib/createUserAccountOrg";
import { sendEmail } from "@server/emails";
import WelcomeQuickStart from "@server/emails/templates/WelcomeQuickStart";
import { alphabet, generateRandomString } from "oslo/crypto";
import { createDate, TimeSpan } from "oslo";
import { getUniqueResourceName, getUniqueSiteName } from "@server/db/names";
import { pickPort } from "@server/routers/target/helpers";
import { addTargets } from "@server/routers/newt/targets";
import { isTargetValid } from "@server/lib/validators";
import { listExitNodes } from "#private/lib/exitNodes";
const bodySchema = z.object({
email: z.email().toLowerCase(),
ip: z.string().refine(isTargetValid),
method: z.enum(["http", "https"]),
port: z.int().min(1).max(65535),
pincode: z
.string()
.regex(/^\d{6}$/)
.optional(),
password: z.string().min(4).max(100).optional(),
enableWhitelist: z.boolean().optional().default(true),
animalId: z.string() // This is actually the secret key for the backend
});
export type QuickStartBody = z.infer<typeof bodySchema>;
export type QuickStartResponse = {
newtId: string;
newtSecret: string;
resourceUrl: string;
completeSignUpLink: string;
};
const DEMO_UBO_KEY = "b460293f-347c-4b30-837d-4e06a04d5a22";
export async function quickStart(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const {
email,
ip,
method,
port,
pincode,
password,
enableWhitelist,
animalId
} = parsedBody.data;
try {
const tokenValidation = validateTokenOnApi(animalId);
if (!tokenValidation.isValid) {
logger.warn(
`Quick start failed for ${email} token ${animalId}: ${tokenValidation.message}`
);
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid or expired token"
)
);
}
if (animalId === DEMO_UBO_KEY) {
if (email !== "mehrdad@getubo.com") {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid email for demo Ubo key"
)
);
}
const [existing] = await db
.select()
.from(users)
.where(
and(
eq(users.email, email),
eq(users.type, UserType.Internal)
)
);
if (existing) {
// delete the user if it already exists
await db.delete(users).where(eq(users.userId, existing.userId));
const orgId = `org_${existing.userId}`;
await db.delete(orgs).where(eq(orgs.orgId, orgId));
}
}
const tempPassword = generateId(15);
const passwordHash = await hashPassword(tempPassword);
const userId = generateId(15);
// TODO: see if that user already exists?
// Create the sandbox user
const existing = await db
.select()
.from(users)
.where(
and(eq(users.email, email), eq(users.type, UserType.Internal))
);
if (existing && existing.length > 0) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"A user with that email address already exists"
)
);
}
let newtId: string;
let secret: string;
let fullDomain: string;
let resource: Resource;
let completeSignUpLink: string;
await db.transaction(async (trx) => {
await trx.insert(users).values({
userId: userId,
type: UserType.Internal,
username: email,
email: email,
passwordHash,
dateCreated: moment().toISOString()
});
// create user"s account
await trx.insert(account).values({
userId
});
});
const { success, error, org } = await createUserAccountOrg(
userId,
email
);
if (!success) {
if (error) {
throw new Error(error);
}
throw new Error("Failed to create user account and organization");
}
if (!org) {
throw new Error("Failed to create user account and organization");
}
const orgId = org.orgId;
await db.transaction(async (trx) => {
const token = generateRandomString(
8,
alphabet("0-9", "A-Z", "a-z")
);
await trx
.delete(passwordResetTokens)
.where(eq(passwordResetTokens.userId, userId));
const tokenHash = await hashPassword(token);
await trx.insert(passwordResetTokens).values({
userId: userId,
email: email,
tokenHash,
expiresAt: createDate(new TimeSpan(7, "d")).getTime()
});
// // Create the sandbox newt
// const newClientAddress = await getNextAvailableClientSubnet(orgId);
// if (!newClientAddress) {
// throw new Error("No available subnet found");
// }
// const clientAddress = newClientAddress.split("/")[0];
newtId = generateId(15);
secret = generateId(48);
// Create the sandbox site
const siteNiceId = await getUniqueSiteName(orgId);
const siteName = `First Site`;
// pick a random exit node
const exitNodesList = await listExitNodes(orgId);
// select a random exit node
const randomExitNode =
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
if (!randomExitNode) {
throw new Error("No exit nodes available");
}
const [newSite] = await trx
.insert(sites)
.values({
orgId,
exitNodeId: randomExitNode.exitNodeId,
name: siteName,
niceId: siteNiceId,
// address: clientAddress,
type: "newt",
dockerSocketEnabled: true
})
.returning();
const siteId = newSite.siteId;
const adminRole = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (adminRole.length === 0) {
throw new Error("Admin role not found");
}
await trx.insert(roleSites).values({
roleId: adminRole[0].roleId,
siteId: newSite.siteId
});
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the site
await trx.insert(userSites).values({
userId: req.user?.userId!,
siteId: newSite.siteId
});
}
// add the peer to the exit node
const secretHash = await hashPassword(secret!);
await trx.insert(newts).values({
newtId: newtId!,
secretHash,
siteId: newSite.siteId,
dateCreated: moment().toISOString()
});
const [randomNamespace] = await trx
.select()
.from(domainNamespaces)
.orderBy(sql`RANDOM()`)
.limit(1);
if (!randomNamespace) {
throw new Error("No domain namespace available");
}
const [randomNamespaceDomain] = await trx
.select()
.from(domains)
.where(eq(domains.domainId, randomNamespace.domainId))
.limit(1);
if (!randomNamespaceDomain) {
throw new Error("No domain found for the namespace");
}
const resourceNiceId = await getUniqueResourceName(orgId);
// Create sandbox resource
const subdomain = `${resourceNiceId}-${generateId(5)}`;
fullDomain = `${subdomain}.${randomNamespaceDomain.baseDomain}`;
const resourceName = `First Resource`;
const newResource = await trx
.insert(resources)
.values({
niceId: resourceNiceId,
fullDomain,
domainId: randomNamespaceDomain.domainId,
orgId,
name: resourceName,
subdomain,
http: true,
protocol: "tcp",
ssl: true,
sso: false,
emailWhitelistEnabled: enableWhitelist
})
.returning();
await trx.insert(roleResources).values({
roleId: adminRole[0].roleId,
resourceId: newResource[0].resourceId
});
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the resource
await trx.insert(userResources).values({
userId: req.user?.userId!,
resourceId: newResource[0].resourceId
});
}
resource = newResource[0];
// Create the sandbox target
const { internalPort, targetIps } = await pickPort(siteId!, trx);
if (!internalPort) {
throw new Error("No available internal port");
}
const newTarget = await trx
.insert(targets)
.values({
resourceId: resource.resourceId,
siteId: siteId!,
internalPort,
ip,
method,
port,
enabled: true
})
.returning();
const newHealthcheck = await trx
.insert(targetHealthCheck)
.values({
targetId: newTarget[0].targetId,
hcEnabled: false
})
.returning();
// add the new target to the targetIps array
targetIps.push(`${ip}/32`);
const [newt] = await trx
.select()
.from(newts)
.where(eq(newts.siteId, siteId!))
.limit(1);
await addTargets(
newt.newtId,
newTarget,
newHealthcheck,
resource.protocol
);
// Set resource pincode if provided
if (pincode) {
await trx
.delete(resourcePincode)
.where(
eq(resourcePincode.resourceId, resource!.resourceId)
);
const pincodeHash = await hashPassword(pincode);
await trx.insert(resourcePincode).values({
resourceId: resource!.resourceId,
pincodeHash,
digitLength: 6
});
}
// Set resource password if provided
if (password) {
await trx
.delete(resourcePassword)
.where(
eq(resourcePassword.resourceId, resource!.resourceId)
);
const passwordHash = await hashPassword(password);
await trx.insert(resourcePassword).values({
resourceId: resource!.resourceId,
passwordHash
});
}
// Set resource OTP if whitelist is enabled
if (enableWhitelist) {
await trx.insert(resourceWhitelist).values({
email,
resourceId: resource!.resourceId
});
}
completeSignUpLink = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?quickstart=true&email=${email}&token=${token}`;
// Store token for email outside transaction
await sendEmail(
WelcomeQuickStart({
username: email,
link: completeSignUpLink,
fallbackLink: `${config.getRawConfig().app.dashboard_url}/auth/reset-password?quickstart=true&email=${email}`,
resourceMethod: method,
resourceHostname: ip,
resourcePort: port,
resourceUrl: `https://${fullDomain}`,
cliCommand: `newt --id ${newtId} --secret ${secret}`
}),
{
to: email,
from: config.getNoReplyEmail(),
subject: `Access your Pangolin dashboard and resources`
}
);
});
return response<QuickStartResponse>(res, {
data: {
newtId: newtId!,
newtSecret: secret!,
resourceUrl: `https://${fullDomain!}`,
completeSignUpLink: completeSignUpLink!
},
success: true,
error: false,
message: "Quick start completed successfully",
status: HttpCode.OK
});
} catch (e) {
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Account already exists with that email. Email: ${email}. IP: ${req.ip}.`
);
}
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"A user with that email address already exists"
)
);
} else {
logger.error(e);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to do quick start"
)
);
}
}
}
const BACKEND_SECRET_KEY = "4f9b6000-5d1a-11f0-9de7-ff2cc032f501";
/**
* Validates a token received from the frontend.
* @param {string} token The validation token from the request.
* @returns {{ isValid: boolean; message: string }} An object indicating if the token is valid.
*/
const validateTokenOnApi = (
token: string
): { isValid: boolean; message: string } => {
if (token === DEMO_UBO_KEY) {
// Special case for demo UBO key
return { isValid: true, message: "Demo UBO key is valid." };
}
if (!token) {
return { isValid: false, message: "Error: No token provided." };
}
try {
// 1. Decode the base64 string
const decodedB64 = atob(token);
// 2. Reverse the character code manipulation
const deobfuscated = decodedB64
.split("")
.map((char) => String.fromCharCode(char.charCodeAt(0) - 5)) // Reverse the shift
.join("");
// 3. Split the data to get the original secret and timestamp
const parts = deobfuscated.split("|");
if (parts.length !== 2) {
throw new Error("Invalid token format.");
}
const receivedKey = parts[0];
const tokenTimestamp = parseInt(parts[1], 10);
// 4. Check if the secret key matches
if (receivedKey !== BACKEND_SECRET_KEY) {
return { isValid: false, message: "Invalid token: Key mismatch." };
}
// 5. Check if the timestamp is recent (e.g., within 30 seconds) to prevent replay attacks
const now = Date.now();
const timeDifference = now - tokenTimestamp;
if (timeDifference > 30000) {
// 30 seconds
return { isValid: false, message: "Invalid token: Expired." };
}
if (timeDifference < 0) {
// Timestamp is in the future
return {
isValid: false,
message: "Invalid token: Timestamp is in the future."
};
}
// If all checks pass, the token is valid
return { isValid: true, message: "Token is valid!" };
} catch (error) {
// This will catch errors from atob (if not valid base64) or other issues.
return {
isValid: false,
message: `Error: ${(error as Error).message}`
};
}
};

View File

@@ -0,0 +1,268 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { customers, db, subscriptions, subscriptionItems } from "@server/db";
import { eq, and, or } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import stripe from "#private/lib/stripe";
import {
getTier1FeaturePriceSet,
getTier3FeaturePriceSet,
getTier2FeaturePriceSet,
FeatureId,
type FeaturePriceSet
} from "@server/lib/billing";
import { getLineItems } from "@server/lib/billing/getLineItems";
const changeTierSchema = z.strictObject({
orgId: z.string()
});
const changeTierBodySchema = z.strictObject({
tier: z.enum(["tier1", "tier2", "tier3"])
});
export async function changeTier(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = changeTierSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const parsedBody = changeTierBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { tier } = parsedBody.data;
// Get the customer for this org
const [customer] = await db
.select()
.from(customers)
.where(eq(customers.orgId, orgId))
.limit(1);
if (!customer) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"No customer found for this organization"
)
);
}
// Get the active subscription for this customer
const [subscription] = await db
.select()
.from(subscriptions)
.where(
and(
eq(subscriptions.customerId, customer.customerId),
eq(subscriptions.status, "active"),
or(
eq(subscriptions.type, "tier1"),
eq(subscriptions.type, "tier2"),
eq(subscriptions.type, "tier3")
)
)
)
.limit(1);
if (!subscription) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"No active subscription found for this organization"
)
);
}
// Get the target tier's price set
let targetPriceSet: FeaturePriceSet;
if (tier === "tier1") {
targetPriceSet = getTier1FeaturePriceSet();
} else if (tier === "tier2") {
targetPriceSet = getTier2FeaturePriceSet();
} else if (tier === "tier3") {
targetPriceSet = getTier3FeaturePriceSet();
} else {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid tier"));
}
// Get current subscription items from our database
const currentItems = await db
.select()
.from(subscriptionItems)
.where(
eq(
subscriptionItems.subscriptionId,
subscription.subscriptionId
)
);
if (currentItems.length === 0) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"No subscription items found"
)
);
}
// Retrieve the full subscription from Stripe to get item IDs
const stripeSubscription = await stripe!.subscriptions.retrieve(
subscription.subscriptionId
);
// Determine if we're switching between different products
// tier1 uses TIER1 product, tier2/tier3 use USERS product
const currentTier = subscription.type;
const switchingProducts =
(currentTier === "tier1" &&
(tier === "tier2" || tier === "tier3")) ||
((currentTier === "tier2" || currentTier === "tier3") &&
tier === "tier1");
let updatedSubscription;
if (switchingProducts) {
// When switching between different products, we need to:
// 1. Delete old subscription items
// 2. Add new subscription items
logger.info(
`Switching products from ${currentTier} to ${tier} for subscription ${subscription.subscriptionId}`
);
// Build array to delete all existing items and add new ones
const itemsToUpdate: any[] = [];
// Mark all existing items for deletion
for (const stripeItem of stripeSubscription.items.data) {
itemsToUpdate.push({
id: stripeItem.id,
deleted: true
});
}
// Add new items for the target tier
const newLineItems = await getLineItems(targetPriceSet, orgId);
for (const lineItem of newLineItems) {
itemsToUpdate.push(lineItem);
}
updatedSubscription = await stripe!.subscriptions.update(
subscription.subscriptionId,
{
items: itemsToUpdate,
proration_behavior: "create_prorations"
}
);
} else {
// Same product, different price tier (tier2 <-> tier3)
// We can simply update the price
logger.info(
`Updating price from ${currentTier} to ${tier} for subscription ${subscription.subscriptionId}`
);
const itemsToUpdate = stripeSubscription.items.data.map(
(stripeItem) => {
// Find the corresponding item in our database
const dbItem = currentItems.find(
(item) => item.priceId === stripeItem.price.id
);
if (!dbItem) {
// Keep the existing item unchanged if we can't find it
return {
id: stripeItem.id,
price: stripeItem.price.id,
quantity: stripeItem.quantity
};
}
// Map to the corresponding feature in the new tier
const newPriceId = targetPriceSet[FeatureId.USERS];
if (newPriceId) {
return {
id: stripeItem.id,
price: newPriceId,
quantity: stripeItem.quantity
};
}
// If no mapping found, keep existing
return {
id: stripeItem.id,
price: stripeItem.price.id,
quantity: stripeItem.quantity
};
}
);
updatedSubscription = await stripe!.subscriptions.update(
subscription.subscriptionId,
{
items: itemsToUpdate,
proration_behavior: "create_prorations"
}
);
}
logger.info(
`Successfully changed tier to ${tier} for org ${orgId}, subscription ${subscription.subscriptionId}`
);
return response<{ subscriptionId: string; newTier: string }>(res, {
data: {
subscriptionId: updatedSubscription.id,
newTier: tier
},
success: true,
error: false,
message: "Tier change successful",
status: HttpCode.OK
});
} catch (error) {
logger.error("Error changing tier:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred while changing tier"
)
);
}
}

View File

@@ -22,14 +22,23 @@ import logger from "@server/logger";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import stripe from "#private/lib/stripe"; import stripe from "#private/lib/stripe";
import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing"; import {
import { getTierPriceSet, TierId } from "@server/lib/billing/tiers"; getTier1FeaturePriceSet,
getTier3FeaturePriceSet,
getTier2FeaturePriceSet
} from "@server/lib/billing";
import { getLineItems } from "@server/lib/billing/getLineItems";
import Stripe from "stripe";
const createCheckoutSessionSchema = z.strictObject({ const createCheckoutSessionSchema = z.strictObject({
orgId: z.string() orgId: z.string()
}); });
export async function createCheckoutSessionSAAS( const createCheckoutSessionBodySchema = z.strictObject({
tier: z.enum(["tier1", "tier2", "tier3"])
});
export async function createCheckoutSession(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -47,6 +56,18 @@ export async function createCheckoutSessionSAAS(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
const parsedBody = createCheckoutSessionBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { tier } = parsedBody.data;
// check if we already have a customer for this org // check if we already have a customer for this org
const [customer] = await db const [customer] = await db
.select() .select()
@@ -65,20 +86,26 @@ export async function createCheckoutSessionSAAS(
); );
} }
const standardTierPrice = getTierPriceSet()[TierId.STANDARD]; let lineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
if (tier === "tier1") {
lineItems = await getLineItems(getTier1FeaturePriceSet(), orgId);
} else if (tier === "tier2") {
lineItems = await getLineItems(getTier2FeaturePriceSet(), orgId);
} else if (tier === "tier3") {
lineItems = await getLineItems(getTier3FeaturePriceSet(), orgId);
} else {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid plan"));
}
logger.debug(`Line items: ${JSON.stringify(lineItems)}`);
const session = await stripe!.checkout.sessions.create({ const session = await stripe!.checkout.sessions.create({
client_reference_id: orgId, // So we can look it up the org later on the webhook client_reference_id: orgId, // So we can look it up the org later on the webhook
billing_address_collection: "required", billing_address_collection: "required",
line_items: [ line_items: lineItems,
{
price: standardTierPrice, // Use the standard tier
quantity: 1
},
...getLineItems(getStandardFeaturePriceSet())
], // Start with the standard feature set that matches the free limits
customer: customer.customerId, customer: customer.customerId,
mode: "subscription", mode: "subscription",
allow_promotion_codes: true,
success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/billing?success=true&session_id={CHECKOUT_SESSION_ID}`, success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/billing?success=true&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/billing?canceled=true` cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/billing?canceled=true`
}); });

View File

@@ -0,0 +1,440 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { SubscriptionType } from "./hooks/getSubType";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { Tier } from "@server/types/Tiers";
import logger from "@server/logger";
import {
db,
idp,
idpOrg,
loginPage,
loginPageBranding,
loginPageBrandingOrg,
loginPageOrg,
orgs,
resources,
roles
} from "@server/db";
import { eq } from "drizzle-orm";
/**
* Get the maximum allowed retention days for a given tier
* Returns null for enterprise tier (unlimited)
*/
function getMaxRetentionDaysForTier(tier: Tier | null): number | null {
if (!tier) {
return 3; // Free tier
}
switch (tier) {
case "tier1":
return 7;
case "tier2":
return 30;
case "tier3":
return 90;
case "enterprise":
return null; // No limit
default:
return 3; // Default to free tier limit
}
}
/**
* Cap retention days to the maximum allowed for the given tier
*/
async function capRetentionDays(
orgId: string,
tier: Tier | null
): Promise<void> {
const maxRetentionDays = getMaxRetentionDaysForTier(tier);
// If there's no limit (enterprise tier), no capping needed
if (maxRetentionDays === null) {
logger.debug(
`No retention day limit for org ${orgId} on tier ${tier || "free"}`
);
return;
}
// Get current org settings
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
if (!org) {
logger.warn(`Org ${orgId} not found when capping retention days`);
return;
}
const updates: Partial<typeof orgs.$inferInsert> = {};
let needsUpdate = false;
// Cap request log retention if it exceeds the limit
if (
org.settingsLogRetentionDaysRequest !== null &&
org.settingsLogRetentionDaysRequest > maxRetentionDays
) {
updates.settingsLogRetentionDaysRequest = maxRetentionDays;
needsUpdate = true;
logger.info(
`Capping request log retention from ${org.settingsLogRetentionDaysRequest} to ${maxRetentionDays} days for org ${orgId}`
);
}
// Cap access log retention if it exceeds the limit
if (
org.settingsLogRetentionDaysAccess !== null &&
org.settingsLogRetentionDaysAccess > maxRetentionDays
) {
updates.settingsLogRetentionDaysAccess = maxRetentionDays;
needsUpdate = true;
logger.info(
`Capping access log retention from ${org.settingsLogRetentionDaysAccess} to ${maxRetentionDays} days for org ${orgId}`
);
}
// Cap action log retention if it exceeds the limit
if (
org.settingsLogRetentionDaysAction !== null &&
org.settingsLogRetentionDaysAction > maxRetentionDays
) {
updates.settingsLogRetentionDaysAction = maxRetentionDays;
needsUpdate = true;
logger.info(
`Capping action log retention from ${org.settingsLogRetentionDaysAction} to ${maxRetentionDays} days for org ${orgId}`
);
}
// Apply updates if needed
if (needsUpdate) {
await db.update(orgs).set(updates).where(eq(orgs.orgId, orgId));
logger.info(
`Successfully capped retention days for org ${orgId} to max ${maxRetentionDays} days`
);
} else {
logger.debug(`No retention day capping needed for org ${orgId}`);
}
}
export async function handleTierChange(
orgId: string,
newTier: SubscriptionType | null,
previousTier?: SubscriptionType | null
): Promise<void> {
logger.info(
`Handling tier change for org ${orgId}: ${previousTier || "none"} -> ${newTier || "free"}`
);
// Get all orgs that have this orgId as their billingOrgId
const associatedOrgs = await db
.select()
.from(orgs)
.where(eq(orgs.billingOrgId, orgId));
logger.info(
`Found ${associatedOrgs.length} org(s) associated with billing org ${orgId}`
);
// Loop over all associated orgs and apply tier changes
for (const org of associatedOrgs) {
await handleTierChangeForOrg(org.orgId, newTier, previousTier);
}
logger.info(
`Completed tier change handling for all orgs associated with billing org ${orgId}`
);
}
async function handleTierChangeForOrg(
orgId: string,
newTier: SubscriptionType | null,
previousTier?: SubscriptionType | null
): Promise<void> {
logger.info(
`Handling tier change for org ${orgId}: ${previousTier || "none"} -> ${newTier || "free"}`
);
// License subscriptions are handled separately and don't use the tier matrix
if (newTier === "license") {
logger.debug(
`New tier is license for org ${orgId}, no feature lifecycle handling needed`
);
return;
}
// If newTier is null, treat as free tier - disable all features
if (newTier === null) {
logger.info(
`Org ${orgId} is reverting to free tier, disabling all paid features`
);
// Cap retention days to free tier limits
await capRetentionDays(orgId, null);
// Disable all features in the tier matrix
for (const [featureKey] of Object.entries(tierMatrix)) {
const feature = featureKey as TierFeature;
logger.info(
`Feature ${feature} is not available in free tier for org ${orgId}. Disabling...`
);
await disableFeature(orgId, feature);
}
logger.info(
`Completed free tier feature lifecycle handling for org ${orgId}`
);
return;
}
// Get the tier (cast as Tier since we've ruled out "license" and null)
const tier = newTier as Tier;
// Cap retention days to the new tier's limits
await capRetentionDays(orgId, tier);
// Check each feature in the tier matrix
for (const [featureKey, allowedTiers] of Object.entries(tierMatrix)) {
const feature = featureKey as TierFeature;
const isFeatureAvailable = allowedTiers.includes(tier);
if (!isFeatureAvailable) {
logger.info(
`Feature ${feature} is not available in tier ${tier} for org ${orgId}. Disabling...`
);
await disableFeature(orgId, feature);
} else {
logger.debug(
`Feature ${feature} is available in tier ${tier} for org ${orgId}`
);
}
}
logger.info(
`Completed tier change feature lifecycle handling for org ${orgId}`
);
}
async function disableFeature(
orgId: string,
feature: TierFeature
): Promise<void> {
try {
switch (feature) {
case TierFeature.OrgOidc:
await disableOrgOidc(orgId);
break;
case TierFeature.LoginPageDomain:
await disableLoginPageDomain(orgId);
break;
case TierFeature.DeviceApprovals:
await disableDeviceApprovals(orgId);
break;
case TierFeature.LoginPageBranding:
await disableLoginPageBranding(orgId);
break;
case TierFeature.LogExport:
await disableLogExport(orgId);
break;
case TierFeature.AccessLogs:
await disableAccessLogs(orgId);
break;
case TierFeature.ActionLogs:
await disableActionLogs(orgId);
break;
case TierFeature.RotateCredentials:
await disableRotateCredentials(orgId);
break;
case TierFeature.MaintencePage:
await disableMaintencePage(orgId);
break;
case TierFeature.DevicePosture:
await disableDevicePosture(orgId);
break;
case TierFeature.TwoFactorEnforcement:
await disableTwoFactorEnforcement(orgId);
break;
case TierFeature.SessionDurationPolicies:
await disableSessionDurationPolicies(orgId);
break;
case TierFeature.PasswordExpirationPolicies:
await disablePasswordExpirationPolicies(orgId);
break;
case TierFeature.AutoProvisioning:
await disableAutoProvisioning(orgId);
break;
default:
logger.warn(
`Unknown feature ${feature} for org ${orgId}, skipping`
);
}
logger.info(
`Successfully disabled feature ${feature} for org ${orgId}`
);
} catch (error) {
logger.error(
`Error disabling feature ${feature} for org ${orgId}:`,
error
);
throw error;
}
}
async function disableOrgOidc(orgId: string): Promise<void> {}
async function disableDeviceApprovals(orgId: string): Promise<void> {
await db
.update(roles)
.set({ requireDeviceApproval: false })
.where(eq(roles.orgId, orgId));
logger.info(`Disabled device approvals on all roles for org ${orgId}`);
}
async function disableLoginPageBranding(orgId: string): Promise<void> {
const [existingBranding] = await db
.select()
.from(loginPageBrandingOrg)
.where(eq(loginPageBrandingOrg.orgId, orgId));
if (existingBranding) {
await db
.delete(loginPageBranding)
.where(
eq(
loginPageBranding.loginPageBrandingId,
existingBranding.loginPageBrandingId
)
);
logger.info(`Disabled login page branding for org ${orgId}`);
}
}
async function disableLoginPageDomain(orgId: string): Promise<void> {
const [existingLoginPage] = await db
.select()
.from(loginPageOrg)
.where(eq(loginPageOrg.orgId, orgId))
.innerJoin(
loginPage,
eq(loginPage.loginPageId, loginPageOrg.loginPageId)
);
if (existingLoginPage) {
await db.delete(loginPageOrg).where(eq(loginPageOrg.orgId, orgId));
await db
.delete(loginPage)
.where(
eq(
loginPage.loginPageId,
existingLoginPage.loginPageOrg.loginPageId
)
);
logger.info(`Disabled login page domain for org ${orgId}`);
}
}
async function disableLogExport(orgId: string): Promise<void> {}
async function disableAccessLogs(orgId: string): Promise<void> {
await db
.update(orgs)
.set({ settingsLogRetentionDaysAccess: 0 })
.where(eq(orgs.orgId, orgId));
logger.info(`Disabled access logs for org ${orgId}`);
}
async function disableActionLogs(orgId: string): Promise<void> {
await db
.update(orgs)
.set({ settingsLogRetentionDaysAction: 0 })
.where(eq(orgs.orgId, orgId));
logger.info(`Disabled action logs for org ${orgId}`);
}
async function disableRotateCredentials(orgId: string): Promise<void> {}
async function disableMaintencePage(orgId: string): Promise<void> {
await db
.update(resources)
.set({
maintenanceModeEnabled: false
})
.where(eq(resources.orgId, orgId));
logger.info(`Disabled maintenance page on all resources for org ${orgId}`);
}
async function disableDevicePosture(orgId: string): Promise<void> {}
async function disableTwoFactorEnforcement(orgId: string): Promise<void> {
await db
.update(orgs)
.set({ requireTwoFactor: false })
.where(eq(orgs.orgId, orgId));
logger.info(`Disabled two-factor enforcement for org ${orgId}`);
}
async function disableSessionDurationPolicies(orgId: string): Promise<void> {
await db
.update(orgs)
.set({ maxSessionLengthHours: null })
.where(eq(orgs.orgId, orgId));
logger.info(`Disabled session duration policies for org ${orgId}`);
}
async function disablePasswordExpirationPolicies(orgId: string): Promise<void> {
await db
.update(orgs)
.set({ passwordExpiryDays: null })
.where(eq(orgs.orgId, orgId));
logger.info(`Disabled password expiration policies for org ${orgId}`);
}
async function disableAutoProvisioning(orgId: string): Promise<void> {
// Get all IDP IDs for this org through the idpOrg join table
const orgIdps = await db
.select({ idpId: idpOrg.idpId })
.from(idpOrg)
.where(eq(idpOrg.orgId, orgId));
// Update autoProvision to false for all IDPs in this org
for (const { idpId } of orgIdps) {
await db
.update(idp)
.set({ autoProvision: false })
.where(eq(idp.idpId, idpId));
}
}

View File

@@ -23,6 +23,8 @@ import logger from "@server/logger";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types"; import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
// Import tables for billing // Import tables for billing
import { import {
@@ -70,9 +72,19 @@ export async function getOrgSubscriptions(
throw err; throw err;
} }
let limitsExceeded = false;
if (build === "saas") {
try {
limitsExceeded = await usageService.checkLimitSet(orgId);
} catch (err) {
logger.error("Error checking limits for org %s: %s", orgId, err);
}
}
return response<GetOrgSubscriptionResponse>(res, { return response<GetOrgSubscriptionResponse>(res, {
data: { data: {
subscriptions subscriptions,
...(build === "saas" ? { limitsExceeded } : {})
}, },
success: true, success: true,
error: false, error: false,
@@ -100,11 +112,13 @@ export async function getOrgSubscriptionsData(
throw new Error(`Not found`); throw new Error(`Not found`);
} }
const billingOrgId = org[0].billingOrgId || org[0].orgId;
// Get customer for org // Get customer for org
const customer = await db const customer = await db
.select() .select()
.from(customers) .from(customers)
.where(eq(customers.orgId, orgId)) .where(eq(customers.orgId, billingOrgId))
.limit(1); .limit(1);
const subscriptionsWithItems: Array<{ const subscriptionsWithItems: Array<{

View File

@@ -78,39 +78,40 @@ export async function getOrgUsage(
// Get usage for org // Get usage for org
const usageData = []; const usageData = [];
const siteUptime = await usageService.getUsage( const sites = await usageService.getUsage(orgId, FeatureId.SITES);
orgId, const users = await usageService.getUsage(orgId, FeatureId.USERS);
FeatureId.SITE_UPTIME const domains = await usageService.getUsage(orgId, FeatureId.DOMAINS);
); const remoteExitNodes = await usageService.getUsage(
const users = await usageService.getUsageDaily(orgId, FeatureId.USERS);
const domains = await usageService.getUsageDaily(
orgId,
FeatureId.DOMAINS
);
const remoteExitNodes = await usageService.getUsageDaily(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES FeatureId.REMOTE_EXIT_NODES
); );
const egressData = await usageService.getUsage( const organizations = await usageService.getUsage(
orgId, orgId,
FeatureId.EGRESS_DATA_MB FeatureId.ORGINIZATIONS
); );
// const egressData = await usageService.getUsage(
// orgId,
// FeatureId.EGRESS_DATA_MB
// );
if (siteUptime) { if (sites) {
usageData.push(siteUptime); usageData.push(sites);
} }
if (users) { if (users) {
usageData.push(users); usageData.push(users);
} }
if (egressData) { // if (egressData) {
usageData.push(egressData); // usageData.push(egressData);
} // }
if (domains) { if (domains) {
usageData.push(domains); usageData.push(domains);
} }
if (remoteExitNodes) { if (remoteExitNodes) {
usageData.push(remoteExitNodes); usageData.push(remoteExitNodes);
} }
if (organizations) {
usageData.push(organizations);
}
const orgLimits = await db const orgLimits = await db
.select() .select()

View File

@@ -1,35 +1,62 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { import {
getLicensePriceSet, getLicensePriceSet,
} from "@server/lib/billing/licenses"; } from "@server/lib/billing/licenses";
import { import {
getTierPriceSet, getTier1FeaturePriceSet,
} from "@server/lib/billing/tiers"; getTier2FeaturePriceSet,
getTier3FeaturePriceSet,
} from "@server/lib/billing/features";
import Stripe from "stripe"; import Stripe from "stripe";
import { Tier } from "@server/types/Tiers";
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): "saas" | "license" { export type SubscriptionType = Tier | "license";
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): SubscriptionType | null {
// Determine subscription type by checking subscription items // Determine subscription type by checking subscription items
let type: "saas" | "license" = "saas"; if (!Array.isArray(fullSubscription.items?.data) || fullSubscription.items.data.length === 0) {
if (Array.isArray(fullSubscription.items?.data)) { return null;
}
for (const item of fullSubscription.items.data) { for (const item of fullSubscription.items.data) {
const priceId = item.price.id; const priceId = item.price.id;
// Check if price ID matches any license price // Check if price ID matches any license price
const licensePrices = Object.values(getLicensePriceSet()); const licensePrices = Object.values(getLicensePriceSet());
if (licensePrices.includes(priceId)) { if (licensePrices.includes(priceId)) {
type = "license"; return "license";
break;
} }
// Check if price ID matches any tier price (saas) // Check if price ID matches home lab tier
const tierPrices = Object.values(getTierPriceSet()); const homeLabPrices = Object.values(getTier1FeaturePriceSet());
if (homeLabPrices.includes(priceId)) {
if (tierPrices.includes(priceId)) { return "tier1";
type = "saas";
break;
} }
// Check if price ID matches tier2 tier
const tier2Prices = Object.values(getTier2FeaturePriceSet());
if (tier2Prices.includes(priceId)) {
return "tier2";
}
// Check if price ID matches tier3 tier
const tier3Prices = Object.values(getTier3FeaturePriceSet());
if (tier3Prices.includes(priceId)) {
return "tier3";
} }
} }
return type; return null;
} }

View File

@@ -31,6 +31,8 @@ import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
import { sendEmail } from "@server/emails"; import { sendEmail } from "@server/emails";
import EnterpriseEditionKeyGenerated from "@server/emails/templates/EnterpriseEditionKeyGenerated"; import EnterpriseEditionKeyGenerated from "@server/emails/templates/EnterpriseEditionKeyGenerated";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { getFeatureIdByPriceId } from "@server/lib/billing/features";
import { handleTierChange } from "../featureLifecycle";
export async function handleSubscriptionCreated( export async function handleSubscriptionCreated(
subscription: Stripe.Subscription subscription: Stripe.Subscription
@@ -59,6 +61,8 @@ export async function handleSubscriptionCreated(
return; return;
} }
const type = getSubType(fullSubscription);
const newSubscription = { const newSubscription = {
subscriptionId: subscription.id, subscriptionId: subscription.id,
customerId: subscription.customer as string, customerId: subscription.customer as string,
@@ -66,7 +70,9 @@ export async function handleSubscriptionCreated(
canceledAt: subscription.canceled_at canceledAt: subscription.canceled_at
? subscription.canceled_at ? subscription.canceled_at
: null, : null,
createdAt: subscription.created createdAt: subscription.created,
type: type,
version: 1 // we are hardcoding the initial version when the subscription is created, and then we will increment it on every update
}; };
await db.insert(subscriptions).values(newSubscription); await db.insert(subscriptions).values(newSubscription);
@@ -87,10 +93,15 @@ export async function handleSubscriptionCreated(
name = product.name || null; name = product.name || null;
} }
// Get the feature ID from the price ID
const featureId = getFeatureIdByPriceId(item.price.id);
return { return {
stripeSubscriptionItemId: item.id,
subscriptionId: subscription.id, subscriptionId: subscription.id,
planId: item.plan.id, planId: item.plan.id,
priceId: item.price.id, priceId: item.price.id,
featureId: featureId || null,
meterId: item.plan.meter, meterId: item.plan.meter,
unitAmount: item.price.unit_amount || 0, unitAmount: item.price.unit_amount || 0,
currentPeriodStart: item.current_period_start, currentPeriodStart: item.current_period_start,
@@ -129,17 +140,23 @@ export async function handleSubscriptionCreated(
return; return;
} }
const type = getSubType(fullSubscription); if (type === "tier1" || type === "tier2" || type === "tier3") {
if (type === "saas") {
logger.debug( logger.debug(
`Handling SAAS subscription lifecycle for org ${customer.orgId}` `Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
); );
// we only need to handle the limit lifecycle for saas subscriptions not for the licenses // we only need to handle the limit lifecycle for saas subscriptions not for the licenses
await handleSubscriptionLifesycle( await handleSubscriptionLifesycle(
customer.orgId, customer.orgId,
subscription.status subscription.status,
type
); );
// Handle initial tier setup - disable features not available in this tier
logger.info(
`Setting up initial tier features for org ${customer.orgId} with type ${type}`
);
await handleTierChange(customer.orgId, type);
const [orgUserRes] = await db const [orgUserRes] = await db
.select() .select()
.from(userOrgs) .from(userOrgs)

View File

@@ -27,6 +27,7 @@ import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
import { getSubType } from "./getSubType"; import { getSubType } from "./getSubType";
import stripe from "#private/lib/stripe"; import stripe from "#private/lib/stripe";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import { handleTierChange } from "../featureLifecycle";
export async function handleSubscriptionDeleted( export async function handleSubscriptionDeleted(
subscription: Stripe.Subscription subscription: Stripe.Subscription
@@ -76,16 +77,23 @@ export async function handleSubscriptionDeleted(
} }
const type = getSubType(fullSubscription); const type = getSubType(fullSubscription);
if (type === "saas") { if (type == "tier1" || type == "tier2" || type == "tier3") {
logger.debug( logger.debug(
`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}` `Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`
); );
await handleSubscriptionLifesycle( await handleSubscriptionLifesycle(
customer.orgId, customer.orgId,
subscription.status subscription.status,
type
); );
// Handle feature lifecycle for cancellation - disable all tier-specific features
logger.info(
`Disabling tier-specific features for org ${customer.orgId} due to subscription deletion`
);
await handleTierChange(customer.orgId, null, type);
const [orgUserRes] = await db const [orgUserRes] = await db
.select() .select()
.from(userOrgs) .from(userOrgs)

View File

@@ -23,11 +23,12 @@ import {
} from "@server/db"; } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { getFeatureIdByMetricId } from "@server/lib/billing/features"; import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
import stripe from "#private/lib/stripe"; import stripe from "#private/lib/stripe";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
import { getSubType } from "./getSubType"; import { getSubType, SubscriptionType } from "./getSubType";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import { handleTierChange } from "../featureLifecycle";
export async function handleSubscriptionUpdated( export async function handleSubscriptionUpdated(
subscription: Stripe.Subscription, subscription: Stripe.Subscription,
@@ -64,6 +65,9 @@ export async function handleSubscriptionUpdated(
.where(eq(customers.customerId, subscription.customer as string)) .where(eq(customers.customerId, subscription.customer as string))
.limit(1); .limit(1);
const type = getSubType(fullSubscription);
const previousType = existingSubscription.type as SubscriptionType | null;
await db await db
.update(subscriptions) .update(subscriptions)
.set({ .set({
@@ -72,16 +76,45 @@ export async function handleSubscriptionUpdated(
? subscription.canceled_at ? subscription.canceled_at
: null, : null,
updatedAt: Math.floor(Date.now() / 1000), updatedAt: Math.floor(Date.now() / 1000),
billingCycleAnchor: subscription.billing_cycle_anchor billingCycleAnchor: subscription.billing_cycle_anchor,
type: type
}) })
.where(eq(subscriptions.subscriptionId, subscription.id)); .where(eq(subscriptions.subscriptionId, subscription.id));
// Handle tier change if the subscription type changed
if (type && type !== previousType) {
logger.info(
`Tier change detected for org ${customer.orgId}: ${previousType} -> ${type}`
);
await handleTierChange(customer.orgId, type, previousType ?? undefined);
}
// Upsert subscription items // Upsert subscription items
if (Array.isArray(fullSubscription.items?.data)) { if (Array.isArray(fullSubscription.items?.data)) {
const itemsToUpsert = fullSubscription.items.data.map((item) => ({ // First, get existing items to preserve featureId when there's no match
const existingItems = await db
.select()
.from(subscriptionItems)
.where(eq(subscriptionItems.subscriptionId, subscription.id));
const itemsToUpsert = fullSubscription.items.data.map((item) => {
// Try to get featureId from price
let featureId: string | null = getFeatureIdByPriceId(item.price.id) || null;
// If no match, try to preserve existing featureId
if (!featureId) {
const existingItem = existingItems.find(
(ei) => ei.stripeSubscriptionItemId === item.id
);
featureId = existingItem?.featureId || null;
}
return {
stripeSubscriptionItemId: item.id,
subscriptionId: subscription.id, subscriptionId: subscription.id,
planId: item.plan.id, planId: item.plan.id,
priceId: item.price.id, priceId: item.price.id,
featureId: featureId,
meterId: item.plan.meter, meterId: item.plan.meter,
unitAmount: item.price.unit_amount || 0, unitAmount: item.price.unit_amount || 0,
currentPeriodStart: item.current_period_start, currentPeriodStart: item.current_period_start,
@@ -90,7 +123,8 @@ export async function handleSubscriptionUpdated(
? JSON.stringify(item.price.tiers) ? JSON.stringify(item.price.tiers)
: null, : null,
interval: item.plan.interval interval: item.plan.interval
})); };
});
if (itemsToUpsert.length > 0) { if (itemsToUpsert.length > 0) {
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
await trx await trx
@@ -154,7 +188,7 @@ export async function handleSubscriptionUpdated(
const orgId = customer.orgId; const orgId = customer.orgId;
if (!orgId) { if (!orgId) {
logger.warn( logger.debug(
`No orgId found in subscription metadata for subscription ${subscription.id}. Skipping usage reset.` `No orgId found in subscription metadata for subscription ${subscription.id}. Skipping usage reset.`
); );
continue; continue;
@@ -234,17 +268,29 @@ export async function handleSubscriptionUpdated(
} }
// --- end usage update --- // --- end usage update ---
const type = getSubType(fullSubscription); if (type === "tier1" || type === "tier2" || type === "tier3") {
if (type === "saas") {
logger.debug( logger.debug(
`Handling SAAS subscription lifecycle for org ${customer.orgId}` `Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
); );
// we only need to handle the limit lifecycle for saas subscriptions not for the licenses // we only need to handle the limit lifecycle for saas subscriptions not for the licenses
await handleSubscriptionLifesycle( await handleSubscriptionLifesycle(
customer.orgId, customer.orgId,
subscription.status subscription.status,
type
); );
} else {
// Handle feature lifecycle when subscription is canceled or becomes unpaid
if (
subscription.status === "canceled" ||
subscription.status === "unpaid" ||
subscription.status === "incomplete_expired"
) {
logger.info(
`Subscription ${subscription.id} for org ${customer.orgId} is ${subscription.status}, disabling paid features`
);
await handleTierChange(customer.orgId, null, previousType ?? undefined);
}
} else if (type === "license") {
if (subscription.status === "canceled" || subscription.status == "unpaid" || subscription.status == "incomplete_expired") { if (subscription.status === "canceled" || subscription.status == "unpaid" || subscription.status == "incomplete_expired") {
try { try {
// WARNING: // WARNING:

View File

@@ -11,8 +11,9 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
export * from "./createCheckoutSessionSAAS"; export * from "./createCheckoutSession";
export * from "./createPortalSession"; export * from "./createPortalSession";
export * from "./getOrgSubscriptions"; export * from "./getOrgSubscriptions";
export * from "./getOrgUsage"; export * from "./getOrgUsage";
export * from "./internalGetOrgTier"; export * from "./internalGetOrgTier";
export * from "./changeTier";

View File

@@ -13,38 +13,66 @@
import { import {
freeLimitSet, freeLimitSet,
tier1LimitSet,
tier2LimitSet,
tier3LimitSet,
limitsService, limitsService,
subscribedLimitSet LimitSet
} from "@server/lib/billing"; } from "@server/lib/billing";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import logger from "@server/logger"; import { SubscriptionType } from "./hooks/getSubType";
function getLimitSetForSubscriptionType(
subType: SubscriptionType | null
): LimitSet {
switch (subType) {
case "tier1":
return tier1LimitSet;
case "tier2":
return tier2LimitSet;
case "tier3":
return tier3LimitSet;
case "license":
// License subscriptions use tier2 limits by default
// This can be adjusted based on your business logic
return tier2LimitSet;
default:
return freeLimitSet;
}
}
export async function handleSubscriptionLifesycle( export async function handleSubscriptionLifesycle(
orgId: string, orgId: string,
status: string status: string,
subType: SubscriptionType | null
) { ) {
switch (status) { switch (status) {
case "active": case "active":
await limitsService.applyLimitSetToOrg(orgId, subscribedLimitSet); const activeLimitSet = getLimitSetForSubscriptionType(subType);
await usageService.checkLimitSet(orgId, true); await limitsService.applyLimitSetToOrg(orgId, activeLimitSet);
await usageService.checkLimitSet(orgId);
break; break;
case "canceled": case "canceled":
// Subscription canceled - revert to free tier
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet); await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
await usageService.checkLimitSet(orgId, true); await usageService.checkLimitSet(orgId);
break; break;
case "past_due": case "past_due":
// Optionally handle past due status, e.g., notify customer // Payment past due - keep current limits but notify customer
// Limits will revert to free tier if it becomes unpaid
break; break;
case "unpaid": case "unpaid":
// Subscription unpaid - revert to free tier
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet); await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
await usageService.checkLimitSet(orgId, true); await usageService.checkLimitSet(orgId);
break; break;
case "incomplete": case "incomplete":
// Optionally handle incomplete status, e.g., notify customer // Payment incomplete - give them time to complete payment
break; break;
case "incomplete_expired": case "incomplete_expired":
// Payment never completed - revert to free tier
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet); await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
await usageService.checkLimitSet(orgId, true); await usageService.checkLimitSet(orgId);
break; break;
default: default:
break; break;

View File

@@ -25,13 +25,15 @@ import * as logs from "#private/routers/auditLogs";
import * as misc from "#private/routers/misc"; import * as misc from "#private/routers/misc";
import * as reKey from "#private/routers/re-key"; import * as reKey from "#private/routers/re-key";
import * as approval from "#private/routers/approvals"; import * as approval from "#private/routers/approvals";
import * as ssh from "#private/routers/ssh";
import { import {
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction, verifyUserHasAction,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
verifySiteAccess, verifySiteAccess,
verifyClientAccess verifyClientAccess,
verifyLimits
} from "@server/middlewares"; } from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import { import {
@@ -52,6 +54,7 @@ import {
authenticated as a, authenticated as a,
authRouter as aa authRouter as aa
} from "@server/routers/external"; } from "@server/routers/external";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export const authenticated = a; export const authenticated = a;
export const unauthenticated = ua; export const unauthenticated = ua;
@@ -76,7 +79,9 @@ unauthenticated.post(
authenticated.put( authenticated.put(
"/org/:orgId/idp/oidc", "/org/:orgId/idp/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc),
verifyOrgAccess, verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createIdp), verifyUserHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp), logActionAudit(ActionsEnum.createIdp),
orgIdp.createOrgOidcIdp orgIdp.createOrgOidcIdp
@@ -85,8 +90,10 @@ authenticated.put(
authenticated.post( authenticated.post(
"/org/:orgId/idp/:idpId/oidc", "/org/:orgId/idp/:idpId/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc),
verifyOrgAccess, verifyOrgAccess,
verifyIdpAccess, verifyIdpAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateIdp), verifyUserHasAction(ActionsEnum.updateIdp),
logActionAudit(ActionsEnum.updateIdp), logActionAudit(ActionsEnum.updateIdp),
orgIdp.updateOrgOidcIdp orgIdp.updateOrgOidcIdp
@@ -135,35 +142,27 @@ authenticated.post(
verifyValidLicense, verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyCertificateAccess, verifyCertificateAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.restartCertificate), verifyUserHasAction(ActionsEnum.restartCertificate),
logActionAudit(ActionsEnum.restartCertificate), logActionAudit(ActionsEnum.restartCertificate),
certificates.restartCertificate certificates.restartCertificate
); );
if (build === "saas") { if (build === "saas") {
unauthenticated.post(
"/quick-start",
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
keyGenerator: (req) => req.path,
handler: (req, res, next) => {
const message = `We're too busy right now. Please try again later.`;
return next(
createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
);
},
store: createStore()
}),
auth.quickStart
);
authenticated.post( authenticated.post(
"/org/:orgId/billing/create-checkout-session-saas", "/org/:orgId/billing/create-checkout-session",
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.billing), verifyUserHasAction(ActionsEnum.billing),
logActionAudit(ActionsEnum.billing), logActionAudit(ActionsEnum.billing),
billing.createCheckoutSessionSAAS billing.createCheckoutSession
);
authenticated.post(
"/org/:orgId/billing/change-tier",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.billing),
logActionAudit(ActionsEnum.billing),
billing.changeTier
); );
authenticated.post( authenticated.post(
@@ -243,6 +242,7 @@ authenticated.put(
"/org/:orgId/remote-exit-node", "/org/:orgId/remote-exit-node",
verifyValidLicense, verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createRemoteExitNode), verifyUserHasAction(ActionsEnum.createRemoteExitNode),
logActionAudit(ActionsEnum.createRemoteExitNode), logActionAudit(ActionsEnum.createRemoteExitNode),
remoteExitNode.createRemoteExitNode remoteExitNode.createRemoteExitNode
@@ -286,7 +286,9 @@ authenticated.delete(
authenticated.put( authenticated.put(
"/org/:orgId/login-page", "/org/:orgId/login-page",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.loginPageDomain),
verifyOrgAccess, verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createLoginPage), verifyUserHasAction(ActionsEnum.createLoginPage),
logActionAudit(ActionsEnum.createLoginPage), logActionAudit(ActionsEnum.createLoginPage),
loginPage.createLoginPage loginPage.createLoginPage
@@ -295,8 +297,10 @@ authenticated.put(
authenticated.post( authenticated.post(
"/org/:orgId/login-page/:loginPageId", "/org/:orgId/login-page/:loginPageId",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.loginPageDomain),
verifyOrgAccess, verifyOrgAccess,
verifyLoginPageAccess, verifyLoginPageAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateLoginPage), verifyUserHasAction(ActionsEnum.updateLoginPage),
logActionAudit(ActionsEnum.updateLoginPage), logActionAudit(ActionsEnum.updateLoginPage),
loginPage.updateLoginPage loginPage.updateLoginPage
@@ -323,6 +327,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/approvals", "/org/:orgId/approvals",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.deviceApprovals),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listApprovals), verifyUserHasAction(ActionsEnum.listApprovals),
logActionAudit(ActionsEnum.listApprovals), logActionAudit(ActionsEnum.listApprovals),
@@ -339,7 +344,9 @@ authenticated.get(
authenticated.put( authenticated.put(
"/org/:orgId/approvals/:approvalId", "/org/:orgId/approvals/:approvalId",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.deviceApprovals),
verifyOrgAccess, verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateApprovals), verifyUserHasAction(ActionsEnum.updateApprovals),
logActionAudit(ActionsEnum.updateApprovals), logActionAudit(ActionsEnum.updateApprovals),
approval.processPendingApproval approval.processPendingApproval
@@ -348,6 +355,7 @@ authenticated.put(
authenticated.get( authenticated.get(
"/org/:orgId/login-page-branding", "/org/:orgId/login-page-branding",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.loginPageBranding),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getLoginPage), verifyUserHasAction(ActionsEnum.getLoginPage),
logActionAudit(ActionsEnum.getLoginPage), logActionAudit(ActionsEnum.getLoginPage),
@@ -357,7 +365,9 @@ authenticated.get(
authenticated.put( authenticated.put(
"/org/:orgId/login-page-branding", "/org/:orgId/login-page-branding",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.loginPageBranding),
verifyOrgAccess, verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateLoginPage), verifyUserHasAction(ActionsEnum.updateLoginPage),
logActionAudit(ActionsEnum.updateLoginPage), logActionAudit(ActionsEnum.updateLoginPage),
loginPage.upsertLoginPageBranding loginPage.upsertLoginPageBranding
@@ -433,7 +443,7 @@ authenticated.post(
authenticated.get( authenticated.get(
"/org/:orgId/logs/action", "/org/:orgId/logs/action",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.actionLogs),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logs.queryActionAuditLogs logs.queryActionAuditLogs
@@ -442,7 +452,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/action/export", "/org/:orgId/logs/action/export",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.logExport),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs),
@@ -452,7 +462,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/access", "/org/:orgId/logs/access",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.accessLogs),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logs.queryAccessAuditLogs logs.queryAccessAuditLogs
@@ -461,7 +471,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/access/export", "/org/:orgId/logs/access/export",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.logExport),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs),
@@ -470,18 +480,20 @@ authenticated.get(
authenticated.post( authenticated.post(
"/re-key/:clientId/regenerate-client-secret", "/re-key/:clientId/regenerate-client-secret",
verifyClientAccess, // this is first to set the org id
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.rotateCredentials),
verifyClientAccess, // this is first to set the org id
verifyLimits,
verifyUserHasAction(ActionsEnum.reGenerateSecret), verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateClientSecret reKey.reGenerateClientSecret
); );
authenticated.post( authenticated.post(
"/re-key/:siteId/regenerate-site-secret", "/re-key/:siteId/regenerate-site-secret",
verifySiteAccess, // this is first to set the org id
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.rotateCredentials),
verifySiteAccess, // this is first to set the org id
verifyLimits,
verifyUserHasAction(ActionsEnum.reGenerateSecret), verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateSiteSecret reKey.reGenerateSiteSecret
); );
@@ -489,8 +501,20 @@ authenticated.post(
authenticated.put( authenticated.put(
"/re-key/:orgId/regenerate-remote-exit-node-secret", "/re-key/:orgId/regenerate-remote-exit-node-secret",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.rotateCredentials),
verifyOrgAccess, verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.reGenerateSecret), verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateExitNodeSecret reKey.reGenerateExitNodeSecret
); );
authenticated.post(
"/org/:orgId/ssh/sign-key",
verifyValidLicense,
verifyValidSubscription(tierMatrix.sshPam),
verifyOrgAccess,
verifyLimits,
// verifyUserHasAction(ActionsEnum.signSshKey),
logActionAudit(ActionsEnum.signSshKey),
ssh.signSshKey
);

View File

@@ -37,8 +37,9 @@ export async function generateNewEnterpriseLicense(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedParams = generateNewEnterpriseLicenseParamsSchema.safeParse(
const parsedParams = generateNewEnterpriseLicenseParamsSchema.safeParse(req.params); req.params
);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
@@ -63,7 +64,10 @@ export async function generateNewEnterpriseLicense(
const licenseData = req.body; const licenseData = req.body;
if (licenseData.tier != "big_license" && licenseData.tier != "small_license") { if (
licenseData.tier != "big_license" &&
licenseData.tier != "small_license"
) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@@ -79,7 +83,8 @@ export async function generateNewEnterpriseLicense(
return next( return next(
createHttpError( createHttpError(
apiResponse.status || HttpCode.BAD_REQUEST, apiResponse.status || HttpCode.BAD_REQUEST,
apiResponse.message || "Failed to create license from Fossorial API" apiResponse.message ||
"Failed to create license from Fossorial API"
) )
); );
} }
@@ -112,8 +117,11 @@ export async function generateNewEnterpriseLicense(
); );
} }
const tier = licenseData.tier === "big_license" ? LicenseId.BIG_LICENSE : LicenseId.SMALL_LICENSE; const tier =
const tierPrice = getLicensePriceSet()[tier] licenseData.tier === "big_license"
? LicenseId.BIG_LICENSE
: LicenseId.SMALL_LICENSE;
const tierPrice = getLicensePriceSet()[tier];
const session = await stripe!.checkout.sessions.create({ const session = await stripe!.checkout.sessions.create({
client_reference_id: keyId.toString(), client_reference_id: keyId.toString(),
@@ -122,10 +130,11 @@ export async function generateNewEnterpriseLicense(
{ {
price: tierPrice, // Use the standard tier price: tierPrice, // Use the standard tier
quantity: 1 quantity: 1
}, }
], // Start with the standard feature set that matches the free limits ], // Start with the standard feature set that matches the free limits
customer: customer.customerId, customer: customer.customerId,
mode: "subscription", mode: "subscription",
allow_promotion_codes: true,
success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?success=true&session_id={CHECKOUT_SESSION_ID}`, success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?success=true&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true` cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true`
}); });

View File

@@ -19,21 +19,20 @@ import {
verifyApiKeyHasAction, verifyApiKeyHasAction,
verifyApiKeyIsRoot, verifyApiKeyIsRoot,
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess verifyApiKeyIdpAccess,
verifyLimits
} from "@server/middlewares"; } from "@server/middlewares";
import { import {
verifyValidSubscription, verifyValidSubscription,
verifyValidLicense verifyValidLicense
} from "#private/middlewares"; } from "#private/middlewares";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import { import {
unauthenticated as ua, unauthenticated as ua,
authenticated as a authenticated as a
} from "@server/routers/integration"; } from "@server/routers/integration";
import { logActionAudit } from "#private/middlewares"; import { logActionAudit } from "#private/middlewares";
import config from "#private/lib/config"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { build } from "@server/build";
export const unauthenticated = ua; export const unauthenticated = ua;
export const authenticated = a; export const authenticated = a;
@@ -57,7 +56,7 @@ authenticated.delete(
authenticated.get( authenticated.get(
"/org/:orgId/logs/action", "/org/:orgId/logs/action",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.actionLogs),
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs), verifyApiKeyHasAction(ActionsEnum.exportLogs),
logs.queryActionAuditLogs logs.queryActionAuditLogs
@@ -66,7 +65,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/action/export", "/org/:orgId/logs/action/export",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.logExport),
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs), verifyApiKeyHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs),
@@ -76,7 +75,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/access", "/org/:orgId/logs/access",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.accessLogs),
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs), verifyApiKeyHasAction(ActionsEnum.exportLogs),
logs.queryAccessAuditLogs logs.queryAccessAuditLogs
@@ -85,7 +84,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/access/export", "/org/:orgId/logs/access/export",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.logExport),
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs), verifyApiKeyHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs),
@@ -95,7 +94,9 @@ authenticated.get(
authenticated.put( authenticated.put(
"/org/:orgId/idp/oidc", "/org/:orgId/idp/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc),
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createIdp), verifyApiKeyHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp), logActionAudit(ActionsEnum.createIdp),
orgIdp.createOrgOidcIdp orgIdp.createOrgOidcIdp
@@ -104,8 +105,10 @@ authenticated.put(
authenticated.post( authenticated.post(
"/org/:orgId/idp/:idpId/oidc", "/org/:orgId/idp/:idpId/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc),
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess, verifyApiKeyIdpAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateIdp), verifyApiKeyHasAction(ActionsEnum.updateIdp),
logActionAudit(ActionsEnum.updateIdp), logActionAudit(ActionsEnum.updateIdp),
orgIdp.updateOrgOidcIdp orgIdp.updateOrgOidcIdp

View File

@@ -30,9 +30,7 @@ import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { createCertificate } from "#private/routers/certificates/createCertificate"; import { createCertificate } from "#private/routers/certificates/createCertificate";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
import { CreateLoginPageResponse } from "@server/routers/loginPage/types"; import { CreateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
@@ -76,19 +74,6 @@ export async function createLoginPage(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const [existing] = await db const [existing] = await db
.select() .select()
.from(loginPageOrg) .from(loginPageOrg)

View File

@@ -25,9 +25,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -53,18 +51,6 @@ export async function deleteLoginPageBranding(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const [existingLoginPageBranding] = await db const [existingLoginPageBranding] = await db
.select() .select()

View File

@@ -25,9 +25,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -51,19 +49,6 @@ export async function getLoginPageBranding(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const [existingLoginPageBranding] = await db const [existingLoginPageBranding] = await db
.select() .select()
.from(loginPageBranding) .from(loginPageBranding)

View File

@@ -23,9 +23,7 @@ import { eq, and } from "drizzle-orm";
import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { subdomainSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas";
import { createCertificate } from "#private/routers/certificates/createCertificate"; import { createCertificate } from "#private/routers/certificates/createCertificate";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
import { UpdateLoginPageResponse } from "@server/routers/loginPage/types"; import { UpdateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z const paramsSchema = z
@@ -87,18 +85,6 @@ export async function updateLoginPage(
const { loginPageId, orgId } = parsedParams.data; const { loginPageId, orgId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const [existingLoginPage] = await db const [existingLoginPage] = await db
.select() .select()

View File

@@ -25,10 +25,9 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, InferInsertModel } from "drizzle-orm"; import { eq, InferInsertModel } from "drizzle-orm";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build"; import { build } from "@server/build";
import config from "@server/private/lib/config"; import { validateLocalPath } from "@app/lib/validateLocalPath";
import config from "#private/lib/config";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -39,14 +38,36 @@ const bodySchema = z.strictObject({
.union([ .union([
z.literal(""), z.literal(""),
z z
.url("Must be a valid URL") .string()
.superRefine(async (url, ctx) => { .superRefine(async (urlOrPath, ctx) => {
const parseResult = z.url().safeParse(urlOrPath);
if (!parseResult.success) {
if (build !== "enterprise") {
ctx.addIssue({
code: "custom",
message: "Must be a valid URL"
});
return;
} else {
try { try {
const response = await fetch(url, { validateLocalPath(urlOrPath);
} catch (error) {
ctx.addIssue({
code: "custom",
message: "Must be either a valid image URL or a valid pathname starting with `/` and not containing query parameters, `..` or `*`"
});
} finally {
return;
}
}
}
try {
const response = await fetch(urlOrPath, {
method: "HEAD" method: "HEAD"
}).catch(() => { }).catch(() => {
// If HEAD fails (CORS or method not allowed), try GET // If HEAD fails (CORS or method not allowed), try GET
return fetch(url, { method: "GET" }); return fetch(urlOrPath, { method: "GET" });
}); });
if (response.status !== 200) { if (response.status !== 200) {
@@ -128,19 +149,6 @@ export async function upsertLoginPageBranding(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
let updateData = parsedBody.data satisfies InferInsertModel< let updateData = parsedBody.data satisfies InferInsertModel<
typeof loginPageBranding typeof loginPageBranding
>; >;

View File

@@ -24,10 +24,11 @@ import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { encrypt } from "@server/lib/crypto"; import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config";
import { build } from "@server/build";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
@@ -93,6 +94,18 @@ export async function createOrgOidcIdp(
); );
} }
if (
privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
"org"
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
)
);
}
const { const {
clientId, clientId,
clientSecret, clientSecret,
@@ -103,22 +116,20 @@ export async function createOrgOidcIdp(
emailPath, emailPath,
namePath, namePath,
name, name,
autoProvision,
variant, variant,
roleMapping, roleMapping,
tags tags
} = parsedBody.data; } = parsedBody.data;
if (build === "saas") { let { autoProvision } = parsedBody.data;
const { tier, active } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD; if (build == "saas") { // this is not paywalled with a ee license because this whole endpoint is restricted
if (!subscribed) { const subscribed = await isSubscribed(
return next( orgId,
createHttpError( tierMatrix.deviceApprovals
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
); );
if (!subscribed) {
autoProvision = false;
} }
} }

View File

@@ -22,6 +22,7 @@ import { fromError } from "zod-validation-error";
import { idp, idpOidcConfig, idpOrg } from "@server/db"; import { idp, idpOidcConfig, idpOrg } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import privateConfig from "#private/lib/config";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -59,6 +60,18 @@ export async function deleteOrgIdp(
const { idpId } = parsedParams.data; const { idpId } = parsedParams.data;
if (
privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
"org"
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
)
);
}
// Check if IDP exists // Check if IDP exists
const [existingIdp] = await db const [existingIdp] = await db
.select() .select()

View File

@@ -24,9 +24,10 @@ import { idp, idpOidcConfig } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto"; import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config";
import { build } from "@server/build"; import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -98,6 +99,18 @@ export async function updateOrgOidcIdp(
); );
} }
if (
privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
"org"
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
)
);
}
const { idpId, orgId } = parsedParams.data; const { idpId, orgId } = parsedParams.data;
const { const {
clientId, clientId,
@@ -109,21 +122,20 @@ export async function updateOrgOidcIdp(
emailPath, emailPath,
namePath, namePath,
name, name,
autoProvision,
roleMapping, roleMapping,
tags tags
} = parsedBody.data; } = parsedBody.data;
if (build === "saas") { let { autoProvision } = parsedBody.data;
const { tier, active } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD; if (build == "saas") {
if (!subscribed) { // this is not paywalled with a ee license because this whole endpoint is restricted
return next( const subscribed = await isSubscribed(
createHttpError( orgId,
HttpCode.FORBIDDEN, tierMatrix.deviceApprovals
"This organization's current plan does not support this feature."
)
); );
if (!subscribed) {
autoProvision = false;
} }
} }

View File

@@ -12,7 +12,14 @@
*/ */
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { db, exitNodes, exitNodeOrgs, ExitNode, ExitNodeOrg } from "@server/db"; import {
db,
exitNodes,
exitNodeOrgs,
ExitNode,
ExitNodeOrg,
orgs
} from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { z } from "zod"; import { z } from "zod";
import { remoteExitNodes } from "@server/db"; import { remoteExitNodes } from "@server/db";
@@ -25,7 +32,7 @@ import { createRemoteExitNodeSession } from "#private/auth/sessions/remoteExitNo
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { hashPassword, verifyPassword } from "@server/auth/password"; import { hashPassword, verifyPassword } from "@server/auth/password";
import logger from "@server/logger"; import logger from "@server/logger";
import { and, eq } from "drizzle-orm"; import { and, eq, inArray, ne } from "drizzle-orm";
import { getNextAvailableSubnet } from "@server/lib/exitNodes"; import { getNextAvailableSubnet } from "@server/lib/exitNodes";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { FeatureId } from "@server/lib/billing";
@@ -85,7 +92,7 @@ export async function createRemoteExitNode(
if (usage) { if (usage) {
const rejectRemoteExitNodes = await usageService.checkLimitSet( const rejectRemoteExitNodes = await usageService.checkLimitSet(
orgId, orgId,
false,
FeatureId.REMOTE_EXIT_NODES, FeatureId.REMOTE_EXIT_NODES,
{ {
...usage, ...usage,
@@ -97,7 +104,7 @@ export async function createRemoteExitNode(
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@pangolin.net" "Remote node limit exceeded. Please upgrade your plan."
) )
); );
} }
@@ -169,7 +176,17 @@ export async function createRemoteExitNode(
); );
} }
let numExitNodeOrgs: ExitNodeOrg[] | undefined; const [org] = await db
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (!org) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
);
}
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
if (!existingExitNode) { if (!existingExitNode) {
@@ -217,19 +234,43 @@ export async function createRemoteExitNode(
}); });
} }
numExitNodeOrgs = await trx // calculate if the node is in any other of the orgs before we count it as an add to the billing org
if (org.billingOrgId) {
const otherBillingOrgs = await trx
.select()
.from(orgs)
.where(
and(
eq(orgs.billingOrgId, org.billingOrgId),
ne(orgs.orgId, orgId)
)
);
const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
const orgsInBillingDomainThatTheNodeIsStillIn = await trx
.select() .select()
.from(exitNodeOrgs) .from(exitNodeOrgs)
.where(eq(exitNodeOrgs.orgId, orgId)); .where(
}); and(
eq(
exitNodeOrgs.exitNodeId,
existingExitNode.exitNodeId
),
inArray(exitNodeOrgs.orgId, billingOrgIds)
)
);
if (numExitNodeOrgs) { if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.updateDaily( await usageService.add(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES, FeatureId.REMOTE_EXIT_NODES,
numExitNodeOrgs.length 1,
trx
); );
} }
}
});
const token = generateSessionToken(); const token = generateSessionToken();
await createRemoteExitNodeSession(token, remoteExitNodeId); await createRemoteExitNodeSession(token, remoteExitNodeId);

View File

@@ -13,9 +13,9 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, ExitNodeOrg, exitNodeOrgs, exitNodes } from "@server/db"; import { db, ExitNodeOrg, exitNodeOrgs, exitNodes, orgs } from "@server/db";
import { remoteExitNodes } from "@server/db"; import { remoteExitNodes } from "@server/db";
import { and, count, eq } from "drizzle-orm"; import { and, count, eq, inArray } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -50,7 +50,8 @@ export async function deleteRemoteExitNode(
const [remoteExitNode] = await db const [remoteExitNode] = await db
.select() .select()
.from(remoteExitNodes) .from(remoteExitNodes)
.where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)); .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
.limit(1);
if (!remoteExitNode) { if (!remoteExitNode) {
return next( return next(
@@ -70,7 +71,17 @@ export async function deleteRemoteExitNode(
); );
} }
let numExitNodeOrgs: ExitNodeOrg[] | undefined; const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
if (!org) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Org with ID ${orgId} not found`
)
);
}
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
await trx await trx
.delete(exitNodeOrgs) .delete(exitNodeOrgs)
@@ -81,37 +92,38 @@ export async function deleteRemoteExitNode(
) )
); );
const [remainingExitNodeOrgs] = await trx // calculate if the user is in any other of the orgs before we count it as an remove to the billing org
.select({ count: count() }) if (org.billingOrgId) {
.from(exitNodeOrgs) const otherBillingOrgs = await trx
.where(eq(exitNodeOrgs.exitNodeId, remoteExitNode.exitNodeId!)); .select()
.from(orgs)
.where(eq(orgs.billingOrgId, org.billingOrgId));
if (remainingExitNodeOrgs.count === 0) { const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
await trx
.delete(remoteExitNodes)
.where(
eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)
);
await trx
.delete(exitNodes)
.where(
eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId!)
);
}
numExitNodeOrgs = await trx const orgsInBillingDomainThatTheNodeIsStillIn = await trx
.select() .select()
.from(exitNodeOrgs) .from(exitNodeOrgs)
.where(eq(exitNodeOrgs.orgId, orgId)); .where(
}); and(
eq(
exitNodeOrgs.exitNodeId,
remoteExitNode.exitNodeId!
),
inArray(exitNodeOrgs.orgId, billingOrgIds)
)
);
if (numExitNodeOrgs) { if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.updateDaily( await usageService.add(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES, FeatureId.REMOTE_EXIT_NODES,
numExitNodeOrgs.length -1,
trx
); );
} }
}
});
return response(res, { return response(res, {
data: null, data: null,

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