Compare commits

...

16 Commits

Author SHA1 Message Date
Owen Schwartz 7abef63963 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-06-20 09:41:30 -07:00
Owen Schwartz 405132f5ec New translations en-us.json (Chinese Simplified)
[ci skip]
2026-06-20 08:08:21 -07:00
Owen Schwartz 70c10f56b6 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-06-17 09:56:51 -07:00
Owen Schwartz c63f26c095 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-06-17 07:48:43 -07:00
Owen f9cc52ece9 Remove NoNewPrivileges
Fixes https://github.com/fosrl/newt/issues/383
2026-06-14 15:02:18 -07:00
Owen a08c6d70fe Comment out 2026-06-14 14:44:08 -07:00
miloschwartz a6568692b7 force set supporter status to true in server info endpoint 2026-06-14 14:40:37 -07:00
Owen a1196d3da6 Remove supporter warning 2026-06-14 14:34:39 -07:00
Owen 70bc4c0b30 Remove the path rewrite from the next route 2026-06-14 14:30:16 -07:00
Owen ea1badf4e0 Add middleware for rewriting host headers 2026-06-14 12:04:02 -07:00
Owen 4435a669a6 Fill in missing ui urls from the passed params 2026-06-14 11:35:27 -07:00
Owen 90eceb457a Clean up url passing 2026-06-14 11:10:05 -07:00
Owen f39cbc9bf4 Add same signature to oss 2026-06-14 11:03:14 -07:00
Owen 50da863bb7 Add maintence page support for remote nodes 2026-06-13 21:45:52 -07:00
Owen c6ddd5c402 Open up holepunch requirements 2026-06-13 14:14:34 -07:00
Owen 0fb5ace9c7 Support the browser gateways on the remote nodes 2026-06-13 14:08:03 -07:00
17 changed files with 319 additions and 270 deletions
+59 -59
View File
@@ -17,7 +17,7 @@
"componentsErrorNoMemberCreate": "您目前不是任何组织的成员。创建组织以开始操作。", "componentsErrorNoMemberCreate": "您目前不是任何组织的成员。创建组织以开始操作。",
"componentsErrorNoMember": "您目前不是任何组织的成员。", "componentsErrorNoMember": "您目前不是任何组织的成员。",
"welcome": "欢迎使用 Pangolin", "welcome": "欢迎使用 Pangolin",
"welcomeTo": "欢迎来到", "welcomeTo": "欢迎使用",
"componentsCreateOrg": "创建组织", "componentsCreateOrg": "创建组织",
"componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。", "componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。",
"componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。", "componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。",
@@ -35,7 +35,7 @@
"trialDaysRemaining": "{count, plural, other {# 天剩余}}", "trialDaysRemaining": "{count, plural, other {# 天剩余}}",
"trialDaysLeftShort": "试用期剩余 {days} 天", "trialDaysLeftShort": "试用期剩余 {days} 天",
"trialGoToBilling": "转到账单页面", "trialGoToBilling": "转到账单页面",
"subscriptionViolationViewBilling": "查看计费", "subscriptionViolationViewBilling": "查看账单",
"componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。", "componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。",
"componentsSupporterMessage": "感谢您的支持!您现在是 Pangolin 的 {tier} 用户。", "componentsSupporterMessage": "感谢您的支持!您现在是 Pangolin 的 {tier} 用户。",
"inviteErrorNotValid": "很抱歉,但看起来你试图访问的邀请尚未被接受或不再有效。", "inviteErrorNotValid": "很抱歉,但看起来你试图访问的邀请尚未被接受或不再有效。",
@@ -58,21 +58,21 @@
"name": "名称", "name": "名称",
"online": "在线", "online": "在线",
"offline": "离线的", "offline": "离线的",
"site": "点", "site": "点",
"dataIn": "数据输入", "dataIn": "数据输入",
"dataOut": "数据输出", "dataOut": "数据输出",
"connectionType": "连接类型", "connectionType": "连接类型",
"tunnelType": "隧道类型", "tunnelType": "隧道类型",
"local": "本地的", "local": "本地的",
"edit": "编辑", "edit": "编辑",
"siteConfirmDelete": "确认删除点", "siteConfirmDelete": "确认删除点",
"siteDelete": "删除点", "siteDelete": "删除点",
"siteMessageRemove": "一旦移除,点将无法访问。与点相关的所有目标也将被移除。", "siteMessageRemove": "一旦移除,点将无法访问。与点相关的所有目标也将被移除。",
"siteQuestionRemove": "您确定要从组织中删除该点吗?", "siteQuestionRemove": "您确定要从组织中删除该点吗?",
"siteManageSites": "管理站点", "siteManageSites": "管理站点",
"siteDescription": "创建和管理站点,启用与私人网络的连接", "siteDescription": "创建和管理站点,启用与私人网络的连接",
"sitesBannerTitle": "连接任何网络", "sitesBannerTitle": "连接任何网络",
"sitesBannerDescription": "站点是连接到远程网络的接,允许Pangolin用户提供资源访问,无论是公共还是私人。可以在任何可以运行二进制文件或容器的地方安装站点网络连接器(Newt)以建立连接。", "sitesBannerDescription": "站点是到远程网络的接,使 Pangolin 能够向任何位置的用户提公共或私有的资源访问。你可以在任何能够运行二进制文件或容器的地方安装站点网络连接器(Newt)以建立连接。",
"sitesBannerButtonText": "安装站点", "sitesBannerButtonText": "安装站点",
"approvalsBannerTitle": "批准或拒绝设备访问", "approvalsBannerTitle": "批准或拒绝设备访问",
"approvalsBannerDescription": "审核、批准或拒绝用户的设备访问请求。 当需要设备批准时,用户必须先获得管理员批准,然后他们的设备才能连接到您的组织资源。", "approvalsBannerDescription": "审核、批准或拒绝用户的设备访问请求。 当需要设备批准时,用户必须先获得管理员批准,然后他们的设备才能连接到您的组织资源。",
@@ -134,7 +134,7 @@
"siteResourcesHowToAccess": "如何访问", "siteResourcesHowToAccess": "如何访问",
"siteResourcesTargetsOnSite": "此站点上的目标", "siteResourcesTargetsOnSite": "此站点上的目标",
"siteSetting": "{siteName} 设置", "siteSetting": "{siteName} 设置",
"siteNewtTunnel": "新点 (推荐)", "siteNewtTunnel": "新点 (推荐)",
"siteNewtTunnelDescription": "最简单的方式来创建任何网络的入口。没有额外的设置。", "siteNewtTunnelDescription": "最简单的方式来创建任何网络的入口。没有额外的设置。",
"siteWg": "基本 WireGuard", "siteWg": "基本 WireGuard",
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。", "siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
@@ -143,23 +143,23 @@
"siteLocalDescriptionSaas": "仅本地资源。没有隧道。仅在远程节点上可用。", "siteLocalDescriptionSaas": "仅本地资源。没有隧道。仅在远程节点上可用。",
"siteSeeAll": "查看所有站点", "siteSeeAll": "查看所有站点",
"siteTunnelDescription": "确定如何连接到站点", "siteTunnelDescription": "确定如何连接到站点",
"siteNewtCredentials": "全权证书", "siteNewtCredentials": "凭证",
"siteNewtCredentialsDescription": "点如何通过服务器进行身份验证", "siteNewtCredentialsDescription": "点如何服务器进行身份验证",
"remoteNodeCredentialsDescription": "这是远程节点如何与服务器进行身份验证", "remoteNodeCredentialsDescription": "这是远程节点如何与服务器进行身份验证",
"siteCredentialsSave": "保存证书", "siteCredentialsSave": "保存证书",
"siteCredentialsSaveDescription": "您只能看到一次。请确保将其复制并保存到一个安全的地方。", "siteCredentialsSaveDescription": "您只能看到一次。请确保将其复制并保存到一个安全的地方。",
"siteInfo": "站点信息", "siteInfo": "站点信息",
"status": "状态", "status": "状态",
"shareTitle": "管理共享链接", "shareTitle": "管理共享链接",
"shareDescription": "创建可共享的链接,允许临时或永久访问代理资源", "shareDescription": "创建可共享的链接,允许临时或永久访问代理资源",
"shareSearch": "搜索共享链接……", "shareSearch": "搜索共享链接……",
"shareCreate": "创建共享链接", "shareCreate": "创建共享链接",
"shareErrorDelete": "删除链接失败", "shareErrorDelete": "删除链接失败",
"shareErrorDeleteMessage": "删除链接时出错", "shareErrorDeleteMessage": "删除链接时出错",
"shareDeleted": "链接已删除", "shareDeleted": "链接已删除",
"shareDeletedDescription": "链接已删除", "shareDeletedDescription": "链接已删除",
"shareDelete": "删除共享链接", "shareDelete": "删除共享链接",
"shareDeleteConfirm": "确认删除共享链接", "shareDeleteConfirm": "确认删除共享链接",
"shareQuestionRemove": "您确定要删除这个共享链接吗?", "shareQuestionRemove": "您确定要删除这个共享链接吗?",
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。", "shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。", "shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
@@ -204,11 +204,11 @@
"proxyResourceTitle": "管理公共资源", "proxyResourceTitle": "管理公共资源",
"proxyResourceDescription": "创建和管理可通过 Web 浏览器公开访问的资源", "proxyResourceDescription": "创建和管理可通过 Web 浏览器公开访问的资源",
"publicResourcesBannerTitle": "基于 Web 的公共访问", "publicResourcesBannerTitle": "基于 Web 的公共访问",
"publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可以通过网络浏览器在互联网上的任何人访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。", "publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可供互联网上的任何人通过 Web 浏览器访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。",
"clientResourceTitle": "管理私有资源", "clientResourceTitle": "管理私有资源",
"clientResourceDescription": "创建和管理只能通过连接客户端访问的资源", "clientResourceDescription": "创建和管理只能通过连接客户端访问的资源",
"privateResourcesBannerTitle": "零信任的私人访问", "privateResourcesBannerTitle": "零信任私有访问",
"privateResourcesBannerDescription": "私资源使用零信任安全,确保只允许明确授的用户和机器访问资源。可以连接用户设备或机器客户端通过安全的虚拟专用网络访问这些资源。", "privateResourcesBannerDescription": "私资源用零信任安全机制,确保只有获得明确授的用户和机器才能访问。用户设备或机器客户端连接后,即可通过安全的虚拟专用网络访问这些资源。",
"resourcesSearch": "搜索资源...", "resourcesSearch": "搜索资源...",
"resourceAdd": "添加资源", "resourceAdd": "添加资源",
"resourceErrorDelte": "删除资源时出错", "resourceErrorDelte": "删除资源时出错",
@@ -327,7 +327,7 @@
"passToAuth": "传递至认证", "passToAuth": "传递至认证",
"orgSettingsDescription": "配置组织设置", "orgSettingsDescription": "配置组织设置",
"orgGeneralSettings": "组织设置", "orgGeneralSettings": "组织设置",
"orgGeneralSettingsDescription": "管理机构的详细信息和配置", "orgGeneralSettingsDescription": "管理组织的详细信息和配置",
"saveGeneralSettings": "保存常规设置", "saveGeneralSettings": "保存常规设置",
"saveSettings": "保存设置", "saveSettings": "保存设置",
"orgDangerZone": "危险区域", "orgDangerZone": "危险区域",
@@ -381,7 +381,7 @@
"accessApprovalsDescription": "查看和管理待审批的组织访问权限", "accessApprovalsDescription": "查看和管理待审批的组织访问权限",
"description": "描述", "description": "描述",
"inviteTitle": "打开邀请", "inviteTitle": "打开邀请",
"inviteDescription": "管理其他用户加入机构的邀请", "inviteDescription": "管理其他用户加入组织的邀请",
"inviteSearch": "搜索邀请...", "inviteSearch": "搜索邀请...",
"minutes": "分钟", "minutes": "分钟",
"hours": "小时", "hours": "小时",
@@ -425,24 +425,24 @@
"apiKeysDelete": "删除 API 密钥", "apiKeysDelete": "删除 API 密钥",
"apiKeysManage": "管理 API 密钥", "apiKeysManage": "管理 API 密钥",
"apiKeysDescription": "API 密钥用于认证集成 API", "apiKeysDescription": "API 密钥用于认证集成 API",
"provisioningKeysTitle": "置备密钥", "provisioningKeysTitle": "预配密钥",
"provisioningKeysManage": "管理置备键", "provisioningKeysManage": "管理预配密钥",
"provisioningKeysDescription": "置备密钥用于验证您组织的自动站点配置。", "provisioningKeysDescription": "置备密钥用于验证您组织的自动站点配置。",
"provisioningManage": "置备中", "provisioningManage": "预配",
"provisioningDescription": "管理预配键和审查等待批准的站点。", "provisioningDescription": "管理预配密钥,并审核待批准的站点。",
"pendingSites": "待站点", "pendingSites": "待审批站点",
"siteApproveSuccess": "站点批准成功", "siteApproveSuccess": "站点批准成功",
"siteApproveError": "批准站点出错", "siteApproveError": "批准站点出错",
"provisioningKeys": "置备键", "provisioningKeys": "置备键",
"searchProvisioningKeys": "搜索配备密钥...", "searchProvisioningKeys": "搜索配备密钥...",
"provisioningKeysAdd": "生成置备键", "provisioningKeysAdd": "生成预配密钥",
"provisioningKeysErrorDelete": "删除预配键时出错", "provisioningKeysErrorDelete": "删除预配键时出错",
"provisioningKeysErrorDeleteMessage": "删除预配键时出错", "provisioningKeysErrorDeleteMessage": "删除预配键时出错",
"provisioningKeysQuestionRemove": "您确定要从组织中删除此预配键吗?", "provisioningKeysQuestionRemove": "您确定要从组织中删除此预配键吗?",
"provisioningKeysMessageRemove": "一旦移除,密钥不能再用于站点预配。", "provisioningKeysMessageRemove": "一旦移除,密钥不能再用于站点预配。",
"provisioningKeysDeleteConfirm": "确认删除置备键", "provisioningKeysDeleteConfirm": "确认删除置备键",
"provisioningKeysDelete": "删除置备键", "provisioningKeysDelete": "删除置备键",
"provisioningKeysCreate": "生成置备键", "provisioningKeysCreate": "生成预配密钥",
"provisioningKeysCreateDescription": "为组织生成一个新的预置密钥", "provisioningKeysCreateDescription": "为组织生成一个新的预置密钥",
"provisioningKeysSeeAll": "查看所有预配键", "provisioningKeysSeeAll": "查看所有预配键",
"provisioningKeysSave": "保存预配键", "provisioningKeysSave": "保存预配键",
@@ -462,16 +462,16 @@
"provisioningKeysNeverUsed": "永不过期", "provisioningKeysNeverUsed": "永不过期",
"provisioningKeysEdit": "编辑置备键", "provisioningKeysEdit": "编辑置备键",
"provisioningKeysEditDescription": "更新此密钥的最大批量大小和过期时间。", "provisioningKeysEditDescription": "更新此密钥的最大批量大小和过期时间。",
"provisioningKeysApproveNewSites": "批准新点", "provisioningKeysApproveNewSites": "批准新点",
"provisioningKeysApproveNewSitesDescription": "自动批准使用此密钥注册的点。", "provisioningKeysApproveNewSitesDescription": "自动批准使用此密钥注册的点。",
"provisioningKeysUpdateError": "更新预配键时出错", "provisioningKeysUpdateError": "更新预配键时出错",
"provisioningKeysUpdated": "置备密钥已更新", "provisioningKeysUpdated": "置备密钥已更新",
"provisioningKeysUpdatedDescription": "您的更改已保存。", "provisioningKeysUpdatedDescription": "您的更改已保存。",
"provisioningKeysBannerTitle": "站点置备密钥", "provisioningKeysBannerTitle": "站点预配密钥",
"provisioningKeysBannerDescription": "生成一个供应密钥,并将其与 Newt 连接器一起使用,在首次启动时自动创建站点 - 无需为每个站点设置单独的凭据。", "provisioningKeysBannerDescription": "生成预配密钥,并将其与 Newt 连接器配合使用,即可在首次启动时自动创建站点无需为每个站点单独配置凭据。",
"provisioningKeysBannerButtonText": "了解更多", "provisioningKeysBannerButtonText": "了解更多",
"pendingSitesBannerTitle": "待站点", "pendingSitesBannerTitle": "待审批站点",
"pendingSitesBannerDescription": "使用供应密钥连接的站点将在此显示以供审核。", "pendingSitesBannerDescription": "使用预配密钥连接的网站会在这里以供审核。",
"pendingSitesBannerButtonText": "了解更多", "pendingSitesBannerButtonText": "了解更多",
"apiKeysSettings": "{apiKeyName} 设置", "apiKeysSettings": "{apiKeyName} 设置",
"userTitle": "管理所有用户", "userTitle": "管理所有用户",
@@ -883,11 +883,11 @@
"resourcesErrorUpdateDescription": "更新资源时出错", "resourcesErrorUpdateDescription": "更新资源时出错",
"access": "访问权限", "access": "访问权限",
"accessControl": "访问控制", "accessControl": "访问控制",
"shareLink": "{resource} 共享链接", "shareLink": "{resource} 共享链接",
"resourceSelect": "选择资源", "resourceSelect": "选择资源",
"shareLinks": "共享链接", "shareLinks": "共享链接",
"share": "分享链接", "share": "分享链接",
"shareDescription2": "创建资源的共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。", "shareDescription2": "创建资源的共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。",
"shareEasyCreate": "轻松创建和分享", "shareEasyCreate": "轻松创建和分享",
"shareConfigurableExpirationDuration": "可配置的过期时间", "shareConfigurableExpirationDuration": "可配置的过期时间",
"shareSecureAndRevocable": "安全和可撤销的", "shareSecureAndRevocable": "安全和可撤销的",
@@ -1059,7 +1059,7 @@
"network": "网络", "network": "网络",
"manage": "管理", "manage": "管理",
"sitesNotFound": "未找到站点。", "sitesNotFound": "未找到站点。",
"pangolinServerAdmin": "服务器管理 - Pangolin", "pangolinServerAdmin": "服务器管理 - Pangolin",
"licenseTierProfessional": "专业许可证", "licenseTierProfessional": "专业许可证",
"licenseTierEnterprise": "企业许可证", "licenseTierEnterprise": "企业许可证",
"licenseTierPersonal": "个人许可证", "licenseTierPersonal": "个人许可证",
@@ -1366,7 +1366,7 @@
"supportKeyBuy": "购买支持者密钥", "supportKeyBuy": "购买支持者密钥",
"logoutError": "注销错误", "logoutError": "注销错误",
"signingAs": "登录为", "signingAs": "登录为",
"serverAdmin": "服务器管理", "serverAdmin": "服务器管理",
"managedSelfhosted": "托管自托管", "managedSelfhosted": "托管自托管",
"otpEnable": "启用双因子认证", "otpEnable": "启用双因子认证",
"otpDisable": "禁用双因子认证", "otpDisable": "禁用双因子认证",
@@ -1536,8 +1536,8 @@
"sidebarSites": "站点", "sidebarSites": "站点",
"sidebarApprovals": "审批请求", "sidebarApprovals": "审批请求",
"sidebarResources": "资源", "sidebarResources": "资源",
"sidebarProxyResources": "公开", "sidebarProxyResources": "公开资源",
"sidebarClientResources": "非公开的", "sidebarClientResources": "私有资源",
"sidebarPolicies": "共享策略", "sidebarPolicies": "共享策略",
"sidebarResourcePolicies": "公共资源", "sidebarResourcePolicies": "公共资源",
"sidebarAccessControl": "访问控制", "sidebarAccessControl": "访问控制",
@@ -1547,17 +1547,17 @@
"sidebarAdmin": "管理员", "sidebarAdmin": "管理员",
"sidebarInvitations": "邀请", "sidebarInvitations": "邀请",
"sidebarRoles": "角色", "sidebarRoles": "角色",
"sidebarShareableLinks": "共享链接", "sidebarShareableLinks": "共享链接",
"sidebarApiKeys": "API密钥", "sidebarApiKeys": "API密钥",
"sidebarProvisioning": "置备中", "sidebarProvisioning": "预配",
"sidebarSettings": "设置", "sidebarSettings": "设置",
"sidebarAllUsers": "所有用户", "sidebarAllUsers": "所有用户",
"sidebarIdentityProviders": "身份提供商", "sidebarIdentityProviders": "身份提供商",
"sidebarLicense": "证书", "sidebarLicense": "证书",
"sidebarClients": "客户端", "sidebarClients": "客户端",
"sidebarUserDevices": "用户设备", "sidebarUserDevices": "用户设备",
"sidebarMachineClients": "机", "sidebarMachineClients": "机器身份",
"sidebarDomains": "域", "sidebarDomains": "域",
"sidebarGeneral": "管理", "sidebarGeneral": "管理",
"sidebarLogAndAnalytics": "日志与分析", "sidebarLogAndAnalytics": "日志与分析",
"sidebarBluePrints": "蓝图", "sidebarBluePrints": "蓝图",
@@ -1689,8 +1689,8 @@
"alertingTabHealthChecks": "健康检查", "alertingTabHealthChecks": "健康检查",
"alertingRulesBannerTitle": "获取通知", "alertingRulesBannerTitle": "获取通知",
"alertingRulesBannerDescription": "每条规则都连接要监视的对象(站点、健康检查或资源),触发时间(例如离线或不健康),以及如何通过电子邮件、Webhooks 或集成将通知发送给团队。使用此列表创建、启用和管理这些规则。", "alertingRulesBannerDescription": "每条规则都连接要监视的对象(站点、健康检查或资源),触发时间(例如离线或不健康),以及如何通过电子邮件、Webhooks 或集成将通知发送给团队。使用此列表创建、启用和管理这些规则。",
"alertingHealthChecksBannerTitle": "监视健康和资源", "alertingHealthChecksBannerTitle": "资源与健康监控",
"alertingHealthChecksBannerDescription": "健康检查是您一次定义的 HTTP 或 TCP 监控。然后可以将它们用作告警规则中的来源,以便目标变得正常或不正常时得到通知。资源的健康检查也会出现在此处。", "alertingHealthChecksBannerDescription": "通过 HTTP 或 TCP 检查目标状态,并在服务异常或恢复时发送通知。资源中配置的健康检查也会显示在这里。",
"standaloneHcTableTitle": "健康检查", "standaloneHcTableTitle": "健康检查",
"standaloneHcSearchPlaceholder": "搜索健康检查…", "standaloneHcSearchPlaceholder": "搜索健康检查…",
"standaloneHcAddButton": "创建健康检查", "standaloneHcAddButton": "创建健康检查",
@@ -1791,17 +1791,17 @@
"theme": "主题", "theme": "主题",
"subnetRequired": "子网是必填项", "subnetRequired": "子网是必填项",
"initialSetupTitle": "初始服务器设置", "initialSetupTitle": "初始服务器设置",
"initialSetupDescription": "创建初始服务器管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。", "initialSetupDescription": "创建初始管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
"createAdminAccount": "创建管理员帐户", "createAdminAccount": "创建管理员帐户",
"setupErrorCreateAdmin": "创建服务器管理员账户时发生错误。", "setupErrorCreateAdmin": "创建管理员账户时发生错误。",
"certificateStatus": "证书", "certificateStatus": "证书",
"certificateStatusAutoRefreshHint": "状态自动刷新。", "certificateStatusAutoRefreshHint": "状态自动刷新。",
"loading": "加载中", "loading": "加载中",
"loadingEllipsis": "加载中……", "loadingEllipsis": "加载中……",
"loadingAnalytics": "加载分析", "loadingAnalytics": "加载分析",
"restart": "重启", "restart": "重启",
"domains": "域", "domains": "域",
"domainsDescription": "创建和管理组织中可用的域", "domainsDescription": "创建和管理组织中可用的域",
"domainsSearch": "搜索域...", "domainsSearch": "搜索域...",
"domainAdd": "添加域", "domainAdd": "添加域",
"domainAddDescription": "注册一个新域名到组织", "domainAddDescription": "注册一个新域名到组织",
@@ -2165,12 +2165,12 @@
"sshSudoMode": "Sudo 访问", "sshSudoMode": "Sudo 访问",
"sshSudoModeNone": "无", "sshSudoModeNone": "无",
"sshSudoModeNoneDescription": "用户不能用sudo运行命令。", "sshSudoModeNoneDescription": "用户不能用sudo运行命令。",
"sshSudoModeFull": "全苏多", "sshSudoModeFull": "完整 Sudo 权限",
"sshSudoModeFullDescription": "用户可以用 sudo 运行任何命令。", "sshSudoModeFullDescription": "用户可以用 sudo 运行任何命令。",
"sshSudoModeCommands": "命令", "sshSudoModeCommands": "命令",
"sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。", "sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。",
"sshSudo": "允许Sudo", "sshSudo": "允许Sudo",
"sshSudoCommands": "Sudo 命令", "sshSudoCommands": "可用 Sudo 命令",
"sshSudoCommandsDescription": "用户可以使用 sudo 运行的命令列表,以逗号、空格或新行分隔。必须使用绝对路径。", "sshSudoCommandsDescription": "用户可以使用 sudo 运行的命令列表,以逗号、空格或新行分隔。必须使用绝对路径。",
"sshCreateHomeDir": "创建主目录", "sshCreateHomeDir": "创建主目录",
"sshUnixGroups": "Unix 组", "sshUnixGroups": "Unix 组",
@@ -2183,7 +2183,7 @@
"roleTextImportAppend": "附加到现有", "roleTextImportAppend": "附加到现有",
"roleTextImportMode": "导入模式", "roleTextImportMode": "导入模式",
"roleTextImportPreview": "预览", "roleTextImportPreview": "预览",
"roleTextImportItemCount": "{count, plural, =0 {No items to import} one {1 item to import} other {# items to import}}", "roleTextImportItemCount": "{count, plural, =0 {没有可导入的项目} one {1 个可导入项目} other {# 个可导入项目}}",
"roleTextImportTotalCount": "{existing} 个现有 + {imported} 个导入 = {total} 个总计", "roleTextImportTotalCount": "{existing} 个现有 + {imported} 个导入 = {total} 个总计",
"roleTextImportConfirm": "导入", "roleTextImportConfirm": "导入",
"roleTextImportInvalidFile": "不支持的文件类型", "roleTextImportInvalidFile": "不支持的文件类型",
@@ -2235,8 +2235,8 @@
"resourceEditDomain": "编辑域名", "resourceEditDomain": "编辑域名",
"siteName": "站点名称", "siteName": "站点名称",
"proxyPort": "端口", "proxyPort": "端口",
"resourcesTableProxyResources": "公开的", "resourcesTableProxyResources": "",
"resourcesTableClientResources": "非公开的", "resourcesTableClientResources": "私有资源",
"resourcesTableNoProxyResourcesFound": "未找到代理资源。", "resourcesTableNoProxyResourcesFound": "未找到代理资源。",
"resourcesTableNoInternalResourcesFound": "未找到内部资源。", "resourcesTableNoInternalResourcesFound": "未找到内部资源。",
"resourcesTableDestination": "目标", "resourcesTableDestination": "目标",
@@ -2925,7 +2925,7 @@
"logRetentionRequestDescription": "保留请求日志的时间", "logRetentionRequestDescription": "保留请求日志的时间",
"logRetentionAccessLabel": "访问日志保留", "logRetentionAccessLabel": "访问日志保留",
"logRetentionAccessDescription": "保留访问日志的时间", "logRetentionAccessDescription": "保留访问日志的时间",
"logRetentionActionLabel": "动作日志保留", "logRetentionActionLabel": "审计日志保留",
"logRetentionActionDescription": "保留操作日志的时间", "logRetentionActionDescription": "保留操作日志的时间",
"logRetentionConnectionLabel": "连接日志保留", "logRetentionConnectionLabel": "连接日志保留",
"logRetentionConnectionDescription": "保留连接日志的时间", "logRetentionConnectionDescription": "保留连接日志的时间",
@@ -2938,11 +2938,11 @@
"logRetentionForever": "永远的", "logRetentionForever": "永远的",
"logRetentionEndOfFollowingYear": "下一年结束", "logRetentionEndOfFollowingYear": "下一年结束",
"actionLogsDescription": "查看此机构执行的操作历史", "actionLogsDescription": "查看此机构执行的操作历史",
"accessLogsDescription": "查看此机构资源的访问认证请求", "accessLogsDescription": "查看此组织资源的访问认证请求",
"connectionLogs": "连接日志", "connectionLogs": "连接日志",
"connectionLogsDescription": "查看此机构隧道的连接日志", "connectionLogsDescription": "查看此机构隧道的连接日志",
"sidebarLogsConnection": "连接日志", "sidebarLogsConnection": "连接日志",
"sidebarLogsStreaming": "流", "sidebarLogsStreaming": "事件流",
"sourceAddress": "源地址", "sourceAddress": "源地址",
"destinationAddress": "目的地址", "destinationAddress": "目的地址",
"duration": "期限", "duration": "期限",
+8 -1
View File
@@ -511,6 +511,12 @@ export class TraefikConfigManager {
let traefikConfig; let traefikConfig;
try { try {
const currentExitNode = await getCurrentExitNodeId(); const currentExitNode = await getCurrentExitNodeId();
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`); // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
traefikConfig = await getTraefikConfig( traefikConfig = await getTraefikConfig(
// this is called by the local exit node to get its own config // this is called by the local exit node to get its own config
@@ -521,7 +527,8 @@ export class TraefikConfigManager {
build == "saas" build == "saas"
? false ? false
: config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config : config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config
build != "oss" // generate browser gateway targets on cloud and enterprise pangolinUIUrl, // generate maintenance pages on cloud and hybrid
pangolinUIUrl // generate browser gateway targets on cloud and hybrid
); );
const domains = new Set<string>(); const domains = new Set<string>();
+2 -2
View File
@@ -44,8 +44,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
allowRawResources = true, allowRawResources = true,
allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE maintenancePageUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE
allowBrowserGatewayResources = true browserGatewayUiUrl: string | null = null // UNUSED BUT USED IN PRIVATE
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
+63 -29
View File
@@ -84,8 +84,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, filterOutNamespaceDomains = false,
generateLoginPageRouters = false, generateLoginPageRouters = false,
allowRawResources = true, allowRawResources = true,
allowMaintenancePage = true, maintenancePageUiUrl: string | null = null,
allowBrowserGatewayResources = true browserGatewayUiUrl: string | null = null
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
@@ -317,7 +317,7 @@ export async function getTraefikConfig(
BrowserGatewayResourceEntry BrowserGatewayResourceEntry
>(); >();
if (allowBrowserGatewayResources) { if (browserGatewayUiUrl) {
for (const row of resourcesWithTargetsAndSites) { for (const row of resourcesWithTargetsAndSites) {
if (!["ssh", "vnc", "rdp"].includes(row.mode)) { if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
continue; continue;
@@ -630,10 +630,11 @@ export async function getTraefikConfig(
} }
} }
if (showMaintenancePage && allowMaintenancePage) { if (showMaintenancePage && maintenancePageUiUrl) {
const maintenanceServiceName = `${key}-maintenance-service`; const maintenanceServiceName = `${key}-maintenance-service`;
const maintenanceRouterName = `${key}-maintenance-router`; const maintenanceRouterName = `${key}-maintenance-router`;
const rewriteMiddlewareName = `${key}-maintenance-rewrite`; const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
const maintenanceHeadersMiddlewareName = `${key}-maintenance-headers`;
const entrypointHttp = const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint; config.getRawConfig().traefik.http_entrypoint;
@@ -646,15 +647,11 @@ export async function getTraefikConfig(
? `*.${domainParts.slice(1).join(".")}` ? `*.${domainParts.slice(1).join(".")}`
: fullDomain; : fullDomain;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
config_output.http.services[maintenanceServiceName] = { config_output.http.services[maintenanceServiceName] = {
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${maintenanceHost}:${maintenancePort}` url: maintenancePageUiUrl
} }
], ],
passHostHeader: true passHostHeader: true
@@ -673,12 +670,26 @@ export async function getTraefikConfig(
} }
}; };
config_output.http.middlewares[
maintenanceHeadersMiddlewareName
] = {
headers: {
customRequestHeaders: {
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
"p-host": fullDomain
}
}
};
config_output.http.routers[maintenanceRouterName] = { config_output.http.routers[maintenanceRouterName] = {
entryPoints: [ entryPoints: [
resource.ssl ? entrypointHttps : entrypointHttp resource.ssl ? entrypointHttps : entrypointHttp
], ],
service: maintenanceServiceName, service: maintenanceServiceName,
middlewares: [rewriteMiddlewareName], middlewares: [
rewriteMiddlewareName,
maintenanceHeadersMiddlewareName
],
rule: rule, rule: rule,
priority: 2000, priority: 2000,
...(resource.ssl ? { tls } : {}) ...(resource.ssl ? { tls } : {})
@@ -691,6 +702,7 @@ export async function getTraefikConfig(
resource.ssl ? entrypointHttps : entrypointHttp resource.ssl ? entrypointHttps : entrypointHttp
], ],
service: maintenanceServiceName, service: maintenanceServiceName,
middlewares: [maintenanceHeadersMiddlewareName],
rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `, rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `,
priority: 2001, priority: 2001,
...(resource.ssl ? { tls } : {}) ...(resource.ssl ? { tls } : {})
@@ -1027,7 +1039,7 @@ export async function getTraefikConfig(
} }
} }
if (allowBrowserGatewayResources) { if (browserGatewayUiUrl) {
// Generate Traefik config for browser gateway resources // Generate Traefik config for browser gateway resources
const browserGatewayPort = 39999; const browserGatewayPort = 39999;
for (const [, bgResource] of browserGatewayResourcesMap.entries()) { for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
@@ -1119,20 +1131,17 @@ export async function getTraefikConfig(
} }
} }
if (showBgMaintenancePage && allowMaintenancePage) { if (showBgMaintenancePage && maintenancePageUiUrl) {
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`; const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`; const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`; const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
const bgMaintenanceHeadersMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-headers`;
const entrypointHttp = const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint; config.getRawConfig().traefik.http_entrypoint;
const entrypointHttps = const entrypointHttps =
config.getRawConfig().traefik.https_entrypoint; config.getRawConfig().traefik.https_entrypoint;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
if (!config_output.http.services) if (!config_output.http.services)
config_output.http.services = {}; config_output.http.services = {};
if (!config_output.http.middlewares) if (!config_output.http.middlewares)
@@ -1144,7 +1153,7 @@ export async function getTraefikConfig(
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${maintenanceHost}:${maintenancePort}` url: maintenancePageUiUrl
} }
], ],
passHostHeader: true passHostHeader: true
@@ -1158,12 +1167,26 @@ export async function getTraefikConfig(
} }
}; };
config_output.http.middlewares![
bgMaintenanceHeadersMiddlewareName
] = {
headers: {
customRequestHeaders: {
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
"p-host": fullDomain
}
}
};
config_output.http.routers![bgMaintenanceRouterName] = { config_output.http.routers![bgMaintenanceRouterName] = {
entryPoints: [ entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp bgResource.ssl ? entrypointHttps : entrypointHttp
], ],
service: bgMaintenanceServiceName, service: bgMaintenanceServiceName,
middlewares: [bgRewriteMiddlewareName], middlewares: [
bgRewriteMiddlewareName,
bgMaintenanceHeadersMiddlewareName
],
rule: hostRule, rule: hostRule,
priority: 2000, priority: 2000,
...(bgResource.ssl ? { tls } : {}) ...(bgResource.ssl ? { tls } : {})
@@ -1176,6 +1199,7 @@ export async function getTraefikConfig(
bgResource.ssl ? entrypointHttps : entrypointHttp bgResource.ssl ? entrypointHttps : entrypointHttp
], ],
service: bgMaintenanceServiceName, service: bgMaintenanceServiceName,
middlewares: [bgMaintenanceHeadersMiddlewareName],
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 2001, priority: 2001,
...(bgResource.ssl ? { tls } : {}) ...(bgResource.ssl ? { tls } : {})
@@ -1234,9 +1258,8 @@ export async function getTraefikConfig(
// The primary type is used for the path rewrite (e.g. /rdp), mirroring // The primary type is used for the path rewrite (e.g. /rdp), mirroring
// how the maintenance page rewrites everything to /maintenance-screen. // how the maintenance page rewrites everything to /maintenance-screen.
const primaryType = typeMap.keys().next().value as string; const primaryType = typeMap.keys().next().value as string;
const internalHost = config.getRawConfig().server.internal_hostname;
const internalPort = config.getRawConfig().server.next_port;
const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`; const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`;
const uiHeadersMiddlewareName = `bg-r${bgResource.resourceId}-ui-headers`;
const entrypoint = bgResource.ssl const entrypoint = bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint ? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint; : config.getRawConfig().traefik.http_entrypoint;
@@ -1252,22 +1275,33 @@ export async function getTraefikConfig(
} }
}; };
config_output.http.middlewares![uiHeadersMiddlewareName] = {
headers: {
customRequestHeaders: {
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
"p-host": fullDomain
}
}
};
config_output.http.services![bgUiServiceName] = { config_output.http.services![bgUiServiceName] = {
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${internalHost}:${internalPort}` url: browserGatewayUiUrl
} }
] ]
} }
}; };
// Assets router at higher priority so /_next files load without rewrite // Assets router at higher priority so /_next files load without rewrite.
// Do NOT apply the path-rewrite middleware here — static assets must
// keep their original path; only the host headers are needed.
config_output.http.routers![ config_output.http.routers![
`bg-r${bgResource.resourceId}-assets-router` `bg-r${bgResource.resourceId}-assets-router`
] = { ] = {
entryPoints: [entrypoint], entryPoints: [entrypoint],
middlewares: routerMiddlewares, middlewares: [...routerMiddlewares, uiHeadersMiddlewareName],
service: bgUiServiceName, service: bgUiServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 101, priority: 101,
@@ -1279,7 +1313,11 @@ export async function getTraefikConfig(
`bg-r${bgResource.resourceId}-ui-router` `bg-r${bgResource.resourceId}-ui-router`
] = { ] = {
entryPoints: [entrypoint], entryPoints: [entrypoint],
middlewares: [...routerMiddlewares, uiRewriteMiddlewareName], middlewares: [
...routerMiddlewares,
uiRewriteMiddlewareName,
uiHeadersMiddlewareName
],
service: bgUiServiceName, service: bgUiServiceName,
rule: hostRule, rule: hostRule,
priority: 100, priority: 100,
@@ -1312,10 +1350,6 @@ export async function getTraefikConfig(
const siteResourceRouterName = `${srKey}-router`; const siteResourceRouterName = `${srKey}-router`;
const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`; const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
if (!config_output.http.routers) { if (!config_output.http.routers) {
config_output.http.routers = {}; config_output.http.routers = {};
} }
@@ -1331,7 +1365,7 @@ export async function getTraefikConfig(
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${maintenanceHost}:${maintenancePort}` url: maintenancePageUiUrl
} }
], ],
passHostHeader: true passHostHeader: true
+4 -2
View File
@@ -277,6 +277,8 @@ hybridRouter.get(
); );
} }
const pangolinUIUrl = config.getRawConfig().app.dashboard_url; // points to the dashboard to serve from there
try { try {
const traefikConfig = await getTraefikConfig( const traefikConfig = await getTraefikConfig(
remoteExitNode.exitNodeId, remoteExitNode.exitNodeId,
@@ -284,8 +286,8 @@ hybridRouter.get(
true, // But don't allow domain namespace resources true, // But don't allow domain namespace resources
false, // Dont include login pages, false, // Dont include login pages,
true, // allow raw resources true, // allow raw resources
false, // dont generate maintenance page pangolinUIUrl, // dont generate maintenance page
false // dont generate browser gateway targets pangolinUIUrl // generate browser gateway targets
); );
return response(res, { return response(res, {
@@ -54,7 +54,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
// TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly) // TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly)
} }
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) { if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 12) {
logger.warn( logger.warn(
`Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the site reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?` `Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the site reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
); );
@@ -348,7 +348,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
// this prevents us from accepting a register from an olm that has not hole punched yet. // this prevents us from accepting a register from an olm that has not hole punched yet.
// the olm will pump the register so we can keep checking // the olm will pump the register so we can keep checking
// TODO: I still think there is a better way to do this rather than locking it out here but ??? // TODO: I still think there is a better way to do this rather than locking it out here but ???
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) { if (now - (client.lastHolePunch || 0) > 12 && sitesCount > 0) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`, `[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
{ orgId: client.orgId, clientId: client.clientId } { orgId: client.orgId, clientId: client.clientId }
+1 -5
View File
@@ -3,7 +3,6 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response"; import { response as sendResponse } from "@server/lib/response";
import config from "@server/lib/config";
import { build } from "@server/build"; import { build } from "@server/build";
import { APP_VERSION } from "@server/lib/consts"; import { APP_VERSION } from "@server/lib/consts";
import license from "#dynamic/license/license"; import license from "#dynamic/license/license";
@@ -22,9 +21,6 @@ export async function getServerInfo(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const supporterData = config.getSupporterData();
const supporterStatusValid = supporterData?.valid || false;
let enterpriseLicenseValid = false; let enterpriseLicenseValid = false;
let enterpriseLicenseType: string | null = null; let enterpriseLicenseType: string | null = null;
@@ -41,7 +37,7 @@ export async function getServerInfo(
return sendResponse<GetServerInfoResponse>(res, { return sendResponse<GetServerInfoResponse>(res, {
data: { data: {
version: APP_VERSION, version: APP_VERSION,
supporterStatusValid, supporterStatusValid: true,
build, build,
enterpriseLicenseValid, enterpriseLicenseValid,
enterpriseLicenseType enterpriseLicenseType
@@ -17,13 +17,18 @@ export async function traefikConfigProvider(
// Get the current exit node name from config // Get the current exit node name from config
const currentExitNodeId = await getCurrentExitNodeId(); const currentExitNodeId = await getCurrentExitNodeId();
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost = config.getRawConfig().server.internal_hostname;
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
const traefikConfig = await getTraefikConfig( const traefikConfig = await getTraefikConfig(
currentExitNodeId, currentExitNodeId,
config.getRawConfig().traefik.site_types, config.getRawConfig().traefik.site_types,
build == "oss", // filter out the namespace domains in open source build == "oss", // filter out the namespace domains in open source
build != "oss", // generate the login pages on the cloud and and enterprise, build != "oss", // generate the login pages on the cloud and and enterprise,
config.getRawConfig().traefik.allow_raw_resources, config.getRawConfig().traefik.allow_raw_resources,
build != "oss" // generate browser gateway resources on cloud and enterprise pangolinUIUrl,
pangolinUIUrl
); );
if (traefikConfig?.http?.middlewares) { if (traefikConfig?.http?.middlewares) {
+11 -6
View File
@@ -42,7 +42,14 @@ import {
SettingsSectionFooter SettingsSectionFooter
} from "@app/components/Settings"; } from "@app/components/Settings";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { ArrowRight, Check, ExternalLink, Heart, InfoIcon, TicketCheck } from "lucide-react"; import {
ArrowRight,
Check,
ExternalLink,
Heart,
InfoIcon,
TicketCheck
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import DismissableBanner from "@app/components/DismissableBanner"; import DismissableBanner from "@app/components/DismissableBanner";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
@@ -50,7 +57,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { SitePriceCalculator } from "@app/components/SitePriceCalculator"; import { SitePriceCalculator } from "@app/components/SitePriceCalculator";
import { Checkbox } from "@app/components/ui/checkbox"; import { Checkbox } from "@app/components/ui/checkbox";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; // import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
const ENTERPRISE_DOCS_URL = const ENTERPRISE_DOCS_URL =
@@ -82,7 +89,7 @@ export default function LicensePage() {
const [isActivatingLicense, setIsActivatingLicense] = useState(false); const [isActivatingLicense, setIsActivatingLicense] = useState(false);
const [isDeletingLicense, setIsDeletingLicense] = useState(false); const [isDeletingLicense, setIsDeletingLicense] = useState(false);
const [isRecheckingLicense, setIsRecheckingLicense] = useState(false); const [isRecheckingLicense, setIsRecheckingLicense] = useState(false);
const { supporterStatus } = useSupporterStatusContext(); // const { supporterStatus } = useSupporterStatusContext();
const t = useTranslations(); const t = useTranslations();
@@ -347,9 +354,7 @@ export default function LicensePage() {
storageKey="license-banner-dismissed" storageKey="license-banner-dismissed"
version={1} version={1}
title={t("licenseBannerTitle")} title={t("licenseBannerTitle")}
titleIcon={ titleIcon={<TicketCheck className="w-5 h-5 text-primary" />}
<TicketCheck className="w-5 h-5 text-primary" />
}
description={t("licenseBannerDescription")} description={t("licenseBannerDescription")}
> >
<Link <Link
+19 -19
View File
@@ -68,15 +68,15 @@ export default async function RootLayout({
const env = pullEnv(); const env = pullEnv();
const locale = await getLocale(); const locale = await getLocale();
const supporterData = { // const supporterData = {
visible: true // visible: true
} as any; // } as any;
const res = await priv.get<AxiosResponse<IsSupporterKeyVisibleResponse>>( // const res = await priv.get<AxiosResponse<IsSupporterKeyVisibleResponse>>(
"supporter-key/visible" // "supporter-key/visible"
); // );
supporterData.visible = res.data.data.visible; // supporterData.visible = res.data.data.visible;
supporterData.tier = res.data.data.tier; // supporterData.tier = res.data.data.tier;
let licenseStatus: GetLicenseStatusResponse; let licenseStatus: GetLicenseStatusResponse;
if (build === "enterprise") { if (build === "enterprise") {
@@ -127,20 +127,20 @@ export default async function RootLayout({
<LicenseStatusProvider <LicenseStatusProvider
licenseStatus={licenseStatus} licenseStatus={licenseStatus}
> >
<SupportStatusProvider {/* <SupportStatusProvider
supporterStatus={supporterData} supporterStatus={supporterData}
> > */}
{/* Main content */} {/* Main content */}
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<SplashImage> <SplashImage>
<LicenseViolation />
{children}
</SplashImage>
<LicenseViolation /> <LicenseViolation />
</div> {children}
</SplashImage>
<LicenseViolation />
</div> </div>
</SupportStatusProvider> </div>
{/* </SupportStatusProvider> */}
</LicenseStatusProvider> </LicenseStatusProvider>
<Toaster /> <Toaster />
</TanstackQueryProvider> </TanstackQueryProvider>
+1 -1
View File
@@ -28,7 +28,7 @@ export default async function MaintenanceScreen() {
try { try {
const headersList = await headers(); const headersList = await headers();
const host = headersList.get("host") || ""; const host = headersList.get("p-host") || headersList.get("host") || "";
const hostname = host.split(":")[0]; const hostname = host.split(":")[0];
const res = await priv.get<AxiosResponse<GetMaintenanceInfoResponse>>( const res = await priv.get<AxiosResponse<GetMaintenanceInfoResponse>>(
+4 -4
View File
@@ -1,24 +1,24 @@
"use client"; "use client";
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; // import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { build } from "@server/build"; import { build } from "@server/build";
export default function AuthPageFooterNotices() { export default function AuthPageFooterNotices() {
const t = useTranslations(); const t = useTranslations();
const { supporterStatus } = useSupporterStatusContext(); // const { supporterStatus } = useSupporterStatusContext();
const { isUnlocked, licenseStatus } = useLicenseStatusContext(); const { isUnlocked, licenseStatus } = useLicenseStatusContext();
return ( return (
<> <>
{supporterStatus?.visible && ( {/* {supporterStatus?.visible && (
<div className="text-center mt-2"> <div className="text-center mt-2">
<span className="text-sm text-muted-foreground opacity-50"> <span className="text-sm text-muted-foreground opacity-50">
{t("noSupportKey")} {t("noSupportKey")}
</span> </span>
</div> </div>
)} )} */}
{build === "enterprise" && !isUnlocked() ? ( {build === "enterprise" && !isUnlocked() ? (
<div className="text-center mt-2"> <div className="text-center mt-2">
<span className="text-sm font-medium text-muted-foreground"> <span className="text-sm font-medium text-muted-foreground">
+28 -27
View File
@@ -9,33 +9,34 @@ export default function SupporterMessage({ tier }: { tier: string }) {
const t = useTranslations(); const t = useTranslations();
return ( return (
<div className="relative flex items-center space-x-2 whitespace-nowrap group"> <></>
<span // <div className="relative flex items-center space-x-2 whitespace-nowrap group">
className="cursor-pointer" // <span
onClick={(e) => { // className="cursor-pointer"
// Get the bounding box of the element // onClick={(e) => {
const rect = ( // // Get the bounding box of the element
e.target as HTMLElement // const rect = (
).getBoundingClientRect(); // e.target as HTMLElement
// ).getBoundingClientRect();
// Trigger confetti centered on the word "Pangolin" // // Trigger confetti centered on the word "Pangolin"
confetti({ // confetti({
particleCount: 100, // particleCount: 100,
spread: 70, // spread: 70,
origin: { // origin: {
x: (rect.left + rect.width / 2) / window.innerWidth, // x: (rect.left + rect.width / 2) / window.innerWidth,
y: rect.top / window.innerHeight // y: rect.top / window.innerHeight
}, // },
colors: ["#FFA500", "#FF4500", "#FFD700"] // colors: ["#FFA500", "#FF4500", "#FFD700"]
}); // });
}} // }}
> // >
Pangolin // Pangolin
</span> // </span>
<Star className="w-3 h-3" /> // <Star className="w-3 h-3" />
<div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block text-primary text-sm rounded-md border shadow-md px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity"> // <div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block text-primary text-sm rounded-md border shadow-md px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
{t("componentsSupporterMessage", { tier: tier })} // {t("componentsSupporterMessage", { tier: tier })}
</div> // </div>
</div> // </div>
); );
} }
+110 -110
View File
@@ -3,7 +3,7 @@
// THIS IS DEPRECATED AND IS NO LONGER SHOWED TO THE USER WITH THE DISCONTINUATION // THIS IS DEPRECATED AND IS NO LONGER SHOWED TO THE USER WITH THE DISCONTINUATION
// OF THE SUPPORTER PROGRAM. IT MAY BE REMOVED IN A FUTURE UPDATE. // OF THE SUPPORTER PROGRAM. IT MAY BE REMOVED IN A FUTURE UPDATE.
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; // import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { import {
Tooltip, Tooltip,
@@ -58,134 +58,134 @@ interface SupporterStatusProps {
export default function SupporterStatus({ export default function SupporterStatus({
isCollapsed = false isCollapsed = false
}: SupporterStatusProps) { }: SupporterStatusProps) {
const { supporterStatus, updateSupporterStatus } = // const { supporterStatus, updateSupporterStatus } =
useSupporterStatusContext(); // useSupporterStatusContext();
const [supportOpen, setSupportOpen] = useState(false); // const [supportOpen, setSupportOpen] = useState(false);
const [keyOpen, setKeyOpen] = useState(false); // const [keyOpen, setKeyOpen] = useState(false);
const [purchaseOptionsOpen, setPurchaseOptionsOpen] = useState(false); // const [purchaseOptionsOpen, setPurchaseOptionsOpen] = useState(false);
const { env } = useEnvContext(); // const { env } = useEnvContext();
const api = createApiClient({ env }); // const api = createApiClient({ env });
const t = useTranslations(); // const t = useTranslations();
const formSchema = z.object({ // const formSchema = z.object({
githubUsername: z.string().nonempty({ // githubUsername: z.string().nonempty({
error: "GitHub username is required" // error: "GitHub username is required"
}), // }),
key: z.string().nonempty({ // key: z.string().nonempty({
error: "Supporter key is required" // error: "Supporter key is required"
}) // })
}); // });
const form = useForm({ // const form = useForm({
resolver: zodResolver(formSchema), // resolver: zodResolver(formSchema),
defaultValues: { // defaultValues: {
githubUsername: "", // githubUsername: "",
key: "" // key: ""
} // }
}); // });
async function hide() { // async function hide() {
await api.post("/supporter-key/hide"); // await api.post("/supporter-key/hide");
updateSupporterStatus({ // updateSupporterStatus({
visible: false // visible: false
}); // });
} // }
async function onSubmit(values: z.infer<typeof formSchema>) { // async function onSubmit(values: z.infer<typeof formSchema>) {
try { // try {
const res = await api.post< // const res = await api.post<
AxiosResponse<ValidateSupporterKeyResponse> // AxiosResponse<ValidateSupporterKeyResponse>
>("/supporter-key/validate", { // >("/supporter-key/validate", {
githubUsername: values.githubUsername, // githubUsername: values.githubUsername,
key: values.key // key: values.key
}); // });
const data = res.data.data; // const data = res.data.data;
if (!data || !data.valid) { // if (!data || !data.valid) {
toast({ // toast({
variant: "destructive", // variant: "destructive",
title: t("supportKeyInvalid"), // title: t("supportKeyInvalid"),
description: t("supportKeyInvalidDescription") // description: t("supportKeyInvalidDescription")
}); // });
return; // return;
} // }
// Trigger the toast // // Trigger the toast
toast({ // toast({
variant: "default", // variant: "default",
title: t("supportKeyValid"), // title: t("supportKeyValid"),
description: t("supportKeyValidDescription") // description: t("supportKeyValidDescription")
}); // });
// Fireworks-style confetti // // Fireworks-style confetti
const duration = 5 * 1000; // 5 seconds // const duration = 5 * 1000; // 5 seconds
const animationEnd = Date.now() + duration; // const animationEnd = Date.now() + duration;
const defaults = { // const defaults = {
startVelocity: 30, // startVelocity: 30,
spread: 360, // spread: 360,
ticks: 60, // ticks: 60,
zIndex: 0, // zIndex: 0,
colors: ["#FFA500", "#FF4500", "#FFD700"] // Orange hues // colors: ["#FFA500", "#FF4500", "#FFD700"] // Orange hues
}; // };
function randomInRange(min: number, max: number) { // function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min; // return Math.random() * (max - min) + min;
} // }
const interval = setInterval(() => { // const interval = setInterval(() => {
const timeLeft = animationEnd - Date.now(); // const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) { // if (timeLeft <= 0) {
clearInterval(interval); // clearInterval(interval);
return; // return;
} // }
const particleCount = 50 * (timeLeft / duration); // const particleCount = 50 * (timeLeft / duration);
// Launch confetti from two random horizontal positions // // Launch confetti from two random horizontal positions
confetti({ // confetti({
...defaults, // ...defaults,
particleCount, // particleCount,
origin: { // origin: {
x: randomInRange(0.1, 0.3), // x: randomInRange(0.1, 0.3),
y: Math.random() - 0.2 // y: Math.random() - 0.2
} // }
}); // });
confetti({ // confetti({
...defaults, // ...defaults,
particleCount, // particleCount,
origin: { // origin: {
x: randomInRange(0.7, 0.9), // x: randomInRange(0.7, 0.9),
y: Math.random() - 0.2 // y: Math.random() - 0.2
} // }
}); // });
}, 250); // }, 250);
setPurchaseOptionsOpen(false); // setPurchaseOptionsOpen(false);
setKeyOpen(false); // setKeyOpen(false);
updateSupporterStatus({ // updateSupporterStatus({
visible: false // visible: false
}); // });
} catch (error) { // } catch (error) {
toast({ // toast({
variant: "destructive", // variant: "destructive",
title: t("error"), // title: t("error"),
description: formatAxiosError( // description: formatAxiosError(
error, // error,
t("supportKeyErrorValidationDescription") // t("supportKeyErrorValidationDescription")
) // )
}); // });
return; // return;
} // }
} // }
return ( return (
<> <>
<Credenza {/* <Credenza
open={purchaseOptionsOpen} open={purchaseOptionsOpen}
onOpenChange={(val) => { onOpenChange={(val) => {
setPurchaseOptionsOpen(val); setPurchaseOptionsOpen(val);
@@ -469,7 +469,7 @@ export default function SupporterStatus({
{t("supportKeyBuy")} {t("supportKeyBuy")}
</Button> </Button>
) )
) : null} ) : null} */}
</> </>
); );
} }
-1
View File
@@ -139,7 +139,6 @@ Restart=always
RestartSec=2 RestartSec=2
UMask=0077 UMask=0077
NoNewPrivileges=true
PrivateTmp=true PrivateTmp=true
[Install] [Install]
+1 -1
View File
@@ -6,7 +6,7 @@ import { cache } from "react";
export const getBrowserTargetForRequest = cache(async () => { export const getBrowserTargetForRequest = cache(async () => {
const headersList = await headers(); const headersList = await headers();
const host = headersList.get("host") || ""; const host = headersList.get("p-host") || headersList.get("host") || "";
const hostname = host.split(":")[0]; const hostname = host.split(":")[0];
try { try {