Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
1b0af3a38d Bump golang.org/x/term in /install in the minor-updates group
Bumps the minor-updates group in /install with 1 update: [golang.org/x/term](https://github.com/golang/term).


Updates `golang.org/x/term` from 0.39.0 to 0.40.0
- [Commits](https://github.com/golang/term/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 02:43:11 +00:00
166 changed files with 2935 additions and 4941 deletions

View File

@@ -16,7 +16,7 @@ COPY . .
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
npm run set:$DATABASE && \
npm run set:$BUILD && \
npm run db:$DATABASE:generate && \
npm run db:generate && \
npm run build && \
npm run build:cli

View File

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

View File

@@ -1,14 +0,0 @@
import { defineConfig } from "drizzle-kit";
import path from "path";
const schema = [path.join("server", "db", "pg", "schema")];
export default defineConfig({
dialect: "postgresql",
schema: schema,
out: path.join("server", "migrations"),
verbose: true,
dbCredentials: {
url: process.env.DATABASE_URL as string
}
});

View File

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

View File

@@ -1,7 +1,7 @@
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -18,8 +18,6 @@
"componentsMember": "Вие сте част от {count, plural, =0 {нула организации} one {една организация} other {# организации}}.",
"componentsInvalidKey": "Засечен е невалиден или изтекъл лиценз. Проверете лицензионните условия, за да се възползвате от всички функционалности.",
"dismiss": "Отхвърляне",
"subscriptionViolationMessage": "Превишихте ограничението на текущия си план. Коригирайте проблема, като премахнете сайтове, потребители или други ресурси, за да оставате в рамките на плана си.",
"subscriptionViolationViewBilling": "Преглед на фактурирането",
"componentsLicenseViolation": "Нарушение на лиценза: Сървърът използва {usedSites} сайта, което надвишава лицензионния лимит от {maxSites} сайта. Проверете лицензионните условия, за да се възползвате от всички функционалности.",
"componentsSupporterMessage": "Благодарим ви, че подкрепяте Pangolin като {tier}!",
"inviteErrorNotValid": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е приета или вече не е валидна.",
@@ -1406,10 +1404,10 @@
"billingUsageLimitsOverview": "Преглед на лимитите за използване",
"billingMonitorUsage": "Следете своята употреба спрямо конфигурираните лимити. Ако имате нужда от увеличаване на лимитите, моля свържете се с нас support@pangolin.net.",
"billingDataUsage": "Използване на данни",
"billingSites": "Сайтове",
"billingUsers": "Потребители",
"billingDomains": "Домейни",
"billingRemoteExitNodes": "Дистанционни възли",
"billingOnlineTime": "Време на работа на сайта",
"billingUsers": "Активни потребители",
"billingDomains": "Активни домейни",
"billingRemoteExitNodes": "Активни самостоятелно хоствани възли",
"billingNoLimitConfigured": "Няма конфигуриран лимит",
"billingEstimatedPeriod": "Очакван период на фактуриране",
"billingIncludedUsage": "Включено използване",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Неуспех при получаване на URL на портала",
"billingPortalError": "Грешка в портала",
"billingDataUsageInfo": "Таксува се за всички данни, прехвърляни през вашите защитени тунели, когато сте свързани към облака. Това включва както входящия, така и изходящия трафик за всички ваши сайтове. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Данните не се таксуват при използване на възли.",
"billingSInfo": "Колко сайта можете да използвате",
"billingUsersInfo": "Колко потребители можете да използвате",
"billingDomainInfo": "Колко домейни можете да използвате",
"billingRemoteExitNodesInfo": "Колко дистанционни възли можете да използвате",
"billingLicenseKeys": "Лицензионни ключове",
"billingLicenseKeysDescription": "Управлявайте вашите абонаменти за лицензионни ключове",
"billingLicenseSubscription": "Абонамент за лиценз",
"billingInactive": "Неактивен",
"billingLicenseItem": "Лицензионен елемент",
"billingQuantity": "Количество",
"billingTotal": "общо",
"billingModifyLicenses": "Промяна на абонамента за лиценз",
"billingOnlineTimeInfo": "Таксува се на база колко време вашите сайтове остават свързани с облака. Пример: 44,640 минути се равняват на един сайт работещ 24/7 за цял месец. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Времето не се таксува при използване на възли.",
"billingUsersInfo": "Таксува се всеки потребител в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните потребителски акаунти във вашата организация.",
"billingDomainInfo": "Таксува се всеки домейн в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните домейн акаунти във вашата организация.",
"billingRemoteExitNodesInfo": "Таксува се всеки управляван възел в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните управлявани възли във вашата организация.",
"domainNotFound": "Домейнът не е намерен",
"domainNotFoundDescription": "Този ресурс е деактивиран, защото домейнът вече не съществува в нашата система. Моля, задайте нов домейн за този ресурс.",
"failed": "Неуспешно",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Номерът на порта е задължителен за не-HTTP ресурси",
"resourcePortNotAllowed": "Номерът на порта не трябва да бъде задаван за HTTP ресурси",
"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": {
"IAgreeToThe": "Съгласен съм с",
"termsOfService": "условията за ползване",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Назад към стандартния вход.",
"orgAuthNoAccount": "Нямате профил?",
"subscriptionRequiredToUse": "Необходим е абонамент, за да използвате тази функция.",
"mustUpgradeToUse": "Трябва да повишите своя абонамент, за да използвате тази функция.",
"subscriptionRequiredTierToUse": "Тази функция изисква <tierLink>{tier}</tierLink> или по-висок план.",
"upgradeToTierToUse": "Повишете до <tierLink>{tier}</tierLink> или по-висок план, за да използвате тази функция.",
"subscriptionTierTier1": "Домашен",
"subscriptionTierTier2": "Екип",
"subscriptionTierTier3": "Бизнес",
"subscriptionTierEnterprise": "Предприятие",
"idpDisabled": "Доставчиците на идентичност са деактивирани.",
"orgAuthPageDisabled": "Страницата за удостоверяване на организацията е деактивирана.",
"domainRestartedDescription": "Проверка на домейна е рестартирана успешно",
@@ -2156,32 +2113,6 @@
}
}
},
"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": "Приоритет",
"priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.",
"instanceName": "Име на инстанция",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация",
"accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация",
"licenseRequiredToUse": "Необходим е лиценз Enterprise, за да се използва тази функция.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> се изисква за използване на тази функция.",
"certResolver": "Решавач на сертификати",
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
"selectCertResolver": "Изберете решавач на сертификати",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Активирана защитна стена.",
"autoUpdatesEnabled": "Активирани автоматични актуализации.",
"tpmAvailable": "TPM е на разположение.",
"windowsAntivirusEnabled": "Активирана антивирусна програма",
"macosSipEnabled": "Protection на системната цялост (SIP).",
"macosGatekeeperEnabled": "Gatekeeper.",
"macosFirewallStealthMode": "Скрит режим на защитната стена.",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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á.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Využití dat",
"billingSites": "Stránky",
"billingUsers": "Uživatelé",
"billingDomains": "Domény",
"billingRemoteExitNodes": "Vzdálené uzly",
"billingOnlineTime": "Stránka online čas",
"billingUsers": "Aktivní uživatelé",
"billingDomains": "Aktivní domény",
"billingRemoteExitNodes": "Aktivní Samostatně hostované uzly",
"billingNoLimitConfigured": "Žádný limit nenastaven",
"billingEstimatedPeriod": "Odhadované období fakturace",
"billingIncludedUsage": "Zahrnuto využití",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Nepodařilo se získat URL 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ů.",
"billingSInfo": "Kolik stránek můžete použít",
"billingUsersInfo": "Kolik uživatelů můžete použít",
"billingDomainInfo": "Kolik domén můžete použít",
"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",
"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ů.",
"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.",
"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.",
"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.",
"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.",
"failed": "Selhalo",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Pro neHTTP zdroje je vyžadováno číslo portu",
"resourcePortNotAllowed": "Číslo portu by nemělo být nastaveno pro HTTP zdroje",
"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": {
"IAgreeToThe": "Souhlasím s",
"termsOfService": "podmínky služby",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Zpět ke standardnímu přihlášení",
"orgAuthNoAccount": "Nemáte účet?",
"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.",
"orgAuthPageDisabled": "Ověřovací stránka organizace je zakázána.",
"domainRestartedDescription": "Ověření domény bylo úspěšně restartováno",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Zobrazit historii akcí provedených 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í.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> je vyžadována pro použití této funkce.",
"certResolver": "Oddělovač certifikátů",
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
"selectCertResolver": "Vyberte řešič certifikátů",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Firewall povolen",
"autoUpdatesEnabled": "Automatické aktualizace povoleny",
"tpmAvailable": "TPM k dispozici",
"windowsAntivirusEnabled": "Antivirus povolen",
"macosSipEnabled": "Ochrana systémové integrity (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Režim neviditelnosti firewallu",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1149,7 +1147,7 @@
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
"actionCreateClient": "Client erstellen",
"actionDeleteClient": "Client löschen",
"actionArchiveClient": "Client archivieren",
"actionArchiveClient": "Kunde archivieren",
"actionUnarchiveClient": "Client dearchivieren",
"actionBlockClient": "Klient sperren",
"actionUnblockClient": "Client entsperren",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Datenverbrauch",
"billingSites": "Seiten",
"billingUsers": "Benutzergeräte",
"billingDomains": "Domänen",
"billingRemoteExitNodes": "Entfernte Knoten",
"billingOnlineTime": "Online-Zeit der Seite",
"billingUsers": "Aktive Benutzer",
"billingDomains": "Aktive Domains",
"billingRemoteExitNodes": "Aktive selbstgehostete Nodes",
"billingNoLimitConfigured": "Kein Limit konfiguriert",
"billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum",
"billingIncludedUsage": "Inklusive Nutzung",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Fehler beim Abrufen der Portal-URL",
"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.",
"billingSInfo": "Anzahl der Sites die Sie verwenden können",
"billingUsersInfo": "Wie viele Benutzer Sie verwenden können",
"billingDomainInfo": "Wie viele Domains Sie verwenden können",
"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",
"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.",
"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.",
"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.",
"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.",
"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.",
"failed": "Fehlgeschlagen",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich",
"resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden",
"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": {
"IAgreeToThe": "Ich stimme den",
"termsOfService": "Nutzungsbedingungen zu",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Zurück zum Standard Login",
"orgAuthNoAccount": "Sie haben noch kein Konto?",
"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.",
"orgAuthPageDisabled": "Organisations-Authentifizierungsseite ist deaktiviert.",
"domainRestartedDescription": "Domain-Verifizierung erfolgreich neu gestartet",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen",
"accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen",
"licenseRequiredToUse": "Um diese Funktion nutzen zu können, ist eine Enterprise-Lizenz erforderlich.",
"ossEnterpriseEditionRequired": "Die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> wird benötigt, um diese Funktion nutzen zu können.",
"certResolver": "Zertifikatsauflöser",
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
"selectCertResolver": "Zertifikatsauflöser auswählen",
@@ -2525,10 +2455,10 @@
"errorUnarchivingDevice": "Fehler beim Entarchivieren des Geräts",
"failedToUnarchiveDevice": "Fehler beim Entfernen des Geräts",
"unarchive": "Archivieren",
"archiveClient": "Client archivieren",
"archiveClient": "Kunde archivieren",
"archiveClientQuestion": "Sind Sie sicher, dass Sie diesen Client archivieren möchten?",
"archiveClientMessage": "Der Client wird archiviert und aus der Liste Ihrer aktiven Clients entfernt.",
"archiveClientConfirm": "Client archivieren",
"archiveClientConfirm": "Kunde archivieren",
"blockClient": "Klient sperren",
"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.",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Firewall aktiviert",
"autoUpdatesEnabled": "Automatische Updates aktiviert",
"tpmAvailable": "TPM verfügbar",
"windowsAntivirusEnabled": "Antivirus aktiviert",
"macosSipEnabled": "Schutz der Systemintegrität (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Stealth-Modus",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -57,7 +55,7 @@
"siteDescription": "Create and manage sites to enable connectivity to private networks",
"sitesBannerTitle": "Connect Any Network",
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
"sitesBannerButtonText": "Install Site Connector",
"sitesBannerButtonText": "Install Site",
"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.",
"approvalsBannerButtonText": "Learn More",
@@ -81,8 +79,8 @@
"siteConfirmCopy": "I have copied the config",
"searchSitesProgress": "Search sites...",
"siteAdd": "Add Site",
"siteInstallNewt": "Install Site",
"siteInstallNewtDescription": "Install the site connector for your system",
"siteInstallNewt": "Install Newt",
"siteInstallNewtDescription": "Get Newt running on your system",
"WgConfiguration": "WireGuard Configuration",
"WgConfigurationDescription": "Use the following configuration to connect to the network",
"operatingSystem": "Operating System",
@@ -1406,10 +1404,10 @@
"billingUsageLimitsOverview": "Usage Limits Overview",
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
"billingDataUsage": "Data Usage",
"billingSites": "Sites",
"billingUsers": "Users",
"billingDomains": "Domains",
"billingRemoteExitNodes": "Remote Nodes",
"billingOnlineTime": "Site Online Time",
"billingUsers": "Active Users",
"billingDomains": "Active Domains",
"billingRemoteExitNodes": "Active Self-hosted Nodes",
"billingNoLimitConfigured": "No limit configured",
"billingEstimatedPeriod": "Estimated Billing Period",
"billingIncludedUsage": "Included Usage",
@@ -1434,10 +1432,10 @@
"billingFailedToGetPortalUrl": "Failed to get portal URL",
"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.",
"billingSInfo": "How many sites you can use",
"billingUsersInfo": "How many users you can use",
"billingDomainInfo": "How many domains you can use",
"billingRemoteExitNodesInfo": "How many remote nodes you can use",
"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.",
"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.",
"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.",
"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.",
"billingLicenseKeys": "License Keys",
"billingLicenseKeysDescription": "Manage your license key subscriptions",
"billingLicenseSubscription": "License Subscription",
@@ -1446,6 +1444,7 @@
"billingQuantity": "Quantity",
"billingTotal": "total",
"billingModifyLicenses": "Modify License Subscription",
"billingPricingCalculatorLink": "View Pricing Calculator",
"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.",
"failed": "Failed",
@@ -1522,32 +1521,6 @@
"resourcePortRequired": "Port number is required for non-HTTP resources",
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
"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": {
"IAgreeToThe": "I agree to the",
"termsOfService": "terms of service",
@@ -1572,8 +1545,8 @@
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
"selectSites": "Select sites",
"sitesDescription": "The client will have connectivity to the selected sites",
"clientInstallOlm": "Install Machine Client",
"clientInstallOlmDescription": "Install the machine client for your system",
"clientInstallOlm": "Install Olm",
"clientInstallOlmDescription": "Get Olm running on your system",
"clientOlmCredentials": "Credentials",
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
"olmEndpoint": "Endpoint",
@@ -1962,13 +1935,6 @@
"orgAuthBackToSignIn": "Back to standard sign in",
"orgAuthNoAccount": "Don't have an account?",
"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.",
"orgAuthPageDisabled": "Organization auth page is disabled.",
"domainRestartedDescription": "Domain verification restarted successfully",
@@ -2281,7 +2247,6 @@
"actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization",
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature.",
"certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Uso de datos",
"billingSites": "Sitios",
"billingUsers": "Usuarios",
"billingDomains": "Dominios",
"billingRemoteExitNodes": "Nodos remotos",
"billingOnlineTime": "Tiempo en línea del sitio",
"billingUsers": "Usuarios activos",
"billingDomains": "Dominios activos",
"billingRemoteExitNodes": "Nodos autogestionados activos",
"billingNoLimitConfigured": "No se ha configurado ningún límite",
"billingEstimatedPeriod": "Período de facturación estimado",
"billingIncludedUsage": "Uso incluido",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Error al obtener la URL 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.",
"billingSInfo": "Cuántos sitios puedes usar",
"billingUsersInfo": "Cuántos usuarios puedes usar",
"billingDomainInfo": "Cuántos dominios puedes usar",
"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",
"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.",
"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.",
"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.",
"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.",
"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.",
"failed": "Fallido",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Se requiere número de puerto para recursos no HTTP",
"resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP",
"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": {
"IAgreeToThe": "Estoy de acuerdo con los",
"termsOfService": "términos del servicio",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Volver a iniciar sesión estándar",
"orgAuthNoAccount": "¿No tienes una cuenta?",
"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.",
"orgAuthPageDisabled": "La página de autenticación de la organización está deshabilitada.",
"domainRestartedDescription": "Verificación de dominio reiniciada con éxito",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Ver un historial de acciones realizadas en 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.",
"ossEnterpriseEditionRequired": "La <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> es necesaria para utilizar esta función.",
"certResolver": "Resolver certificado",
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
"selectCertResolver": "Seleccionar Resolver Certificado",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Cortafuegos activado",
"autoUpdatesEnabled": "Actualizaciones automáticas habilitadas",
"tpmAvailable": "TPM disponible",
"windowsAntivirusEnabled": "Antivirus activado",
"macosSipEnabled": "Protección de integridad del sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Sigilo Firewall",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Utilisation des données",
"billingSites": "Nœuds",
"billingUsers": "Utilisateurs",
"billingDomains": "Domaines",
"billingRemoteExitNodes": "Nœuds distants",
"billingOnlineTime": "Temps en ligne du site",
"billingUsers": "Utilisateurs actifs",
"billingDomains": "Domaines actifs",
"billingRemoteExitNodes": "Nœuds auto-hébergés actifs",
"billingNoLimitConfigured": "Aucune limite configurée",
"billingEstimatedPeriod": "Période de facturation estimée",
"billingIncludedUsage": "Utilisation incluse",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Échec pour obtenir l'URL 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.",
"billingSInfo": "Combien de sites vous pouvez utiliser",
"billingUsersInfo": "Combien d'utilisateurs vous pouvez utiliser",
"billingDomainInfo": "Combien de domaines vous pouvez utiliser",
"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",
"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.",
"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.",
"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.",
"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.",
"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.",
"failed": "Échec",
@@ -1522,32 +1512,6 @@
"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",
"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": {
"IAgreeToThe": "Je suis d'accord avec",
"termsOfService": "les conditions d'utilisation",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Retour à la connexion standard",
"orgAuthNoAccount": "Vous n'avez pas de compte ?",
"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.",
"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",
@@ -2156,32 +2113,6 @@
}
}
},
"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é",
"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",
@@ -2281,7 +2212,6 @@
"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",
"licenseRequiredToUse": "Une licence Entreprise est nécessaire pour utiliser cette fonctionnalité.",
"ossEnterpriseEditionRequired": "La version <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> est requise pour utiliser cette fonctionnalité.",
"certResolver": "Résolveur de certificat",
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
"selectCertResolver": "Sélectionnez le résolveur de certificat",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Pare-feu activé",
"autoUpdatesEnabled": "Mises à jour automatiques activées",
"tpmAvailable": "TPM disponible",
"windowsAntivirusEnabled": "Antivirus activé",
"macosSipEnabled": "Protection contre l'intégrité du système (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Mode furtif du pare-feu",

View File

@@ -18,8 +18,6 @@
"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à.",
"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à.",
"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.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Utilizzo dei Dati",
"billingSites": "Siti",
"billingUsers": "Utenti",
"billingDomains": "Domini",
"billingRemoteExitNodes": "Nodi Remoti",
"billingOnlineTime": "Tempo Online del Sito",
"billingUsers": "Utenti Attivi",
"billingDomains": "Domini Attivi",
"billingRemoteExitNodes": "Nodi Self-hosted Attivi",
"billingNoLimitConfigured": "Nessun limite configurato",
"billingEstimatedPeriod": "Periodo di Fatturazione Stimato",
"billingIncludedUsage": "Utilizzo Incluso",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Errore durante l'ottenimento dell'URL 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.",
"billingSInfo": "Quanti siti puoi usare",
"billingUsersInfo": "Quanti utenti puoi usare",
"billingDomainInfo": "Quanti domini puoi usare",
"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",
"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.",
"billingUsersInfo": "Sei addebitato per ogni utente nell'organizzazione. La fatturazione viene calcolata quotidianamente in base al numero di account utente attivi nel tuo org.",
"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.",
"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.",
"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.",
"failed": "Fallito",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP",
"resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP",
"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": {
"IAgreeToThe": "Accetto i",
"termsOfService": "termini di servizio",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Torna alla modalità di accesso standard",
"orgAuthNoAccount": "Non hai un account?",
"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.",
"orgAuthPageDisabled": "La pagina di autenticazione dell'organizzazione è disabilitata.",
"domainRestartedDescription": "Verifica del dominio riavviata con successo",
@@ -2156,32 +2113,6 @@
}
}
},
"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à",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Visualizza una cronologia delle azioni eseguite 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.",
"ossEnterpriseEditionRequired": "L' <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> è necessaria per utilizzare questa funzione.",
"certResolver": "Risolutore Di Certificato",
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
"selectCertResolver": "Seleziona Risolutore Di Certificato",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Firewall Abilitato",
"autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati",
"tpmAvailable": "TPM Disponibile",
"windowsAntivirusEnabled": "Antivirus Abilitato",
"macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo Del Firewall",

View File

@@ -18,8 +18,6 @@
"componentsMember": "당신은 {count, plural, =0 {조직이 없습니다} one {하나의 조직} other {# 개의 조직}}의 구성원입니다.",
"componentsInvalidKey": "유효하지 않거나 만료된 라이센스 키가 감지되었습니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.",
"dismiss": "해제",
"subscriptionViolationMessage": "현재 계획의 한계를 초과했습니다. 사이트, 사용자 또는 기타 리소스를 제거하여 계획 내에 머물도록 해결하세요.",
"subscriptionViolationViewBilling": "청구 보기",
"componentsLicenseViolation": "라이센스 위반: 이 서버는 {usedSites} 사이트를 사용하고 있으며, 이는 {maxSites} 사이트의 라이센스 한도를 초과합니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.",
"componentsSupporterMessage": "{tier}로 판골린을 지원해 주셔서 감사합니다!",
"inviteErrorNotValid": "죄송하지만, 접근하려는 초대가 수락되지 않았거나 더 이상 유효하지 않은 것 같습니다.",
@@ -1406,10 +1404,10 @@
"billingUsageLimitsOverview": "사용 한도 개요",
"billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@pangolin.net로 연락하십시오.",
"billingDataUsage": "데이터 사용량",
"billingSites": "사이트",
"billingUsers": "사용자",
"billingDomains": "도메인",
"billingRemoteExitNodes": "원격 노드",
"billingOnlineTime": "사이트 온라인 시간",
"billingUsers": "활성 사용자",
"billingDomains": "활성 도메인",
"billingRemoteExitNodes": "활성 자체 호스팅 노드",
"billingNoLimitConfigured": "구성된 한도가 없습니다.",
"billingEstimatedPeriod": "예상 청구 기간",
"billingIncludedUsage": "포함 사용량",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "포털 URL을 가져오는 데 실패했습니다.",
"billingPortalError": "포털 오류",
"billingDataUsageInfo": "클라우드에 연결할 때 보안 터널을 통해 전송된 모든 데이터에 대해 비용이 청구됩니다. 여기에는 모든 사이트의 들어오고 나가는 트래픽이 포함됩니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용하는 경우 데이터는 요금이 청구되지 않습니다.",
"billingSInfo": "사용할 수 있는 사이트 수",
"billingUsersInfo": "사용할 수 있는 사용자 수",
"billingDomainInfo": "사용할 수 있는 도메인 수",
"billingRemoteExitNodesInfo": "사용할 수 있는 원격 노드 수",
"billingLicenseKeys": "라이센스 키",
"billingLicenseKeysDescription": "라이센스 키 구독을 관리하세요",
"billingLicenseSubscription": "라이센스 구독",
"billingInactive": "비활성화됨",
"billingLicenseItem": "라이센스 항목",
"billingQuantity": "수량",
"billingTotal": "총계",
"billingModifyLicenses": "라이센스 구독 수정",
"billingOnlineTimeInfo": "사이트가 클라우드에 연결된 시간에 따라 요금이 청구됩니다. 예를 들어, 44,640분은 사이트가 한 달 내내 24시간 작동하는 것과 같습니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용할 때 시간은 요금이 청구되지 않습니다.",
"billingUsersInfo": "조직의 사용자마다 요금이 청구됩니다. 청구는 조직의 활성 사용자 계정 수에 따라 매일 계산됩니다.",
"billingDomainInfo": "조직의 도메인마다 요금이 청구됩니다. 청구는 조직의 활성 도메인 계정 수에 따라 매일 계산됩니다.",
"billingRemoteExitNodesInfo": "조직의 관리 노드마다 요금이 청구됩니다. 청구는 조직의 활성 관리 노드 수에 따라 매일 계산됩니다.",
"domainNotFound": "도메인을 찾을 수 없습니다",
"domainNotFoundDescription": "이 리소스는 도메인이 더 이상 시스템에 존재하지 않아 비활성화되었습니다. 이 리소스에 대한 새 도메인을 설정하세요.",
"failed": "실패",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다",
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요",
"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": {
"IAgreeToThe": "동의합니다",
"termsOfService": "서비스 약관",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "표준 로그인을 통해 돌아가기",
"orgAuthNoAccount": "계정이 없으신가요?",
"subscriptionRequiredToUse": "이 기능을 사용하려면 구독이 필요합니다.",
"mustUpgradeToUse": "이 기능을 사용하려면 구독을 업그레이드해야 합니다.",
"subscriptionRequiredTierToUse": "이 기능을 사용하려면 <tierLink>{tier}</tierLink> 이상의 등급이 필요합니다.",
"upgradeToTierToUse": "이 기능을 사용하려면 <tierLink>{tier}</tierLink> 이상으로 업그레이드하세요.",
"subscriptionTierTier1": "홈",
"subscriptionTierTier2": "팀",
"subscriptionTierTier3": "비즈니스",
"subscriptionTierEnterprise": "기업",
"idpDisabled": "신원 공급자가 비활성화되었습니다.",
"orgAuthPageDisabled": "조직 인증 페이지가 비활성화되었습니다.",
"domainRestartedDescription": "도메인 인증이 성공적으로 재시작되었습니다.",
@@ -2156,32 +2113,6 @@
}
}
},
"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": "우선순위",
"priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.",
"instanceName": "인스턴스 이름",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다",
"accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다",
"licenseRequiredToUse": "이 기능을 사용하려면 Enterprise 라이선스가 필요합니다.",
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink>이 필요합니다.",
"certResolver": "인증서 해결사",
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
"selectCertResolver": "인증서 해결사 선택",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "방화벽 활성화",
"autoUpdatesEnabled": "자동 업데이트 활성화",
"tpmAvailable": "TPM 사용 가능",
"windowsAntivirusEnabled": "안티바이러스 활성화됨",
"macosSipEnabled": "시스템 무결성 보호 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "방화벽 스텔스 모드",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1406,10 +1404,10 @@
"billingUsageLimitsOverview": "Oversikt over bruksgrenser",
"billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@pangolin.net.",
"billingDataUsage": "Databruk",
"billingSites": "Områder",
"billingUsers": "Brukere",
"billingDomains": "Domener",
"billingRemoteExitNodes": "Eksterne Noder",
"billingOnlineTime": "Online tid for nettsteder",
"billingUsers": "Aktive brukere",
"billingDomains": "Aktive domener",
"billingRemoteExitNodes": "Aktive selvstyrte noder",
"billingNoLimitConfigured": "Ingen grense konfigurert",
"billingEstimatedPeriod": "Estimert faktureringsperiode",
"billingIncludedUsage": "Inkludert Bruk",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Mislyktes å hente portal URL",
"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.",
"billingSInfo": "Hvor mange nettsteder du kan bruke",
"billingUsersInfo": "Hvor mange brukere du kan bruke",
"billingDomainInfo": "Hvor mange domener du kan bruke",
"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",
"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.",
"billingUsersInfo": "Du lades for hver bruker i organisasjonen. Fakturering beregnes daglig basert på antall aktive brukerkontoer i dine org.",
"billingDomainInfo": "Du lades for hvert domene i organisasjonen. Fakturering beregnes daglig basert på antallet aktive domenekontoer i din org.",
"billingRemoteExitNodesInfo": "Du lades for hver håndterte node i organisasjonen. Fakturering beregnes daglig basert på antallet aktive håndterte noder i dine org.",
"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.",
"failed": "Mislyktes",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser",
"resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser",
"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": {
"IAgreeToThe": "Jeg godtar",
"termsOfService": "brukervilkårene",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Tilbake til standard innlogging",
"orgAuthNoAccount": "Har du ikke konto?",
"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.",
"orgAuthPageDisabled": "Informasjons-siden for organisasjon er deaktivert.",
"domainRestartedDescription": "Domene-verifiseringen ble startet på nytt",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen",
"accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen",
"licenseRequiredToUse": "En Enterprise lisens er påkrevd for å bruke denne funksjonen.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen.",
"certResolver": "Sertifikat løser",
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
"selectCertResolver": "Velg sertifikatløser",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Brannmur aktivert",
"autoUpdatesEnabled": "Automatiske oppdateringer aktivert",
"tpmAvailable": "TPM tilgjengelig",
"windowsAntivirusEnabled": "Antivirus aktivert",
"macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Brannmur Usynlig Modus",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Gegevensgebruik",
"billingSites": "Sites",
"billingUsers": "Gebruikers",
"billingDomains": "Domeinen",
"billingRemoteExitNodes": "Externe knooppunten",
"billingOnlineTime": "Site Online Tijd",
"billingUsers": "Actieve Gebruikers",
"billingDomains": "Actieve Domeinen",
"billingRemoteExitNodes": "Actieve Zelfgehoste Nodes",
"billingNoLimitConfigured": "Geen limiet ingesteld",
"billingEstimatedPeriod": "Geschatte Facturatie Periode",
"billingIncludedUsage": "Opgenomen Gebruik",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Niet gelukt om portal URL te krijgen",
"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.",
"billingSInfo": "Hoeveel sites u kunt gebruiken",
"billingUsersInfo": "Hoeveel gebruikers je kan gebruiken",
"billingDomainInfo": "Hoeveel domeinen je kunt gebruiken",
"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",
"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.",
"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.",
"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.",
"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.",
"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.",
"failed": "Mislukt",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen",
"resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen",
"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": {
"IAgreeToThe": "Ik ga akkoord met de",
"termsOfService": "servicevoorwaarden",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Terug naar standaard aanmelden",
"orgAuthNoAccount": "Nog geen account?",
"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.",
"orgAuthPageDisabled": "Pagina voor organisatie-authenticatie is uitgeschakeld.",
"domainRestartedDescription": "Domeinverificatie met succes opnieuw gestart",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie",
"accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken",
"licenseRequiredToUse": "Een Enterprise-licentie is vereist om deze functie te gebruiken.",
"ossEnterpriseEditionRequired": "De <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is vereist om deze functie te gebruiken.",
"certResolver": "Certificaat Resolver",
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
"selectCertResolver": "Certificaat Resolver selecteren",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Firewall ingeschakeld",
"autoUpdatesEnabled": "Auto Updates Ingeschakeld",
"tpmAvailable": "TPM beschikbaar",
"windowsAntivirusEnabled": "Antivirus ingeschakeld",
"macosSipEnabled": "Systeemintegriteitsbescherming (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Verberg Modus",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Użycie danych",
"billingSites": "Witryny",
"billingUsers": "Użytkownicy",
"billingDomains": "Domeny",
"billingRemoteExitNodes": "Zdalne węzły",
"billingOnlineTime": "Czas Online Strony",
"billingUsers": "Aktywni użytkownicy",
"billingDomains": "Aktywne domeny",
"billingRemoteExitNodes": "Aktywne samodzielnie-hostowane węzły",
"billingNoLimitConfigured": "Nie skonfigurowano limitu",
"billingEstimatedPeriod": "Szacowany Okres Rozliczeniowy",
"billingIncludedUsage": "Zawarte użycie",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Nie udało się uzyskać adresu URL 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.",
"billingSInfo": "Ile stron możesz użyć",
"billingUsersInfo": "Ile użytkowników możesz użyć",
"billingDomainInfo": "Ile domen możesz użyć",
"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",
"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.",
"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.",
"billingDomainInfo": "Opłata za każdą domenę w organizacji. Płatność jest obliczana codziennie na podstawie liczby aktywnych kont domen w Twojej organizacji.",
"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.",
"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.",
"failed": "Niepowodzenie",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP",
"resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP",
"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": {
"IAgreeToThe": "Zgadzam się z",
"termsOfService": "warunkami usługi",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Powrót do standardowego logowania",
"orgAuthNoAccount": "Nie masz konta?",
"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",
"orgAuthPageDisabled": "Strona autoryzacji organizacji jest wyłączona.",
"domainRestartedDescription": "Weryfikacja domeny zrestartowana pomyślnie",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Zobacz historię działań wykonywanych 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.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> jest wymagany do korzystania z tej funkcji.",
"certResolver": "Rozwiązywanie certyfikatów",
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
"selectCertResolver": "Wybierz Resolver certyfikatów",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Zapora włączona",
"autoUpdatesEnabled": "Automatyczne aktualizacje włączone",
"tpmAvailable": "TPM dostępne",
"windowsAntivirusEnabled": "Antywirus włączony",
"macosSipEnabled": "Ochrona integralności systemu (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Tryb Stealth zapory",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Uso de Dados",
"billingSites": "sites",
"billingUsers": "Utilizadores",
"billingDomains": "Domínios",
"billingRemoteExitNodes": "Nós remotos",
"billingOnlineTime": "Tempo Online do Site",
"billingUsers": "Usuários Ativos",
"billingDomains": "Domínios Ativos",
"billingRemoteExitNodes": "Nodos Auto-Hospedados Ativos",
"billingNoLimitConfigured": "Nenhum limite configurado",
"billingEstimatedPeriod": "Período Estimado de Cobrança",
"billingIncludedUsage": "Uso Incluído",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Falha ao obter URL 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.",
"billingSInfo": "Quantos sites você pode usar",
"billingUsersInfo": "Quantos usuários você pode usar",
"billingDomainInfo": "Quantos domínios você pode usar",
"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",
"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.",
"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.",
"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.",
"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.",
"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.",
"failed": "Falhou",
@@ -1522,32 +1512,6 @@
"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",
"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": {
"IAgreeToThe": "Concordo com",
"termsOfService": "os termos de serviço",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Voltar para entrada padrão",
"orgAuthNoAccount": "Não possui uma conta?",
"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.",
"orgAuthPageDisabled": "A página de autenticação da organização está desativada.",
"domainRestartedDescription": "Verificação de domínio reiniciado com sucesso",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Visualizar histórico de ações realizadas 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.",
"ossEnterpriseEditionRequired": "O <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> é necessário para usar este recurso.",
"certResolver": "Resolvedor de Certificado",
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
"selectCertResolver": "Selecionar solucionador de certificado",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Firewall habilitado",
"autoUpdatesEnabled": "Atualizações Automáticas Habilitadas",
"tpmAvailable": "TPM disponível",
"windowsAntivirusEnabled": "Antivírus habilitado",
"macosSipEnabled": "Proteção da Integridade do Sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo do Firewall",

View File

@@ -18,8 +18,6 @@
"componentsMember": "Вы состоите в {count, plural, =0 {0 организациях} one {# организации} few {# организациях} many {# организациях} other {# организациях}}.",
"componentsInvalidKey": "Обнаружены недействительные или просроченные лицензионные ключи. Соблюдайте условия лицензии для использования всех функций.",
"dismiss": "Отменить",
"subscriptionViolationMessage": "Вы превысили лимиты для вашего текущего плана. Исправьте проблему, удалив сайты, пользователей или другие ресурсы, чтобы остаться в пределах вашего плана.",
"subscriptionViolationViewBilling": "Просмотр биллинга",
"componentsLicenseViolation": "Нарушение лицензии: Сервер использует {usedSites} сайтов, что превышает лицензионный лимит в {maxSites} сайтов. Соблюдайте условия лицензии для использования всех функций.",
"componentsSupporterMessage": "Спасибо за поддержку Pangolin в качестве {tier}!",
"inviteErrorNotValid": "Извините, но это приглашение не было принято или срок его действия истёк.",
@@ -1406,10 +1404,10 @@
"billingUsageLimitsOverview": "Обзор лимитов использования",
"billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@pangolin.net.",
"billingDataUsage": "Использование данных",
"billingSites": "Сайты",
"billingUsers": "Пользователи",
"billingDomains": "Домены",
"billingRemoteExitNodes": "Удаленные узлы",
"billingOnlineTime": "Время работы сайта",
"billingUsers": "Активные пользователи",
"billingDomains": "Активные домены",
"billingRemoteExitNodes": "Активные самоуправляемые узлы",
"billingNoLimitConfigured": "Лимит не установлен",
"billingEstimatedPeriod": "Предполагаемый период выставления счетов",
"billingIncludedUsage": "Включенное использование",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Не удалось получить URL-адрес портала",
"billingPortalError": "Ошибка портала",
"billingDataUsageInfo": "Вы несете ответственность за все данные, переданные через безопасные туннели при подключении к облаку. Это включает как входящий, так и исходящий трафик на всех ваших сайтах. При достижении лимита ваши сайты будут отключаться до тех пор, пока вы не обновите план или не уменьшите его использование. При использовании узлов не взимается плата.",
"billingSInfo": "Сколько сайтов вы можете использовать",
"billingUsersInfo": "Сколько пользователей вы можете использовать",
"billingDomainInfo": "Сколько доменов вы можете использовать",
"billingRemoteExitNodesInfo": "Сколько удаленных узлов вы можете использовать",
"billingLicenseKeys": "Лицензионные ключи",
"billingLicenseKeysDescription": "Управление подписками на лицензионные ключи",
"billingLicenseSubscription": "Лицензионное соглашение",
"billingInactive": "Неактивный",
"billingLicenseItem": "Элемент лицензии",
"billingQuantity": "Количество",
"billingTotal": "итого",
"billingModifyLicenses": "Изменить лицензию подписки",
"billingOnlineTimeInfo": "Вы тарифицируете на то, как долго ваши сайты будут подключены к облаку. Например, 44 640 минут равны одному сайту, работающему круглосуточно за весь месяц. Когда вы достигните лимита, ваши сайты будут отключаться до тех пор, пока вы не обновите тарифный план или не сократите нагрузку. При использовании узлов не тарифицируется.",
"billingUsersInfo": "Вы оплачиваете за каждого пользователя в организации. Платеж рассчитывается ежедневно в зависимости от количества активных учетных записей в вашем органе.",
"billingDomainInfo": "Вы платите за каждый домен в организации. Платеж рассчитывается ежедневно в зависимости от количества активных доменных аккаунтов в вашем органе.",
"billingRemoteExitNodesInfo": "Вы платите за каждый управляемый узел организации. Платёж рассчитывается ежедневно на основе количества активных управляемых узлов в вашем органе.",
"domainNotFound": "Домен не найден",
"domainNotFoundDescription": "Этот ресурс отключен, так как домен больше не существует в нашей системе. Пожалуйста, установите новый домен для этого ресурса.",
"failed": "Ошибка",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "Номер порта необходим для не-HTTP ресурсов",
"resourcePortNotAllowed": "Номер порта не должен быть установлен для HTTP ресурсов",
"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": {
"IAgreeToThe": "Я согласен с",
"termsOfService": "условия использования",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Вернуться к стандартному входу",
"orgAuthNoAccount": "Нет учётной записи?",
"subscriptionRequiredToUse": "Для использования этой функции требуется подписка.",
"mustUpgradeToUse": "Вы должны обновить подписку, чтобы использовать эту функцию.",
"subscriptionRequiredTierToUse": "Эта функция требует <tierLink>{tier}</tierLink> или выше.",
"upgradeToTierToUse": "Обновитесь до <tierLink>{tier}</tierLink> или выше, чтобы использовать эту функцию.",
"subscriptionTierTier1": "Главная",
"subscriptionTierTier2": "Команда",
"subscriptionTierTier3": "Бизнес",
"subscriptionTierEnterprise": "Предприятие",
"idpDisabled": "Провайдеры идентификации отключены.",
"orgAuthPageDisabled": "Страница авторизации организации отключена.",
"domainRestartedDescription": "Проверка домена успешно перезапущена",
@@ -2156,32 +2113,6 @@
}
}
},
"newPricingLicenseForm": {
"title": "Получить лицензию",
"description": "Выберите план и расскажите нам, как вы планируете использовать Панголин.",
"chooseTier": "Выберите ваш план",
"viewPricingLink": "Смотрите цены, возможности и ограничения",
"tiers": {
"starter": {
"title": "Старт",
"description": "Функции предприятия, 25 пользователей, 25 сайтов, и поддержка сообщества."
},
"scale": {
"title": "Масштаб",
"description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка."
}
},
"personalUseOnly": "Только для личного пользования (бесплатная лицензия — без оформления)",
"buttons": {
"continueToCheckout": "Продолжить оформление заказа"
},
"toasts": {
"checkoutError": {
"title": "Ошибка оформления заказа",
"description": "Не удалось начать оформление заказа. Пожалуйста, попробуйте еще раз."
}
}
},
"priority": "Приоритет",
"priorityDescription": "Маршруты с более высоким приоритетом оцениваются первым. Приоритет = 100 означает автоматическое упорядочение (решение системы). Используйте другой номер для обеспечения ручного приоритета.",
"instanceName": "Имя экземпляра",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Просмотр истории действий, выполненных в этой организации",
"accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации",
"licenseRequiredToUse": "Для использования этой функции требуется лицензия предприятия.",
"ossEnterpriseEditionRequired": "Для использования этой функции требуется корпоративная версия <enterpriseEditionLink></enterpriseEditionLink>.",
"certResolver": "Резольвер сертификата",
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
"selectCertResolver": "Выберите резолвер сертификата",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Брандмауэр включен",
"autoUpdatesEnabled": "Автоматические обновления включены",
"tpmAvailable": "Доступно TPM",
"windowsAntivirusEnabled": "Антивирус включен",
"macosSipEnabled": "Защита целостности системы (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Стилс-режим брандмауэра",

View File

@@ -18,8 +18,6 @@
"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.",
"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.",
"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.",
@@ -1406,10 +1404,10 @@
"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.",
"billingDataUsage": "Veri Kullanımı",
"billingSites": "Siteler",
"billingUsers": "Kullanıcılar",
"billingDomains": "Alan Adları",
"billingRemoteExitNodes": "Uzak Düğümler",
"billingOnlineTime": "Site Çevrimiçi Süresi",
"billingUsers": "Aktif Kullanıcılar",
"billingDomains": "Aktif Alanlar",
"billingRemoteExitNodes": "Aktif Öz-Host Düğümleri",
"billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı",
"billingEstimatedPeriod": "Tahmini Fatura Dönemi",
"billingIncludedUsage": "Dahil Kullanım",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "Portal URL'si alınamadı",
"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.",
"billingSInfo": "Kaç tane site kullanabileceğiniz",
"billingUsersInfo": "Kaç tane kullanıcı kullanabileceğiniz",
"billingDomainInfo": "Kaç tane alan adı kullanabileceğiniz",
"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",
"billingOnlineTimeInfo": "Sitelerinizin buluta ne kadar süre bağlı kaldığına göre ücretlendirilirsiniz. Örneğin, 44,640 dakika, bir sitenin 24/7 boyunca tam bir ay boyunca çalışması anlamına gelir. Limitinize ulaştığınızda, planınızı yükseltmeyip kullanımı azaltmazsanız siteleriniz bağlantıyı keser. Düğümler kullanırken zamandan ücret alınmaz.",
"billingUsersInfo": "Kuruluşunuzdaki her kullanıcı için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif kullanıcı hesaplarının sayısına göre günlük olarak hesaplanır.",
"billingDomainInfo": "Kuruluşunuzdaki her alan adı için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif alan adları hesaplarının sayısına göre günlük olarak hesaplanır.",
"billingRemoteExitNodesInfo": "Kuruluşunuzdaki her yönetilen Düğüm için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif yönetilen Düğümler sayısına göre günlük olarak hesaplanır.",
"domainNotFound": "Alan Adı Bulunamadı",
"domainNotFoundDescription": "Bu kaynak devre dışıdır çünkü alan adı sistemimizde artık mevcut değil. Bu kaynak için yeni bir alan adı belirleyin.",
"failed": "Başarısız",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir",
"resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı",
"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": {
"IAgreeToThe": "Kabul ediyorum",
"termsOfService": "hizmet şartları",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "Standart girişe geri dön",
"orgAuthNoAccount": "Hesabınız yok mu?",
"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.",
"orgAuthPageDisabled": "Kuruluş kimlik doğrulama sayfası devre dışı bırakılmıştır.",
"domainRestartedDescription": "Alan doğrulaması başarıyla yeniden başlatıldı",
@@ -2156,32 +2113,6 @@
}
}
},
"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",
"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",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin",
"accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin",
"licenseRequiredToUse": "Bu özelliği kullanmak için bir kurumsal lisans gereklidir.",
"ossEnterpriseEditionRequired": "Bu özelliği kullanmak için <enterpriseEditionLink>Kurumsal Sürüm</enterpriseEditionLink> gereklidir.",
"certResolver": "Sertifika Çözücü",
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
"selectCertResolver": "Sertifika Çözücü Seçin",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "Güvenlik Duvarı Etkin",
"autoUpdatesEnabled": "Otomatik Güncellemeler Etkin",
"tpmAvailable": "TPM Mevcut",
"windowsAntivirusEnabled": "Antivirüs Etkinleştirildi",
"macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu",

View File

@@ -18,8 +18,6 @@
"componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。",
"componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。",
"dismiss": "忽略",
"subscriptionViolationMessage": "您的当前计划超出了您的限制。通过移除站点、用户或其他资源以保持在您的计划范围内来纠正问题。",
"subscriptionViolationViewBilling": "查看计费",
"componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。",
"componentsSupporterMessage": "感谢您的支持!您现在是 Pangolin 的 {tier} 用户。",
"inviteErrorNotValid": "很抱歉,但看起来你试图访问的邀请尚未被接受或不再有效。",
@@ -1406,10 +1404,10 @@
"billingUsageLimitsOverview": "使用限制概览",
"billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@pangolin.net。",
"billingDataUsage": "数据使用情况",
"billingSites": "站点",
"billingUsers": "用户",
"billingDomains": "域",
"billingRemoteExitNodes": "远程节点",
"billingOnlineTime": "站点在线时间",
"billingUsers": "活跃用户",
"billingDomains": "活跃域",
"billingRemoteExitNodes": "活跃自托管节点",
"billingNoLimitConfigured": "未配置限制",
"billingEstimatedPeriod": "估计结算周期",
"billingIncludedUsage": "包含的使用量",
@@ -1434,18 +1432,10 @@
"billingFailedToGetPortalUrl": "无法获取门户网址",
"billingPortalError": "门户错误",
"billingDataUsageInfo": "当连接到云端时,您将为通过安全隧道传输的所有数据收取费用。 这包括您所有站点的进出流量。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取数据。",
"billingSInfo": "您可以使用多少站点",
"billingUsersInfo": "您可以使用多少用户",
"billingDomainInfo": "您可以使用多少域",
"billingRemoteExitNodesInfo": "您可以使用多少远程节点",
"billingLicenseKeys": "许可证密钥",
"billingLicenseKeysDescription": "管理您的许可证密钥订阅",
"billingLicenseSubscription": "许可订阅",
"billingInactive": "未激活",
"billingLicenseItem": "许可证项目",
"billingQuantity": "数量",
"billingTotal": "总计",
"billingModifyLicenses": "修改许可订阅",
"billingOnlineTimeInfo": "您要根据您的网站连接到云端的时间长短收取费用。 例如44,640分钟等于一个24/7全月运行的网站。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取费用。",
"billingUsersInfo": "您为组织中的每个用户收取费用。每日计费是根据您组织中活跃用户帐户的数量计算的。",
"billingDomainInfo": "您在组织中的每个域都要收取费用。每日计费是根据您组织中的活动域帐户数计算的。",
"billingRemoteExitNodesInfo": "您为组织中的每个管理节点收取费用。计费是每日根据您组织中活跃的管理节点数计算的。",
"domainNotFound": "域未找到",
"domainNotFoundDescription": "此资源已禁用,因为该域不再在我们的系统中存在。请为此资源设置一个新域。",
"failed": "失败",
@@ -1522,32 +1512,6 @@
"resourcePortRequired": "非 HTTP 资源必须输入端口号",
"resourcePortNotAllowed": "HTTP 资源不应设置端口号",
"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": {
"IAgreeToThe": "我同意",
"termsOfService": "服务条款",
@@ -1962,13 +1926,6 @@
"orgAuthBackToSignIn": "返回标准登录",
"orgAuthNoAccount": "没有账户?",
"subscriptionRequiredToUse": "需要订阅才能使用此功能。",
"mustUpgradeToUse": "您必须升级您的订阅才能使用此功能。",
"subscriptionRequiredTierToUse": "此功能需要 <tierLink>{tier}</tierLink> 或更高级别。",
"upgradeToTierToUse": "升级到 <tierLink>{tier}</tierLink> 或更高级别以使用此功能。",
"subscriptionTierTier1": "首页",
"subscriptionTierTier2": "团队",
"subscriptionTierTier3": "业务",
"subscriptionTierEnterprise": "企业",
"idpDisabled": "身份提供者已禁用。",
"orgAuthPageDisabled": "组织认证页面已禁用。",
"domainRestartedDescription": "域验证重新启动成功",
@@ -2156,32 +2113,6 @@
}
}
},
"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": "优先权",
"priorityDescription": "先评估更高优先级线路。优先级 = 100意味着自动排序(系统决定). 使用另一个数字强制执行手动优先级。",
"instanceName": "实例名称",
@@ -2281,7 +2212,6 @@
"actionLogsDescription": "查看此机构执行的操作历史",
"accessLogsDescription": "查看此机构资源的访问认证请求",
"licenseRequiredToUse": "需要企业许可证才能使用此功能。",
"ossEnterpriseEditionRequired": "需要 <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> 才能使用此功能。",
"certResolver": "证书解决器",
"certResolverDescription": "选择用于此资源的证书解析器。",
"selectCertResolver": "选择证书解析",
@@ -2580,7 +2510,6 @@
"firewallEnabled": "防火墙已启用",
"autoUpdatesEnabled": "启用自动更新",
"tpmAvailable": "TPM 可用",
"windowsAntivirusEnabled": "抗病毒已启用",
"macosSipEnabled": "系统完整性保护 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "防火墙隐形模式",

View File

@@ -14,12 +14,10 @@
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
"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",
"db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
"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:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json",

View File

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

View File

@@ -70,9 +70,7 @@ export const subscriptions = sqliteTable("subscriptions", {
canceledAt: integer("canceledAt"),
createdAt: integer("createdAt").notNull(),
updatedAt: integer("updatedAt"),
version: integer("version"),
billingCycleAnchor: integer("billingCycleAnchor"),
type: text("type") // tier1, tier2, tier3, or license
billingCycleAnchor: integer("billingCycleAnchor")
});
export const subscriptionItems = sqliteTable("subscriptionItems", {
@@ -86,7 +84,6 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
}),
planId: text("planId").notNull(),
priceId: text("priceId"),
featureId: text("featureId"),
meterId: text("meterId"),
unitAmount: real("unitAmount"),
tiers: text("tiers"),
@@ -129,7 +126,6 @@ export const limits = sqliteTable("limits", {
})
.notNull(),
value: real("value"),
override: integer("override", { mode: "boolean" }).default(false),
description: text("description")
});

View File

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

View File

@@ -1,41 +1,30 @@
import Stripe from "stripe";
export enum FeatureId {
SITE_UPTIME = "siteUptime",
USERS = "users",
SITES = "sites",
EGRESS_DATA_MB = "egressDataMb",
DOMAINS = "domains",
REMOTE_EXIT_NODES = "remoteExitNodes",
TIER1 = "tier1"
REMOTE_EXIT_NODES = "remoteExitNodes"
}
export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> {
switch (featureId) {
case FeatureId.USERS:
return "Users";
case FeatureId.SITES:
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.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 FeatureMeterIds: Record<FeatureId, string> = {
[FeatureId.SITE_UPTIME]: "mtr_61Srrej5wUJuiTWgo41D3Ee2Ir7WmDLU",
[FeatureId.USERS]: "mtr_61SrreISyIWpwUNGR41D3Ee2Ir7WmQro",
[FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW",
[FeatureId.DOMAINS]: "mtr_61Ss9nIKDNMw0LDRU41D3Ee2Ir7WmRPU",
[FeatureId.REMOTE_EXIT_NODES]: "mtr_61T86UXnfxTVXy9sD41D3Ee2Ir7WmFTE"
};
export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = {
// [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
export const FeatureMeterIdsSandbox: Record<FeatureId, string> = {
[FeatureId.SITE_UPTIME]: "mtr_test_61Snh3cees4w60gv841DCpkOb237BDEu",
[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 | undefined {
export function getFeatureMeterId(featureId: FeatureId): string {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
@@ -54,81 +43,45 @@ export function getFeatureIdByMetricId(
)?.[0];
}
export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
export const homeLabFeaturePriceSet: FeaturePriceSet = {
[FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
export type FeaturePriceSet = {
[key in Exclude<FeatureId, FeatureId.DOMAINS>]: string;
} & {
[FeatureId.DOMAINS]?: string; // Optional since domains are not billed
};
export const homeLabFeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
export const standardFeaturePriceSet: FeaturePriceSet = {
// Free tier matches the freeLimitSet
[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 function getHomeLabFeaturePriceSet(): FeaturePriceSet {
export const standardFeaturePriceSetSandbox: 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 (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
) {
return homeLabFeaturePriceSet;
return standardFeaturePriceSet;
} else {
return homeLabFeaturePriceSetSandbox;
return standardFeaturePriceSetSandbox;
}
}
export const tier2FeaturePriceSet: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
};
export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
};
export function getStarterFeaturePriceSet(): 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 getScaleFeaturePriceSet(): 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 = [
getHomeLabFeaturePriceSet(),
getStarterFeaturePriceSet(),
getScaleFeaturePriceSet()
];
for (const priceSet of allPriceSets) {
const entry = (Object.entries(priceSet) as [FeatureId, string][]).find(
([_, price]) => price === priceId
);
if (entry) {
return entry[0];
}
}
return undefined;
export function getLineItems(
featurePriceSet: FeaturePriceSet
): Stripe.Checkout.SessionCreateParams.LineItem[] {
return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({
price: priceId
}));
}

View File

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

View File

@@ -2,7 +2,6 @@ import { db, limits } from "@server/db";
import { and, eq } from "drizzle-orm";
import { LimitSet } from "./limitSet";
import { FeatureId } from "./features";
import logger from "@server/logger";
class LimitService {
async applyLimitSetToOrg(orgId: string, limitSet: LimitSet): Promise<void> {
@@ -14,21 +13,6 @@ class LimitService {
for (const [featureId, entry] of limitEntries) {
const limitId = `${orgId}-${featureId}`;
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
.insert(limits)
.values({ limitId, orgId, featureId, value, description });

View File

@@ -1,50 +0,0 @@
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
}
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"]
};

View File

@@ -0,0 +1,34 @@
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,6 +1,8 @@
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 {
db,
usage,
@@ -30,7 +32,11 @@ interface StripeEvent {
}
export function noop() {
if (build !== "saas") {
if (
build !== "saas" ||
!process.env.S3_BUCKET ||
!process.env.LOCAL_FILE_PATH
) {
return true;
}
return false;
@@ -38,40 +44,31 @@ export function noop() {
export class UsageService {
private bucketName: string | undefined;
private events: StripeEvent[] = [];
private lastUploadTime: number = Date.now();
private isUploading: boolean = false;
private currentEventFile: string | null = null;
private currentFileStartTime: number = 0;
private eventsDir: string | undefined;
private uploadingFiles: Set<string> = new Set();
constructor() {
if (noop()) {
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;
// this.bucketName = process.env.S3_BUCKET || undefined;
// Ensure events directory exists
this.initializeEventsDirectory().then(() => {
this.uploadPendingEventFilesOnStartup();
});
// // Periodically check and upload events
// setInterval(() => {
// this.checkAndUploadEvents().catch((err) => {
// logger.error("Error in periodic event upload:", err);
// });
// }, 30000); // every 30 seconds
// // Handle graceful shutdown on SIGTERM
// process.on("SIGTERM", async () => {
// logger.info(
// "SIGTERM received, uploading events before shutdown..."
// );
// await this.forceUpload();
// logger.info("Events uploaded, proceeding with shutdown");
// });
// // Handle SIGINT as well (Ctrl+C)
// process.on("SIGINT", async () => {
// logger.info("SIGINT received, uploading events before shutdown...");
// await this.forceUpload();
// logger.info("Events uploaded, proceeding with shutdown");
// process.exit(0);
// });
// 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
}
/**
@@ -81,6 +78,85 @@ export class UsageService {
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(
orgId: string,
featureId: FeatureId,
@@ -130,9 +206,7 @@ export class UsageService {
}
// Log event for Stripe
// if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
// await this.logStripeEvent(featureId, value, customerId);
// }
await this.logStripeEvent(featureId, value, customerId);
return usage || null;
} catch (error: any) {
@@ -212,7 +286,7 @@ export class UsageService {
return new Date(date * 1000).toISOString().split("T")[0];
}
async updateCount(
async updateDaily(
orgId: string,
featureId: FeatureId,
value?: number,
@@ -238,6 +312,8 @@ export class UsageService {
value = this.truncateValue(value);
}
const today = this.getTodayDateString();
let currentUsage: Usage | null = null;
await db.transaction(async (trx) => {
@@ -251,34 +327,66 @@ export class UsageService {
.limit(1);
if (currentUsage) {
await trx
.update(usage)
.set({
instantaneousValue: value,
updatedAt: Math.floor(Date.now() / 1000)
})
.where(eq(usage.usageId, usageId));
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
.update(usage)
.set({
latestValue: newRunningTotal,
instantaneousValue: value,
updatedAt: Math.floor(Date.now() / 1000)
})
.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 {
// First record for this meter
const meterId = getFeatureMeterId(featureId);
const truncatedValue = this.truncateValue(value || 0);
await trx.insert(usage).values({
usageId,
featureId,
orgId,
meterId,
instantaneousValue: value || 0,
latestValue: value || 0,
instantaneousValue: truncatedValue,
latestValue: truncatedValue,
updatedAt: Math.floor(Date.now() / 1000)
});
}
});
// if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
// await this.logStripeEvent(featureId, value || 0, customerId);
// }
await this.logStripeEvent(featureId, value || 0, customerId);
} catch (error) {
logger.error(
`Failed to update count usage for ${orgId}/${featureId}:`,
`Failed to update daily usage for ${orgId}/${featureId}:`,
error
);
}
@@ -342,58 +450,121 @@ export class UsageService {
}
};
this.addEventToMemory(event);
await this.checkAndUploadEvents();
await this.writeEventToFile(event);
await this.checkAndUploadFile();
}
private addEventToMemory(event: StripeEvent): void {
if (!this.bucketName) {
private async writeEventToFile(event: StripeEvent): Promise<void> {
if (!this.eventsDir || !this.bucketName) {
logger.warn(
"S3 bucket name is not configured, skipping event storage."
"Stripe local file path or bucket name is not configured, skipping event file write."
);
return;
}
this.events.push(event);
}
private async checkAndUploadEvents(): Promise<void> {
const now = Date.now();
const timeSinceLastUpload = now - this.lastUploadTime;
// Check if at least 1 minute has passed since last upload
if (timeSinceLastUpload >= 60000 && this.events.length > 0) {
await this.uploadEventsToS3();
}
}
private async uploadEventsToS3(): Promise<void> {
if (!this.bucketName) {
logger.warn(
"S3 bucket name is not configured, skipping S3 upload."
);
return;
if (!this.currentEventFile) {
this.currentEventFile = this.generateEventFileName();
this.currentFileStartTime = Date.now();
}
if (this.events.length === 0) {
return;
}
// Check if already uploading
if (this.isUploading) {
logger.debug("Already uploading events, skipping");
return;
}
this.isUploading = true;
const filePath = path.join(this.eventsDir, this.currentEventFile);
try {
// Take a snapshot of current events and clear the array
const eventsToUpload = [...this.events];
this.events = [];
this.lastUploadTime = Date.now();
let events: StripeEvent[] = [];
const fileName = this.generateEventFileName();
const fileContent = JSON.stringify(eventsToUpload, null, 2);
// 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({
@@ -405,15 +576,29 @@ export class UsageService {
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 ${eventsToUpload.length} events`
`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 events to S3:", error);
// Note: Events are lost if upload fails. In a production system,
// you might want to add the events back to the array or implement retry logic
logger.error(`Failed to upload ${fileName} to S3:`, error);
} finally {
this.isUploading = false;
// Always remove from uploading set
this.uploadingFiles.delete(fileName);
}
}
@@ -498,16 +683,129 @@ export class UsageService {
}
}
public async getUsageDaily(
orgId: string,
featureId: FeatureId
): Promise<Usage | null> {
if (noop()) {
return null;
}
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
return this.getUsage(orgId, featureId);
}
public async forceUpload(): Promise<void> {
if (this.events.length > 0) {
// Force upload regardless of time
this.lastUploadTime = 0; // Reset to force upload
await this.uploadEventsToS3();
await this.uploadFileToS3();
}
/**
* 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 {
// Remove empty file
try {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
logger.debug(
`Empty file ${file} was already deleted`
);
}
}
} finally {
// Always remove from uploading set
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(
orgId: string,
kickSites = false,
featureId?: FeatureId,
usage?: Usage,
trx: Transaction | typeof db = db
@@ -581,6 +879,58 @@ export class UsageService {
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) {
logger.error(`Error checking limits for org ${orgId}:`, error);
}

View File

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

View File

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

View File

@@ -107,11 +107,6 @@ export class Config {
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;
}

View File

@@ -182,7 +182,7 @@ export async function createUserAccountOrg(
const customerId = await createCustomer(orgId, userEmail);
if (customerId) {
await usageService.updateCount(orgId, FeatureId.USERS, 1, customerId); // Only 1 because we are crating the org
await usageService.updateDaily(orgId, FeatureId.USERS, 1, customerId); // Only 1 because we are crating the org
}
return {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
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

@@ -11,59 +11,46 @@
* 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 { db, customers, subscriptions } from "@server/db";
import { Tier } from "@server/types/Tiers";
import { eq, and, ne } from "drizzle-orm";
export async function getOrgTierData(
orgId: string
): Promise<{ tier: Tier | null; active: boolean }> {
let tier: Tier | null = null;
): Promise<{ tier: string | null; active: boolean }> {
let tier = null;
let active = false;
if (build !== "saas") {
return { tier, active };
}
try {
// Get customer for org
const [customer] = await db
.select()
.from(customers)
.where(eq(customers.orgId, orgId))
.limit(1);
// TODO: THIS IS INEFFICIENT!!! WE SHOULD IMPROVE HOW WE STORE TIERS WITH SUBSCRIPTIONS AND RETRIEVE THEM
if (customer) {
// 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);
const subscriptionsWithItems = await getOrgSubscriptionsData(orgId);
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;
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;
}
}
}
} catch (error) {
// If org not found or error occurs, return null tier and inactive
// This is acceptable behavior as per the function signature
}
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 };
}

View File

@@ -13,6 +13,8 @@
import { build } from "@server/build";
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 { eq } from "drizzle-orm";
import {
@@ -78,8 +80,6 @@ export async function checkOrgAccessPolicy(
}
}
// TODO: check that the org is subscribed
// get the needed data
if (!props.org) {

View File

@@ -125,6 +125,16 @@ export class PrivateConfig {
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) {
process.env.USE_PANGOLIN_DNS =
this.rawPrivateConfig.flags.use_pangolin_dns.toString();

View File

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

View File

@@ -1,29 +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 { 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

@@ -95,7 +95,7 @@ export const privateConfigSchema = z.object({
.object({
enable_redis: 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().default(false)
})
.optional()
.prefault({}),
@@ -176,9 +176,9 @@ export const privateConfigSchema = z.object({
.string()
.optional()
.transform(getEnvOrYaml("STRIPE_WEBHOOK_SECRET")),
// s3Bucket: z.string(),
// s3Region: z.string().default("us-east-1"),
// localFilePath: z.string().optional()
s3Bucket: z.string(),
s3Region: z.string().default("us-east-1"),
localFilePath: z.string()
})
.optional()
});

View File

@@ -16,61 +16,46 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { Tier } from "@server/types/Tiers";
export function verifyValidSubscription(tiers: Tier[]) {
return async function (
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
if (build != "saas") {
return next();
}
const orgId =
req.params.orgId ||
req.body.orgId ||
req.query.orgId ||
req.userOrgId;
if (!orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization ID is required to verify subscription"
)
);
}
const { tier, active } = await getOrgTierData(orgId);
const isTier = tiers.includes(tier as Tier);
if (!active) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Organization does not have an active subscription"
)
);
}
if (!isTier) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Organization subscription tier does not have access to this feature"
)
);
}
export async function verifyValidSubscription(
req: Request,
res: Response,
next: NextFunction
) {
try {
if (build != "saas") {
return next();
} catch (e) {
}
const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId;
if (!orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying subscription"
HttpCode.BAD_REQUEST,
"Organization ID is required to verify subscription"
)
);
}
};
const tier = await getOrgTierData(orgId);
if (!tier.active) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Organization does not have an active subscription"
)
);
}
return next();
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying subscription"
)
);
}
}

View File

@@ -19,6 +19,8 @@ import { fromError } from "zod-validation-error";
import type { Request, Response, NextFunction } from "express";
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import {
approvals,
clients,
@@ -219,6 +221,19 @@ export async function listApprovals(
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 approvalsList = await queryApprovals(
orgId.toString(),
limit,

View File

@@ -17,7 +17,10 @@ import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { build } from "@server/build";
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 { and, eq, type InferInsertModel } from "drizzle-orm";
import type { NextFunction, Request, Response } from "express";
@@ -61,6 +64,20 @@ export async function processPendingApproval(
}
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 approval = await db

View File

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

View File

@@ -0,0 +1,585 @@
/*
* 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

@@ -1,268 +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 { 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 {
getHomeLabFeaturePriceSet,
getScaleFeaturePriceSet,
getStarterFeaturePriceSet,
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 = getHomeLabFeaturePriceSet();
} else if (tier === "tier2") {
targetPriceSet = getStarterFeaturePriceSet();
} else if (tier === "tier3") {
targetPriceSet = getScaleFeaturePriceSet();
} 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,23 +22,14 @@ import logger from "@server/logger";
import config from "@server/lib/config";
import { fromError } from "zod-validation-error";
import stripe from "#private/lib/stripe";
import {
getHomeLabFeaturePriceSet,
getScaleFeaturePriceSet,
getStarterFeaturePriceSet
} from "@server/lib/billing";
import { getLineItems } from "@server/lib/billing/getLineItems";
import Stripe from "stripe";
import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing";
import { getTierPriceSet, TierId } from "@server/lib/billing/tiers";
const createCheckoutSessionSchema = z.strictObject({
orgId: z.string()
});
const createCheckoutSessionBodySchema = z.strictObject({
tier: z.enum(["tier1", "tier2", "tier3"])
});
export async function createCheckoutSession(
export async function createCheckoutSessionSAAS(
req: Request,
res: Response,
next: NextFunction
@@ -56,18 +47,6 @@ export async function createCheckoutSession(
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
const [customer] = await db
.select()
@@ -86,26 +65,20 @@ export async function createCheckoutSession(
);
}
let lineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
if (tier === "tier1") {
lineItems = await getLineItems(getHomeLabFeaturePriceSet(), orgId);
} else if (tier === "tier2") {
lineItems = await getLineItems(getStarterFeaturePriceSet(), orgId);
} else if (tier === "tier3") {
lineItems = await getLineItems(getScaleFeaturePriceSet(), orgId);
} else {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid plan"));
}
logger.debug(`Line items: ${JSON.stringify(lineItems)}`);
const standardTierPrice = getTierPriceSet()[TierId.STANDARD];
const session = await stripe!.checkout.sessions.create({
client_reference_id: orgId, // So we can look it up the org later on the webhook
billing_address_collection: "required",
line_items: lineItems,
line_items: [
{
price: standardTierPrice, // Use the standard tier
quantity: 1
},
...getLineItems(getStandardFeaturePriceSet())
], // Start with the standard feature set that matches the free limits
customer: customer.customerId,
mode: "subscription",
allow_promotion_codes: true,
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`
});

View File

@@ -1,297 +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 { 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";
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"}`
);
// 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`
);
// 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;
// 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,8 +23,6 @@ import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
// Import tables for billing
import {
@@ -72,19 +70,9 @@ export async function getOrgSubscriptions(
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, {
data: {
subscriptions,
...(build === "saas" ? { limitsExceeded } : {})
subscriptions
},
success: true,
error: false,

View File

@@ -78,10 +78,16 @@ export async function getOrgUsage(
// Get usage for org
const usageData = [];
const sites = await usageService.getUsage(orgId, FeatureId.SITES);
const users = await usageService.getUsage(orgId, FeatureId.USERS);
const domains = await usageService.getUsage(orgId, FeatureId.DOMAINS);
const remoteExitNodes = await usageService.getUsage(
const siteUptime = await usageService.getUsage(
orgId,
FeatureId.SITE_UPTIME
);
const users = await usageService.getUsageDaily(orgId, FeatureId.USERS);
const domains = await usageService.getUsageDaily(
orgId,
FeatureId.DOMAINS
);
const remoteExitNodes = await usageService.getUsageDaily(
orgId,
FeatureId.REMOTE_EXIT_NODES
);
@@ -90,8 +96,8 @@ export async function getOrgUsage(
FeatureId.EGRESS_DATA_MB
);
if (sites) {
usageData.push(sites);
if (siteUptime) {
usageData.push(siteUptime);
}
if (users) {
usageData.push(users);

View File

@@ -1,62 +1,35 @@
/*
* 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 {
getLicensePriceSet,
} from "@server/lib/billing/licenses";
import {
getHomeLabFeaturePriceSet,
getStarterFeaturePriceSet,
getScaleFeaturePriceSet,
} from "@server/lib/billing/features";
getTierPriceSet,
} from "@server/lib/billing/tiers";
import Stripe from "stripe";
import { Tier } from "@server/types/Tiers";
export type SubscriptionType = Tier | "license";
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): SubscriptionType | null {
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): "saas" | "license" {
// Determine subscription type by checking subscription items
if (!Array.isArray(fullSubscription.items?.data) || fullSubscription.items.data.length === 0) {
return null;
}
let type: "saas" | "license" = "saas";
if (Array.isArray(fullSubscription.items?.data)) {
for (const item of fullSubscription.items.data) {
const priceId = item.price.id;
for (const item of fullSubscription.items.data) {
const priceId = item.price.id;
// Check if price ID matches any license price
const licensePrices = Object.values(getLicensePriceSet());
// Check if price ID matches any license price
const licensePrices = Object.values(getLicensePriceSet());
if (licensePrices.includes(priceId)) {
return "license";
}
if (licensePrices.includes(priceId)) {
type = "license";
break;
}
// Check if price ID matches home lab tier
const homeLabPrices = Object.values(getHomeLabFeaturePriceSet());
if (homeLabPrices.includes(priceId)) {
return "tier1";
}
// Check if price ID matches any tier price (saas)
const tierPrices = Object.values(getTierPriceSet());
// Check if price ID matches tier2 tier
const tier2Prices = Object.values(getStarterFeaturePriceSet());
if (tier2Prices.includes(priceId)) {
return "tier2";
}
// Check if price ID matches tier3 tier
const tier3Prices = Object.values(getScaleFeaturePriceSet());
if (tier3Prices.includes(priceId)) {
return "tier3";
if (tierPrices.includes(priceId)) {
type = "saas";
break;
}
}
}
return null;
return type;
}

View File

@@ -31,8 +31,6 @@ import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
import { sendEmail } from "@server/emails";
import EnterpriseEditionKeyGenerated from "@server/emails/templates/EnterpriseEditionKeyGenerated";
import config from "@server/lib/config";
import { getFeatureIdByPriceId } from "@server/lib/billing/features";
import { handleTierChange } from "../featureLifecycle";
export async function handleSubscriptionCreated(
subscription: Stripe.Subscription
@@ -61,8 +59,6 @@ export async function handleSubscriptionCreated(
return;
}
const type = getSubType(fullSubscription);
const newSubscription = {
subscriptionId: subscription.id,
customerId: subscription.customer as string,
@@ -70,9 +66,7 @@ export async function handleSubscriptionCreated(
canceledAt: subscription.canceled_at
? subscription.canceled_at
: null,
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
createdAt: subscription.created
};
await db.insert(subscriptions).values(newSubscription);
@@ -93,15 +87,10 @@ export async function handleSubscriptionCreated(
name = product.name || null;
}
// Get the feature ID from the price ID
const featureId = getFeatureIdByPriceId(item.price.id);
return {
stripeSubscriptionItemId: item.id,
subscriptionId: subscription.id,
planId: item.plan.id,
priceId: item.price.id,
featureId: featureId || null,
meterId: item.plan.meter,
unitAmount: item.price.unit_amount || 0,
currentPeriodStart: item.current_period_start,
@@ -140,23 +129,17 @@ export async function handleSubscriptionCreated(
return;
}
if (type === "tier1" || type === "tier2" || type === "tier3") {
const type = getSubType(fullSubscription);
if (type === "saas") {
logger.debug(
`Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
`Handling SAAS subscription lifecycle for org ${customer.orgId}`
);
// we only need to handle the limit lifecycle for saas subscriptions not for the licenses
await handleSubscriptionLifesycle(
customer.orgId,
subscription.status,
type
subscription.status
);
// 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
.select()
.from(userOrgs)

View File

@@ -27,7 +27,6 @@ import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
import { getSubType } from "./getSubType";
import stripe from "#private/lib/stripe";
import privateConfig from "#private/lib/config";
import { handleTierChange } from "../featureLifecycle";
export async function handleSubscriptionDeleted(
subscription: Stripe.Subscription
@@ -77,23 +76,16 @@ export async function handleSubscriptionDeleted(
}
const type = getSubType(fullSubscription);
if (type == "tier1" || type == "tier2" || type == "tier3") {
if (type === "saas") {
logger.debug(
`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`
);
await handleSubscriptionLifesycle(
customer.orgId,
subscription.status,
type
subscription.status
);
// 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
.select()
.from(userOrgs)

View File

@@ -23,12 +23,11 @@ import {
} from "@server/db";
import { eq, and } from "drizzle-orm";
import logger from "@server/logger";
import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
import { getFeatureIdByMetricId } from "@server/lib/billing/features";
import stripe from "#private/lib/stripe";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
import { getSubType, SubscriptionType } from "./getSubType";
import { getSubType } from "./getSubType";
import privateConfig from "#private/lib/config";
import { handleTierChange } from "../featureLifecycle";
export async function handleSubscriptionUpdated(
subscription: Stripe.Subscription,
@@ -65,9 +64,6 @@ export async function handleSubscriptionUpdated(
.where(eq(customers.customerId, subscription.customer as string))
.limit(1);
const type = getSubType(fullSubscription);
const previousType = existingSubscription.type as SubscriptionType | null;
await db
.update(subscriptions)
.set({
@@ -76,55 +72,25 @@ export async function handleSubscriptionUpdated(
? subscription.canceled_at
: null,
updatedAt: Math.floor(Date.now() / 1000),
billingCycleAnchor: subscription.billing_cycle_anchor,
type: type
billingCycleAnchor: subscription.billing_cycle_anchor
})
.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
if (Array.isArray(fullSubscription.items?.data)) {
// 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,
planId: item.plan.id,
priceId: item.price.id,
featureId: featureId,
meterId: item.plan.meter,
unitAmount: item.price.unit_amount || 0,
currentPeriodStart: item.current_period_start,
currentPeriodEnd: item.current_period_end,
tiers: item.price.tiers
? JSON.stringify(item.price.tiers)
: null,
interval: item.plan.interval
};
});
const itemsToUpsert = fullSubscription.items.data.map((item) => ({
subscriptionId: subscription.id,
planId: item.plan.id,
priceId: item.price.id,
meterId: item.plan.meter,
unitAmount: item.price.unit_amount || 0,
currentPeriodStart: item.current_period_start,
currentPeriodEnd: item.current_period_end,
tiers: item.price.tiers
? JSON.stringify(item.price.tiers)
: null,
interval: item.plan.interval
}));
if (itemsToUpsert.length > 0) {
await db.transaction(async (trx) => {
await trx
@@ -188,7 +154,7 @@ export async function handleSubscriptionUpdated(
const orgId = customer.orgId;
if (!orgId) {
logger.debug(
logger.warn(
`No orgId found in subscription metadata for subscription ${subscription.id}. Skipping usage reset.`
);
continue;
@@ -268,29 +234,17 @@ export async function handleSubscriptionUpdated(
}
// --- end usage update ---
if (type === "tier1" || type === "tier2" || type === "tier3") {
const type = getSubType(fullSubscription);
if (type === "saas") {
logger.debug(
`Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
`Handling SAAS subscription lifecycle for org ${customer.orgId}`
);
// we only need to handle the limit lifecycle for saas subscriptions not for the licenses
await handleSubscriptionLifesycle(
customer.orgId,
subscription.status,
type
subscription.status
);
// 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") {
} else {
if (subscription.status === "canceled" || subscription.status == "unpaid" || subscription.status == "incomplete_expired") {
try {
// WARNING:

View File

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

View File

@@ -13,66 +13,38 @@
import {
freeLimitSet,
tier1LimitSet,
tier2LimitSet,
tier3LimitSet,
limitsService,
LimitSet
subscribedLimitSet
} from "@server/lib/billing";
import { usageService } from "@server/lib/billing/usageService";
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;
}
}
import logger from "@server/logger";
export async function handleSubscriptionLifesycle(
orgId: string,
status: string,
subType: SubscriptionType | null
status: string
) {
switch (status) {
case "active":
const activeLimitSet = getLimitSetForSubscriptionType(subType);
await limitsService.applyLimitSetToOrg(orgId, activeLimitSet);
await usageService.checkLimitSet(orgId);
await limitsService.applyLimitSetToOrg(orgId, subscribedLimitSet);
await usageService.checkLimitSet(orgId, true);
break;
case "canceled":
// Subscription canceled - revert to free tier
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
await usageService.checkLimitSet(orgId);
await usageService.checkLimitSet(orgId, true);
break;
case "past_due":
// Payment past due - keep current limits but notify customer
// Limits will revert to free tier if it becomes unpaid
// Optionally handle past due status, e.g., notify customer
break;
case "unpaid":
// Subscription unpaid - revert to free tier
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
await usageService.checkLimitSet(orgId);
await usageService.checkLimitSet(orgId, true);
break;
case "incomplete":
// Payment incomplete - give them time to complete payment
// Optionally handle incomplete status, e.g., notify customer
break;
case "incomplete_expired":
// Payment never completed - revert to free tier
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
await usageService.checkLimitSet(orgId);
await usageService.checkLimitSet(orgId, true);
break;
default:
break;

View File

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

View File

@@ -126,7 +126,6 @@ export async function generateNewEnterpriseLicense(
], // Start with the standard feature set that matches the free limits
customer: customer.customerId,
mode: "subscription",
allow_promotion_codes: true,
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`
});

View File

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

View File

@@ -30,7 +30,9 @@ import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { createCertificate } from "#private/routers/certificates/createCertificate";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
import { CreateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z.strictObject({
@@ -74,6 +76,19 @@ export async function createLoginPage(
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
.select()
.from(loginPageOrg)

View File

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

View File

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

View File

@@ -23,7 +23,9 @@ import { eq, and } from "drizzle-orm";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { subdomainSchema } from "@server/lib/schemas";
import { createCertificate } from "#private/routers/certificates/createCertificate";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
import { UpdateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z
@@ -85,6 +87,18 @@ export async function updateLoginPage(
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
.select()

View File

@@ -25,8 +25,10 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { eq, InferInsertModel } from "drizzle-orm";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
import config from "#private/lib/config";
import config from "@server/private/lib/config";
const paramsSchema = z.strictObject({
orgId: z.string()
@@ -126,6 +128,19 @@ export async function upsertLoginPageBranding(
const { orgId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
let updateData = parsedBody.data satisfies InferInsertModel<
typeof loginPageBranding
>;

View File

@@ -24,9 +24,10 @@ import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { 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 { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
@@ -102,19 +103,23 @@ export async function createOrgOidcIdp(
emailPath,
namePath,
name,
autoProvision,
variant,
roleMapping,
tags
} = parsedBody.data;
let { autoProvision } = parsedBody.data;
const subscribed = await isSubscribed(
orgId,
tierMatrix.deviceApprovals
);
if (!subscribed) {
autoProvision = false;
if (build === "saas") {
const { tier, active } = 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 key = config.getRawConfig().server.secret!;

View File

@@ -24,8 +24,9 @@ import { idp, idpOidcConfig } from "@server/db";
import { eq, and } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
const paramsSchema = z
.object({
@@ -108,18 +109,22 @@ export async function updateOrgOidcIdp(
emailPath,
namePath,
name,
autoProvision,
roleMapping,
tags
} = parsedBody.data;
let { autoProvision } = parsedBody.data;
const subscribed = await isSubscribed(
orgId,
tierMatrix.deviceApprovals
);
if (!subscribed) {
autoProvision = false;
if (build === "saas") {
const { tier, active } = 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."
)
);
}
}
// Check if IDP exists and is of type OIDC

View File

@@ -85,7 +85,7 @@ export async function createRemoteExitNode(
if (usage) {
const rejectRemoteExitNodes = await usageService.checkLimitSet(
orgId,
false,
FeatureId.REMOTE_EXIT_NODES,
{
...usage,
@@ -97,7 +97,7 @@ export async function createRemoteExitNode(
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Remote node limit exceeded. Please upgrade your plan."
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@pangolin.net"
)
);
}
@@ -224,7 +224,7 @@ export async function createRemoteExitNode(
});
if (numExitNodeOrgs) {
await usageService.updateCount(
await usageService.updateDaily(
orgId,
FeatureId.REMOTE_EXIT_NODES,
numExitNodeOrgs.length

View File

@@ -106,7 +106,7 @@ export async function deleteRemoteExitNode(
});
if (numExitNodeOrgs) {
await usageService.updateCount(
await usageService.updateDaily(
orgId,
FeatureId.REMOTE_EXIT_NODES,
numExitNodeOrgs.length

View File

@@ -1,6 +1,6 @@
import { db, orgs, requestAuditLog } from "@server/db";
import logger from "@server/logger";
import { and, eq, lt, sql } from "drizzle-orm";
import { and, eq, lt } from "drizzle-orm";
import cache from "@server/lib/cache";
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
import { stripPortFromHost } from "@server/lib/ip";
@@ -67,27 +67,17 @@ async function flushAuditLogs() {
const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length);
try {
// Use a transaction to ensure all inserts succeed or fail together
// This prevents index corruption from partial writes
await db.transaction(async (tx) => {
// Batch insert logs in groups of 25 to avoid overwhelming the database
const BATCH_DB_SIZE = 25;
for (let i = 0; i < logsToWrite.length; i += BATCH_DB_SIZE) {
const batch = logsToWrite.slice(i, i + BATCH_DB_SIZE);
await tx.insert(requestAuditLog).values(batch);
}
});
// Batch insert logs in groups of 25 to avoid overwhelming the database
const BATCH_DB_SIZE = 25;
for (let i = 0; i < logsToWrite.length; i += BATCH_DB_SIZE) {
const batch = logsToWrite.slice(i, i + BATCH_DB_SIZE);
await db.insert(requestAuditLog).values(batch);
}
logger.debug(`Flushed ${logsToWrite.length} audit logs to database`);
} catch (error) {
logger.error("Error flushing audit logs:", error);
// On transaction error, put logs back at the front of the buffer to retry
// but only if buffer isn't too large
if (auditLogBuffer.length < MAX_BUFFER_SIZE - logsToWrite.length) {
auditLogBuffer.unshift(...logsToWrite);
logger.info(`Re-queued ${logsToWrite.length} audit logs for retry`);
} else {
logger.error(`Buffer full, dropped ${logsToWrite.length} audit logs`);
}
// On error, we lose these logs - consider a fallback strategy if needed
// (e.g., write to file, or put back in buffer with retry limit)
} finally {
isFlushInProgress = false;
// If buffer filled up while we were flushing, flush again

View File

@@ -17,7 +17,8 @@ import {
ResourceHeaderAuthExtendedCompatibility,
ResourcePassword,
ResourcePincode,
ResourceRule
ResourceRule,
resourceSessions
} from "@server/db";
import config from "@server/lib/config";
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
@@ -31,6 +32,7 @@ import { fromError } from "zod-validation-error";
import { getCountryCodeForIp } from "@server/lib/geoip";
import { getAsnForIp } from "@server/lib/asn";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { verifyPassword } from "@server/auth/password";
import {
checkOrgAccessPolicy,
@@ -38,9 +40,8 @@ import {
} from "#dynamic/lib/checkOrgAccessPolicy";
import { logRequestAudit } from "./logRequestAudit";
import cache from "@server/lib/cache";
import semver from "semver";
import { APP_VERSION } from "@server/lib/consts";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string(), z.string()).optional(),
@@ -797,11 +798,8 @@ async function notAllowed(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const subscribed = await isSubscribed(
orgId,
tierMatrix.loginPageDomain
);
if (subscribed) {
const { tier } = await getOrgTierData(orgId); // returns null in oss
if (tier === TierId.STANDARD) {
loginPage = await getOrgLoginPage(orgId);
}
}
@@ -854,8 +852,8 @@ async function headerAuthChallenged(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain);
if (subscribed) {
const { tier } = await getOrgTierData(orgId); // returns null in oss
if (tier === TierId.STANDARD) {
loginPage = await getOrgLoginPage(orgId);
}
}
@@ -1041,11 +1039,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
const MAX_RECURSION_DEPTH = 100;
// Recursive function to try different wildcard matches
function matchSegments(
patternIndex: number,
pathIndex: number,
depth: number = 0
): boolean {
function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean {
// Check recursion depth limit
if (depth > MAX_RECURSION_DEPTH) {
logger.warn(
@@ -1131,11 +1125,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
);
return matchSegments(
patternIndex + 1,
pathIndex + 1,
depth + 1
);
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
}
logger.debug(

View File

@@ -2,8 +2,6 @@ import { Limit, Subscription, SubscriptionItem, Usage } from "@server/db";
export type GetOrgSubscriptionResponse = {
subscriptions: Array<{ subscription: Subscription; items: SubscriptionItem[] }>;
/** When build === saas, true if org has exceeded plan limits (sites, users, etc.) */
limitsExceeded?: boolean;
};
export type GetOrgUsageResponse = {

View File

@@ -101,7 +101,7 @@ export async function createClient(
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid subnet format. Please provide a valid IP."
"Invalid subnet format. Please provide a valid CIDR notation."
)
);
}

View File

@@ -13,7 +13,6 @@ import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const getClientSchema = z.strictObject({
clientId: z
@@ -57,29 +56,19 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
}
type PostureData = {
biometricsEnabled?: boolean | null | "-";
diskEncrypted?: boolean | null | "-";
firewallEnabled?: boolean | null | "-";
autoUpdatesEnabled?: boolean | null | "-";
tpmAvailable?: boolean | null | "-";
windowsAntivirusEnabled?: boolean | null | "-";
macosSipEnabled?: boolean | null | "-";
macosGatekeeperEnabled?: boolean | null | "-";
macosFirewallStealthMode?: boolean | null | "-";
linuxAppArmorEnabled?: boolean | null | "-";
linuxSELinuxEnabled?: boolean | null | "-";
biometricsEnabled?: boolean | null;
diskEncrypted?: boolean | null;
firewallEnabled?: boolean | null;
autoUpdatesEnabled?: boolean | null;
tpmAvailable?: boolean | null;
windowsAntivirusEnabled?: boolean | null;
macosSipEnabled?: boolean | null;
macosGatekeeperEnabled?: boolean | null;
macosFirewallStealthMode?: boolean | null;
linuxAppArmorEnabled?: boolean | null;
linuxSELinuxEnabled?: boolean | null;
};
function maskPostureDataWithPlaceholder(posture: PostureData): PostureData {
const masked: PostureData = {};
for (const key of Object.keys(posture) as (keyof PostureData)[]) {
if (posture[key] !== undefined && posture[key] !== null) {
(masked as Record<keyof PostureData, "-">)[key] = "-";
}
}
return masked;
}
function getPlatformPostureData(
platform: string | null | undefined,
fingerprint: typeof currentFingerprint.$inferSelect | null
@@ -295,11 +284,9 @@ export async function getClient(
);
}
const isUserDevice = client.user !== null && client.user !== undefined;
// Replace name with device name if OLM exists
let clientName = client.clients.name;
if (client.olms && isUserDevice) {
if (client.olms) {
const model = client.currentFingerprint?.deviceModel || null;
clientName = getUserDeviceName(model, client.clients.name);
}
@@ -307,35 +294,32 @@ export async function getClient(
// Build fingerprint data if available
const fingerprintData = client.currentFingerprint
? {
username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null,
kernelVersion:
client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null
}
username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null,
kernelVersion:
client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null
}
: null;
// Build posture data if available (platform-specific)
// Licensed: real values; not licensed: same keys but values set to "-"
const rawPosture = getPlatformPostureData(
client.currentFingerprint?.platform || null,
client.currentFingerprint
);
// Only return posture data if org is licensed/subscribed
let postureData: PostureData | null = null;
const isOrgLicensed = await isLicensedOrSubscribed(
client.clients.orgId,
tierMatrix.devicePosture
client.clients.orgId
);
const postureData: PostureData | null = rawPosture
? isOrgLicensed
? rawPosture
: maskPostureDataWithPlaceholder(rawPosture)
: null;
if (isOrgLicensed) {
postureData = getPlatformPostureData(
client.currentFingerprint?.platform || null,
client.currentFingerprint
);
}
const data: GetClientResponse = {
...client.clients,

View File

@@ -320,10 +320,7 @@ export async function listClients(
// Merge clients with their site associations and replace name with device name
const clientsWithSites = clientsList.map((client) => {
const model = client.deviceModel || null;
let newName = client.name;
if (filter === "user") {
newName = getUserDeviceName(model, client.name);
}
const newName = getUserDeviceName(model, client.name);
return {
...client,
name: newName,

View File

@@ -131,7 +131,7 @@ export async function createOrgDomain(
}
const rejectDomains = await usageService.checkLimitSet(
orgId,
false,
FeatureId.DOMAINS,
{
...usage,
@@ -354,7 +354,7 @@ export async function createOrgDomain(
});
if (numOrgDomains) {
await usageService.updateCount(
await usageService.updateDaily(
orgId,
FeatureId.DOMAINS,
numOrgDomains.length

View File

@@ -86,7 +86,7 @@ export async function deleteAccountDomain(
});
if (numOrgDomains) {
await usageService.updateCount(
await usageService.updateDaily(
orgId,
FeatureId.DOMAINS,
numOrgDomains.length

View File

@@ -41,8 +41,7 @@ import {
verifyUserHasAction,
verifyUserIsOrgOwner,
verifySiteResourceAccess,
verifyOlmAccess,
verifyLimits
verifyOlmAccess
} from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions";
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
@@ -80,7 +79,6 @@ authenticated.get(
authenticated.post(
"/org/:orgId",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateOrg),
logActionAudit(ActionsEnum.updateOrg),
org.updateOrg
@@ -163,7 +161,6 @@ authenticated.get(
authenticated.put(
"/org/:orgId/client",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createClient),
logActionAudit(ActionsEnum.createClient),
client.createClient
@@ -181,7 +178,6 @@ authenticated.delete(
authenticated.post(
"/client/:clientId/archive",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.archiveClient),
logActionAudit(ActionsEnum.archiveClient),
client.archiveClient
@@ -190,7 +186,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unarchive",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.unarchiveClient),
logActionAudit(ActionsEnum.unarchiveClient),
client.unarchiveClient
@@ -199,7 +194,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId/block",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.blockClient),
logActionAudit(ActionsEnum.blockClient),
client.blockClient
@@ -208,7 +202,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unblock",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.unblockClient),
logActionAudit(ActionsEnum.unblockClient),
client.unblockClient
@@ -217,7 +210,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId",
verifyClientAccess, // this will check if the user has access to the client
verifyLimits,
verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client
logActionAudit(ActionsEnum.updateClient),
client.updateClient
@@ -232,7 +224,6 @@ authenticated.post(
authenticated.post(
"/site/:siteId",
verifySiteAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateSite),
logActionAudit(ActionsEnum.updateSite),
site.updateSite
@@ -282,7 +273,6 @@ authenticated.get(
authenticated.put(
"/org/:orgId/site-resource",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createSiteResource),
logActionAudit(ActionsEnum.createSiteResource),
siteResource.createSiteResource
@@ -313,7 +303,6 @@ authenticated.get(
authenticated.post(
"/site-resource/:siteResourceId",
verifySiteResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateSiteResource),
logActionAudit(ActionsEnum.updateSiteResource),
siteResource.updateSiteResource
@@ -352,7 +341,6 @@ authenticated.post(
"/site-resource/:siteResourceId/roles",
verifySiteResourceAccess,
verifyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.setSiteResourceRoles
@@ -362,7 +350,6 @@ authenticated.post(
"/site-resource/:siteResourceId/users",
verifySiteResourceAccess,
verifySetResourceUsers,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceUsers
@@ -372,7 +359,6 @@ authenticated.post(
"/site-resource/:siteResourceId/clients",
verifySiteResourceAccess,
verifySetResourceClients,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceClients
@@ -382,7 +368,6 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/add",
verifySiteResourceAccess,
verifySetResourceClients,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.addClientToSiteResource
@@ -392,7 +377,6 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/remove",
verifySiteResourceAccess,
verifySetResourceClients,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.removeClientFromSiteResource
@@ -401,7 +385,6 @@ authenticated.post(
authenticated.put(
"/org/:orgId/resource",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createResource),
logActionAudit(ActionsEnum.createResource),
resource.createResource
@@ -516,7 +499,6 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateResource),
logActionAudit(ActionsEnum.updateResource),
resource.updateResource
@@ -532,7 +514,6 @@ authenticated.delete(
authenticated.put(
"/resource/:resourceId/target",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createTarget),
logActionAudit(ActionsEnum.createTarget),
target.createTarget
@@ -547,7 +528,6 @@ authenticated.get(
authenticated.put(
"/resource/:resourceId/rule",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createResourceRule),
logActionAudit(ActionsEnum.createResourceRule),
resource.createResourceRule
@@ -561,7 +541,6 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId/rule/:ruleId",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateResourceRule),
logActionAudit(ActionsEnum.updateResourceRule),
resource.updateResourceRule
@@ -583,7 +562,6 @@ authenticated.get(
authenticated.post(
"/target/:targetId",
verifyTargetAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateTarget),
logActionAudit(ActionsEnum.updateTarget),
target.updateTarget
@@ -599,7 +577,6 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/role",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createRole),
logActionAudit(ActionsEnum.createRole),
role.createRole
@@ -614,7 +591,6 @@ authenticated.get(
authenticated.post(
"/role/:roleId",
verifyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
@@ -643,7 +619,6 @@ authenticated.post(
"/role/:roleId/add/:userId",
verifyRoleAccess,
verifyUserAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRole
@@ -653,7 +628,6 @@ authenticated.post(
"/resource/:resourceId/roles",
verifyResourceAccess,
verifyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.setResourceRoles
@@ -663,7 +637,6 @@ authenticated.post(
"/resource/:resourceId/users",
verifyResourceAccess,
verifySetResourceUsers,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.setResourceUsers
@@ -672,7 +645,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/password`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourcePassword),
logActionAudit(ActionsEnum.setResourcePassword),
resource.setResourcePassword
@@ -681,7 +653,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/pincode`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourcePincode),
logActionAudit(ActionsEnum.setResourcePincode),
resource.setResourcePincode
@@ -690,7 +661,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/header-auth`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceHeaderAuth),
logActionAudit(ActionsEnum.setResourceHeaderAuth),
resource.setResourceHeaderAuth
@@ -699,7 +669,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceWhitelist),
logActionAudit(ActionsEnum.setResourceWhitelist),
resource.setResourceWhitelist
@@ -715,7 +684,6 @@ authenticated.get(
authenticated.post(
`/resource/:resourceId/access-token`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.generateAccessToken),
logActionAudit(ActionsEnum.generateAccessToken),
accessToken.generateAccessToken
@@ -806,7 +774,6 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/user",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createOrgUser),
logActionAudit(ActionsEnum.createOrgUser),
user.createOrgUser
@@ -816,7 +783,6 @@ authenticated.post(
"/org/:orgId/user/:userId",
verifyOrgAccess,
verifyUserAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateOrgUser),
logActionAudit(ActionsEnum.updateOrgUser),
user.updateOrgUser
@@ -889,7 +855,6 @@ authenticated.post(
"/user/:userId/olm/:olmId/archive",
verifyIsLoggedInUser,
verifyOlmAccess,
verifyLimits,
olm.archiveUserOlm
);
@@ -1004,7 +969,6 @@ authenticated.post(
`/org/:orgId/api-key/:apiKeyId/actions`,
verifyOrgAccess,
verifyApiKeyAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setApiKeyActions),
logActionAudit(ActionsEnum.setApiKeyActions),
apiKeys.setApiKeyActions
@@ -1021,7 +985,6 @@ authenticated.get(
authenticated.put(
`/org/:orgId/api-key`,
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createApiKey),
logActionAudit(ActionsEnum.createApiKey),
apiKeys.createOrgApiKey
@@ -1047,7 +1010,6 @@ authenticated.get(
authenticated.put(
`/org/:orgId/domain`,
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createOrgDomain),
logActionAudit(ActionsEnum.createOrgDomain),
domain.createOrgDomain
@@ -1057,7 +1019,6 @@ authenticated.post(
`/org/:orgId/domain/:domainId/restart`,
verifyOrgAccess,
verifyDomainAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.restartOrgDomain),
logActionAudit(ActionsEnum.restartOrgDomain),
domain.restartOrgDomain
@@ -1104,7 +1065,6 @@ authenticated.get(
authenticated.put(
"/org/:orgId/blueprint",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.applyBlueprint),
blueprints.applyYAMLBlueprint
);

View File

@@ -114,6 +114,7 @@ export async function updateSiteBandwidth(
// Aggregate usage data by organization (collected outside transaction)
const orgUsageMap = new Map<string, number>();
const orgUptimeMap = new Map<string, number>();
if (activePeers.length > 0) {
// Remove any active peers from offline tracking since they're sending data
@@ -165,6 +166,14 @@ export async function updateSiteBandwidth(
updatedSite.orgId,
currentOrgUsage + totalBandwidth
);
// Add 10 seconds of uptime for each active site
const currentOrgUptime =
orgUptimeMap.get(updatedSite.orgId) || 0;
orgUptimeMap.set(
updatedSite.orgId,
currentOrgUptime + 10 / 60
);
}
} catch (error) {
logger.error(
@@ -178,9 +187,11 @@ export async function updateSiteBandwidth(
// Process usage updates outside of site update transactions
// This separates the concerns and reduces lock contention
if (calcUsageAndLimits && orgUsageMap.size > 0) {
if (calcUsageAndLimits && (orgUsageMap.size > 0 || orgUptimeMap.size > 0)) {
// Sort org IDs to ensure consistent lock ordering
const allOrgIds = [...new Set([...orgUsageMap.keys()])].sort();
const allOrgIds = [
...new Set([...orgUsageMap.keys(), ...orgUptimeMap.keys()])
].sort();
for (const orgId of allOrgIds) {
try {
@@ -197,7 +208,7 @@ export async function updateSiteBandwidth(
usageService
.checkLimitSet(
orgId,
true,
FeatureId.EGRESS_DATA_MB,
bandwidthUsage
)
@@ -209,6 +220,32 @@ export async function updateSiteBandwidth(
});
}
}
// Process uptime usage for this org
const totalUptime = orgUptimeMap.get(orgId);
if (totalUptime) {
const uptimeUsage = await usageService.add(
orgId,
FeatureId.SITE_UPTIME,
totalUptime
);
if (uptimeUsage) {
// Fire and forget - don't block on limit checking
usageService
.checkLimitSet(
orgId,
true,
FeatureId.SITE_UPTIME,
uptimeUsage
)
.catch((error: any) => {
logger.error(
`Error checking uptime limits for org ${orgId}:`,
error
);
});
}
}
} catch (error) {
logger.error(`Error processing usage for org ${orgId}:`, error);
// Continue with other orgs

View File

@@ -93,9 +93,7 @@ export async function createOidcIdp(
name,
autoProvision,
type: "oidc",
tags,
defaultOrgMapping: `'{{orgId}}'`,
defaultRoleMapping: `'Member'`
tags
})
.returning();

View File

@@ -14,8 +14,8 @@ import jsonwebtoken from "jsonwebtoken";
import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import { build } from "@server/build";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
const paramsSchema = z
.object({
@@ -113,10 +113,8 @@ export async function generateOidcUrl(
}
if (build === "saas") {
const subscribed = await isSubscribed(
orgId,
tierMatrix.orgOidc
);
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(

View File

@@ -34,8 +34,6 @@ import { FeatureId } from "@server/lib/billing";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const ensureTrailingSlash = (url: string): string => {
return url;
@@ -328,33 +326,6 @@ export async function validateOidcCallback(
.where(eq(idpOrg.idpId, existingIdp.idp.idpId))
.innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
allOrgs = idpOrgs.map((o) => o.orgs);
// TODO: when there are multiple orgs we need to do this better!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
if (allOrgs.length > 1) {
// for some reason there is more than one org
logger.error(
"More than one organization linked to this IdP. This should not happen with auto-provisioning enabled."
);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Multiple organizations linked to this IdP. Please contact support."
)
);
}
const subscribed = await isSubscribed(
allOrgs[0].orgId,
tierMatrix.autoProvisioning
);
if (subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
} else {
allOrgs = await db.select().from(orgs);
}
@@ -616,7 +587,7 @@ export async function validateOidcCallback(
});
for (const orgCount of orgUserCounts) {
await usageService.updateCount(
await usageService.updateDaily(
orgCount.orgId,
FeatureId.USERS,
orgCount.userCount

View File

@@ -26,8 +26,7 @@ import {
verifyApiKeyIsRoot,
verifyApiKeyClientAccess,
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits
verifyApiKeySetResourceClients
} from "@server/middlewares";
import HttpCode from "@server/types/HttpCode";
import { Router } from "express";
@@ -75,7 +74,6 @@ authenticated.get(
authenticated.post(
"/org/:orgId",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateOrg),
logActionAudit(ActionsEnum.updateOrg),
org.updateOrg
@@ -92,7 +90,6 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/site",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createSite),
logActionAudit(ActionsEnum.createSite),
site.createSite
@@ -129,7 +126,6 @@ authenticated.get(
authenticated.post(
"/site/:siteId",
verifyApiKeySiteAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateSite),
logActionAudit(ActionsEnum.updateSite),
site.updateSite
@@ -150,9 +146,8 @@ authenticated.get(
);
// Site Resource endpoints
authenticated.put(
"/org/:orgId/site-resource",
"/org/:orgId/private-resource",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
logActionAudit(ActionsEnum.createSiteResource),
siteResource.createSiteResource
@@ -183,7 +178,6 @@ authenticated.get(
authenticated.post(
"/site-resource/:siteResourceId",
verifyApiKeySiteResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
logActionAudit(ActionsEnum.updateSiteResource),
siteResource.updateSiteResource
@@ -222,7 +216,6 @@ authenticated.post(
"/site-resource/:siteResourceId/roles",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.setSiteResourceRoles
@@ -232,7 +225,6 @@ authenticated.post(
"/site-resource/:siteResourceId/users",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceUsers
@@ -242,7 +234,6 @@ authenticated.post(
"/site-resource/:siteResourceId/roles/add",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.addRoleToSiteResource
@@ -252,7 +243,6 @@ authenticated.post(
"/site-resource/:siteResourceId/roles/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.removeRoleFromSiteResource
@@ -262,7 +252,6 @@ authenticated.post(
"/site-resource/:siteResourceId/users/add",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.addUserToSiteResource
@@ -272,7 +261,6 @@ authenticated.post(
"/site-resource/:siteResourceId/users/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.removeUserFromSiteResource
@@ -282,7 +270,6 @@ authenticated.post(
"/site-resource/:siteResourceId/clients",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceClients
@@ -292,7 +279,6 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/add",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.addClientToSiteResource
@@ -302,7 +288,6 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.removeClientFromSiteResource
@@ -311,7 +296,6 @@ authenticated.post(
authenticated.put(
"/org/:orgId/resource",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource),
logActionAudit(ActionsEnum.createResource),
resource.createResource
@@ -320,7 +304,6 @@ authenticated.put(
authenticated.put(
"/org/:orgId/site/:siteId/resource",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource),
logActionAudit(ActionsEnum.createResource),
resource.createResource
@@ -357,7 +340,6 @@ authenticated.get(
authenticated.post(
"/org/:orgId/create-invite",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.inviteUser),
logActionAudit(ActionsEnum.inviteUser),
user.inviteUser
@@ -395,7 +377,6 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResource),
logActionAudit(ActionsEnum.updateResource),
resource.updateResource
@@ -412,7 +393,6 @@ authenticated.delete(
authenticated.put(
"/resource/:resourceId/target",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createTarget),
logActionAudit(ActionsEnum.createTarget),
target.createTarget
@@ -428,7 +408,6 @@ authenticated.get(
authenticated.put(
"/resource/:resourceId/rule",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
logActionAudit(ActionsEnum.createResourceRule),
resource.createResourceRule
@@ -444,7 +423,6 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId/rule/:ruleId",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
logActionAudit(ActionsEnum.updateResourceRule),
resource.updateResourceRule
@@ -468,7 +446,6 @@ authenticated.get(
authenticated.post(
"/target/:targetId",
verifyApiKeyTargetAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateTarget),
logActionAudit(ActionsEnum.updateTarget),
target.updateTarget
@@ -485,7 +462,6 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/role",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createRole),
logActionAudit(ActionsEnum.createRole),
role.createRole
@@ -494,7 +470,6 @@ authenticated.put(
authenticated.post(
"/role/:roleId",
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
@@ -526,7 +501,6 @@ authenticated.post(
"/role/:roleId/add/:userId",
verifyApiKeyRoleAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRole
@@ -536,7 +510,6 @@ authenticated.post(
"/resource/:resourceId/roles",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.setResourceRoles
@@ -546,7 +519,6 @@ authenticated.post(
"/resource/:resourceId/users",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.setResourceUsers
@@ -556,7 +528,6 @@ authenticated.post(
"/resource/:resourceId/roles/add",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.addRoleToResource
@@ -566,7 +537,6 @@ authenticated.post(
"/resource/:resourceId/roles/remove",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.removeRoleFromResource
@@ -576,7 +546,6 @@ authenticated.post(
"/resource/:resourceId/users/add",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.addUserToResource
@@ -586,7 +555,6 @@ authenticated.post(
"/resource/:resourceId/users/remove",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.removeUserFromResource
@@ -595,7 +563,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/password`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
logActionAudit(ActionsEnum.setResourcePassword),
resource.setResourcePassword
@@ -604,7 +571,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/pincode`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
logActionAudit(ActionsEnum.setResourcePincode),
resource.setResourcePincode
@@ -613,7 +579,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/header-auth`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
logActionAudit(ActionsEnum.setResourceHeaderAuth),
resource.setResourceHeaderAuth
@@ -622,7 +587,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
logActionAudit(ActionsEnum.setResourceWhitelist),
resource.setResourceWhitelist
@@ -631,7 +595,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist/add`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
resource.addEmailToResourceWhitelist
);
@@ -639,7 +602,6 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist/remove`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
resource.removeEmailFromResourceWhitelist
);
@@ -654,7 +616,6 @@ authenticated.get(
authenticated.post(
`/resource/:resourceId/access-token`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
logActionAudit(ActionsEnum.generateAccessToken),
accessToken.generateAccessToken
@@ -692,7 +653,6 @@ authenticated.get(
authenticated.post(
"/user/:userId/2fa",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateUser),
logActionAudit(ActionsEnum.updateUser),
user.updateUser2FA
@@ -715,7 +675,6 @@ authenticated.get(
authenticated.put(
"/org/:orgId/user",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createOrgUser),
logActionAudit(ActionsEnum.createOrgUser),
user.createOrgUser
@@ -725,7 +684,6 @@ authenticated.post(
"/org/:orgId/user/:userId",
verifyApiKeyOrgAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateOrgUser),
logActionAudit(ActionsEnum.updateOrgUser),
user.updateOrgUser
@@ -756,7 +714,6 @@ authenticated.get(
authenticated.post(
`/org/:orgId/api-key/:apiKeyId/actions`,
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setApiKeyActions),
logActionAudit(ActionsEnum.setApiKeyActions),
apiKeys.setApiKeyActions
@@ -772,7 +729,6 @@ authenticated.get(
authenticated.put(
`/org/:orgId/api-key`,
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createApiKey),
logActionAudit(ActionsEnum.createApiKey),
apiKeys.createOrgApiKey
@@ -789,7 +745,6 @@ authenticated.delete(
authenticated.put(
"/idp/oidc",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp),
idp.createOidcIdp
@@ -798,7 +753,6 @@ authenticated.put(
authenticated.post(
"/idp/:idpId/oidc",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateIdp),
logActionAudit(ActionsEnum.updateIdp),
idp.updateOidcIdp
@@ -822,7 +776,6 @@ authenticated.get(
authenticated.put(
"/idp/:idpId/org/:orgId",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createIdpOrg),
logActionAudit(ActionsEnum.createIdpOrg),
idp.createIdpOrgPolicy
@@ -831,7 +784,6 @@ authenticated.put(
authenticated.post(
"/idp/:idpId/org/:orgId",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateIdpOrg),
logActionAudit(ActionsEnum.updateIdpOrg),
idp.updateIdpOrgPolicy
@@ -876,7 +828,6 @@ authenticated.get(
authenticated.put(
"/org/:orgId/client",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createClient),
logActionAudit(ActionsEnum.createClient),
client.createClient
@@ -903,7 +854,6 @@ authenticated.delete(
authenticated.post(
"/client/:clientId/archive",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.archiveClient),
logActionAudit(ActionsEnum.archiveClient),
client.archiveClient
@@ -912,7 +862,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unarchive",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.unarchiveClient),
logActionAudit(ActionsEnum.unarchiveClient),
client.unarchiveClient
@@ -921,7 +870,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId/block",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.blockClient),
logActionAudit(ActionsEnum.blockClient),
client.blockClient
@@ -930,7 +878,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unblock",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.unblockClient),
logActionAudit(ActionsEnum.unblockClient),
client.unblockClient
@@ -939,7 +886,6 @@ authenticated.post(
authenticated.post(
"/client/:clientId",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateClient),
logActionAudit(ActionsEnum.updateClient),
client.updateClient
@@ -948,7 +894,6 @@ authenticated.post(
authenticated.put(
"/org/:orgId/blueprint",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
logActionAudit(ActionsEnum.applyBlueprint),
blueprints.applyJSONBlueprint

View File

@@ -1,13 +1,17 @@
import { db, ExitNode, newts, Transaction } from "@server/db";
import { db, ExitNode, exitNodeOrgs, newts, Transaction } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm";
import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db";
import { targetHealthCheck } from "@server/db";
import { eq, and, sql, inArray, ne } from "drizzle-orm";
import { addPeer, deletePeer } from "../gerbil/peers";
import logger from "@server/logger";
import config from "@server/lib/config";
import {
findNextAvailableCidr,
getNextAvailableClientSubnet
} from "@server/lib/ip";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import {
selectBestExitNode,
verifyExitNodeOrgAccess
@@ -26,6 +30,8 @@ export type ExitNodePingResult = {
wasPreviouslyConnected: boolean;
};
const numTimesLimitExceededForId: Record<string, number> = {};
export const handleNewtRegisterMessage: MessageHandler = async (context) => {
const { message, client, sendToClient } = context;
const newt = client as Newt;
@@ -90,6 +96,42 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
fetchContainers(newt.newtId);
}
const rejectSiteUptime = await usageService.checkLimitSet(
oldSite.orgId,
false,
FeatureId.SITE_UPTIME
);
const rejectEgressDataMb = await usageService.checkLimitSet(
oldSite.orgId,
false,
FeatureId.EGRESS_DATA_MB
);
// Do we need to check the users and domains daily limits here?
// const rejectUsers = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.USERS);
// const rejectDomains = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.DOMAINS);
// if (rejectEgressDataMb || rejectSiteUptime || rejectUsers || rejectDomains) {
if (rejectEgressDataMb || rejectSiteUptime) {
logger.info(
`Usage limits exceeded for org ${oldSite.orgId}. Rejecting newt registration.`
);
// PREVENT FURTHER REGISTRATION ATTEMPTS SO WE DON'T SPAM
// Increment the limit exceeded count for this site
numTimesLimitExceededForId[newt.newtId] =
(numTimesLimitExceededForId[newt.newtId] || 0) + 1;
if (numTimesLimitExceededForId[newt.newtId] > 15) {
logger.debug(
`Newt ${newt.newtId} has exceeded usage limits 15 times. Terminating...`
);
}
return;
}
let siteSubnet = oldSite.subnet;
let exitNodeIdToQuery = oldSite.exitNodeId;
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {

View File

@@ -117,8 +117,6 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
return;
}
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
try {
// get the client
const [client] = await db
@@ -221,9 +219,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
logger.error("Error handling ping message", { error });
}
if (isUserDevice) {
await handleFingerprintInsertion(olm, fingerprint, postures);
}
await handleFingerprintInsertion(olm, fingerprint, postures);
return {
message: {

View File

@@ -53,11 +53,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
postures
});
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
if (isUserDevice) {
await handleFingerprintInsertion(olm, fingerprint, postures);
}
await handleFingerprintInsertion(olm, fingerprint, postures);
if (
(olmVersion && olm.version !== olmVersion) ||

View File

@@ -271,7 +271,7 @@ export async function createOrg(
// make sure we have the stripe customer
const customerId = await createCustomer(orgId, req.user?.email);
if (customerId) {
await usageService.updateCount(
await usageService.updateDaily(
orgId,
FeatureId.USERS,
1,

View File

@@ -10,10 +10,10 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { cache } from "@server/lib/cache";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { getOrgTierData } from "#dynamic/lib/billing";
const updateOrgParamsSchema = z.strictObject({
orgId: z.string()
@@ -88,83 +88,26 @@ export async function updateOrg(
const { orgId } = parsedParams.data;
// Check 2FA enforcement feature
const has2FAFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix[TierFeature.TwoFactorEnforcement]
);
if (!has2FAFeature) {
const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) {
parsedBody.data.requireTwoFactor = undefined;
}
// Check session duration policies feature
const hasSessionDurationFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix[TierFeature.SessionDurationPolicies]
);
if (!hasSessionDurationFeature) {
parsedBody.data.maxSessionLengthHours = undefined;
}
// Check password expiration policies feature
const hasPasswordExpirationFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix[TierFeature.PasswordExpirationPolicies]
);
if (!hasPasswordExpirationFeature) {
parsedBody.data.passwordExpiryDays = undefined;
}
if (build == "saas") {
const { tier } = await getOrgTierData(orgId);
// Determine max allowed retention days based on tier
let maxRetentionDays: number | null = null;
if (!tier) {
maxRetentionDays = 3;
} else if (tier === "tier1") {
maxRetentionDays = 7;
} else if (tier === "tier2") {
maxRetentionDays = 30;
} else if (tier === "tier3") {
maxRetentionDays = 90;
}
// For enterprise tier, no check (maxRetentionDays remains null)
if (maxRetentionDays !== null) {
if (
parsedBody.data.settingsLogRetentionDaysRequest !== undefined &&
parsedBody.data.settingsLogRetentionDaysRequest > maxRetentionDays
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription`
)
);
}
if (
parsedBody.data.settingsLogRetentionDaysAccess !== undefined &&
parsedBody.data.settingsLogRetentionDaysAccess > maxRetentionDays
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription`
)
);
}
if (
parsedBody.data.settingsLogRetentionDaysAction !== undefined &&
parsedBody.data.settingsLogRetentionDaysAction > maxRetentionDays
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription`
)
);
}
}
const { tier } = await getOrgTierData(orgId);
if (
build == "saas" &&
tier != TierId.STANDARD &&
parsedBody.data.settingsLogRetentionDaysRequest &&
parsedBody.data.settingsLogRetentionDaysRequest > 30
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You are not allowed to set log retention days greater than 30 with your current subscription"
)
);
}
const updatedOrg = await db

View File

@@ -24,7 +24,6 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateResourceParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -342,7 +341,7 @@ async function updateHttpResource(
headers = null;
}
const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
if (!isLicensed) {
updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined;

View File

@@ -12,7 +12,6 @@ import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const createRoleParamsSchema = z.strictObject({
orgId: z.string()
@@ -101,7 +100,7 @@ export async function createRole(
);
}
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) {
roleData.requireDeviceApproval = undefined;
}

View File

@@ -10,7 +10,6 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { OpenAPITags, registry } from "@server/openApi";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateRoleParamsSchema = z.strictObject({
roleId: z.string().transform(Number).pipe(z.int().positive())
@@ -111,7 +110,7 @@ export async function updateRole(
);
}
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) {
updateData.requireDeviceApproval = undefined;
}

View File

@@ -17,9 +17,6 @@ import { hashPassword } from "@server/auth/password";
import { isValidIP } from "@server/lib/validators";
import { isIpInCidr } from "@server/lib/ip";
import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
const createSiteParamsSchema = z.strictObject({
orgId: z.string()
@@ -128,35 +125,6 @@ export async function createSite(
);
}
if (build == "saas") {
const usage = await usageService.getUsage(orgId, FeatureId.SITES);
if (!usage) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No usage data found for this organization"
)
);
}
const rejectSites = await usageService.checkLimitSet(
orgId,
FeatureId.SITES,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
} // We need to add one to know if we are violating the limit
);
if (rejectSites) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Site limit exceeded. Please upgrade your plan."
)
);
}
}
let updatedAddress = null;
if (address) {
if (!org.subnet) {
@@ -287,8 +255,8 @@ export async function createSite(
const niceId = await getUniqueSiteName(orgId);
let newSite: Site | undefined;
let numSites: Site[] | undefined;
let newSite: Site;
await db.transaction(async (trx) => {
if (type == "newt") {
[newSite] = await trx
@@ -443,35 +411,13 @@ export async function createSite(
});
}
numSites = await trx
.select()
.from(sites)
.where(eq(sites.orgId, orgId));
});
if (numSites) {
await usageService.updateCount(
orgId,
FeatureId.SITES,
numSites.length
);
}
if (!newSite) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create site"
)
);
}
return response<CreateSiteResponse>(res, {
data: newSite,
success: true,
error: false,
message: "Site created successfully",
status: HttpCode.CREATED
return response<CreateSiteResponse>(res, {
data: newSite,
success: true,
error: false,
message: "Site created successfully",
status: HttpCode.CREATED
});
});
} catch (error) {
logger.error(error);

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, Site, siteResources } from "@server/db";
import { db, siteResources } from "@server/db";
import { newts, newtSessions, sites } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
@@ -12,8 +12,6 @@ import { fromError } from "zod-validation-error";
import { sendToClient } from "#dynamic/routers/ws";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
const deleteSiteSchema = z.strictObject({
siteId: z.string().transform(Number).pipe(z.int().positive())
@@ -64,7 +62,6 @@ export async function deleteSite(
}
let deletedNewtId: string | null = null;
let numSites: Site[] | undefined;
await db.transaction(async (trx) => {
if (site.type == "wireguard") {
@@ -102,20 +99,8 @@ export async function deleteSite(
}
await trx.delete(sites).where(eq(sites.siteId, siteId));
numSites = await trx
.select()
.from(sites)
.where(eq(sites.orgId, site.orgId));
});
if (numSites) {
await usageService.updateCount(
site.orgId,
FeatureId.SITES,
numSites.length
);
}
// Send termination message outside of transaction to prevent blocking
if (deletedNewtId) {
const payload = {

View File

@@ -13,7 +13,6 @@ import { verifySession } from "@server/auth/sessions/verifySession";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { build } from "@server/build";
const acceptInviteBodySchema = z.strictObject({
token: z.string(),
@@ -93,38 +92,6 @@ export async function acceptInvite(
);
}
if (build == "saas") {
const usage = await usageService.getUsage(
existingInvite.orgId,
FeatureId.USERS
);
if (!usage) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No usage data found for this organization"
)
);
}
const rejectUsers = await usageService.checkLimitSet(
existingInvite.orgId,
FeatureId.USERS,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
} // We need to add one to know if we are violating the limit
);
if (rejectUsers) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Can not accept because this org's user limit is exceeded. Please contact your administrator to upgrade their plan."
)
);
}
}
let roleId: number;
let totalUsers: UserOrg[] | undefined;
// get the role to make sure it exists
@@ -158,21 +125,17 @@ export async function acceptInvite(
.delete(userInvites)
.where(eq(userInvites.inviteId, inviteId));
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
// Get the total number of users in the org now
totalUsers = await trx
totalUsers = await db
.select()
.from(userOrgs)
.where(eq(userOrgs.orgId, existingInvite.orgId));
logger.debug(
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}. Total users in org: ${totalUsers.length}`
);
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
});
if (totalUsers) {
await usageService.updateCount(
await usageService.updateDaily(
existingInvite.orgId,
FeatureId.USERS,
totalUsers.length

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