From 36c0d9aba2997d4583d31f7efda6bf55a89bf9b2 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 17 Aug 2025 21:29:07 -0700 Subject: [PATCH] add hybrid splash --- messages/en-US.json | 1 + server/lib/config.ts | 4 +- server/lib/readConfigFile.ts | 1 + server/routers/external.ts | 10 +- src/app/admin/managed/page.tsx | 176 +++++++++++++++++++++++++++++++ src/app/navigation.tsx | 15 ++- src/components/LayoutSidebar.tsx | 32 +++++- src/lib/types/env.ts | 2 +- 8 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 src/app/admin/managed/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 7f00e40d..602754d8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -973,6 +973,7 @@ "logoutError": "Error logging out", "signingAs": "Signed in as", "serverAdmin": "Server Admin", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Enable Two-factor", "otpDisable": "Disable Two-factor", "logout": "Log Out", diff --git a/server/lib/config.ts b/server/lib/config.ts index 6b41df79..82932441 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -103,9 +103,7 @@ export class Config { private async checkKeyStatus() { const licenseStatus = await license.check(); - if ( - !licenseStatus.isHostLicensed - ) { + if (!licenseStatus.isHostLicensed) { this.checkSupporterKey(); } } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 23db4e52..8107385c 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -34,6 +34,7 @@ export const configSchema = z }), hybrid: z .object({ + name: z.string().optional(), id: z.string().optional(), secret: z.string().optional(), endpoint: z.string().optional(), diff --git a/server/routers/external.ts b/server/routers/external.ts index fd7fff50..95add3ed 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -15,6 +15,7 @@ import * as accessToken from "./accessToken"; import * as idp from "./idp"; import * as license from "./license"; import * as apiKeys from "./apiKeys"; +import * as hybrid from "./hybrid"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -951,7 +952,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email || req.ip}`, + keyGenerator: (req) => + `requestEmailVerificationCode:${req.body.email || req.ip}`, handler: (req, res, next) => { const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -972,7 +974,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `requestPasswordReset:${req.body.email || req.ip}`, + keyGenerator: (req) => + `requestPasswordReset:${req.body.email || req.ip}`, handler: (req, res, next) => { const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -1066,7 +1069,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Allow 5 security key registrations per 15 minutes - keyGenerator: (req) => `securityKeyRegister:${req.user?.userId || req.ip}`, + keyGenerator: (req) => + `securityKeyRegister:${req.user?.userId || req.ip}`, handler: (req, res, next) => { const message = `You can only register a security key ${5} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); diff --git a/src/app/admin/managed/page.tsx b/src/app/admin/managed/page.tsx new file mode 100644 index 00000000..cb25ba5d --- /dev/null +++ b/src/app/admin/managed/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionTitle as SectionTitle, + SettingsSectionBody, + SettingsSectionFooter +} from "@app/components/Settings"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { Alert } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + Shield, + Zap, + RefreshCw, + Activity, + Wrench, + CheckCircle, + ExternalLink +} from "lucide-react"; +import Link from "next/link"; + +export default async function ManagedPage() { + return ( + <> + + + + + +

+ Managed Self-Hosted Pangolin is a + deployment option designed for people who want + simplicity and extra reliability while still keeping + their data private and self-hosted. +

+

+ With this option, you still run your own Pangolin + node — your tunnels, SSL termination, and traffic + all stay on your server. The difference is that + management and monitoring are handled through our + cloud dashboard, which unlocks a number of benefits: +

+ +
+
+
+ +
+

+ Simpler operations +

+

+ No need to run your own mail server + or set up complex alerting. You'll + get health checks and downtime + alerts out of the box. +

+
+
+ +
+ +
+

+ Automatic updates +

+

+ The cloud dashboard evolves quickly, + so you get new features and bug + fixes without having to manually + pull new containers every time. +

+
+
+ +
+ +
+

+ Less maintenance +

+

+ No database migrations, backups, or + extra infrastructure to manage. We + handle that in the cloud. +

+
+
+
+ +
+
+ +
+

+ Cloud failover +

+

+ If your node goes down, your tunnels + can temporarily fail over to our + cloud points of presence until you + bring it back online. +

+
+
+
+ +
+

+ High availability (PoPs) +

+

+ You can also attach multiple nodes + to your account for redundancy and + better performance. +

+
+
+ +
+ +
+

+ Future enhancements +

+

+ We're planning to add more + analytics, alerting, and management + tools to make your deployment even + more robust. +

+
+
+
+
+ + + Read the docs to learn more about the Managed + Self-Hosted option in our{" "} + + documentation + + + . + +
+ + + + + +
+
+ + ); +} diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index b26b98ec..f77bf3a9 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -13,10 +13,12 @@ import { TicketCheck, User, Globe, // Added from 'dev' branch - MonitorUp // Added from 'dev' branch + MonitorUp, // Added from 'dev' branch + Zap } from "lucide-react"; -export type SidebarNavSection = { // Added from 'dev' branch +export type SidebarNavSection = { + // Added from 'dev' branch heading: string; items: SidebarNavItem[]; }; @@ -108,6 +110,15 @@ export const adminNavSections: SidebarNavSection[] = [ { heading: "Admin", items: [ + ...(build == "oss" + ? [ + { + title: "managedSelfhosted", + href: "/admin/managed", + icon: + } + ] + : []), { title: "sidebarAllUsers", href: "/admin/users", diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index d309c11f..cfc21144 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -6,7 +6,7 @@ import { OrgSelector } from "@app/components/OrgSelector"; import { cn } from "@app/lib/cn"; import { ListUserOrgsResponse } from "@server/routers/org"; import SupporterStatus from "@app/components/SupporterStatus"; -import { ExternalLink, Server, BookOpenText } from "lucide-react"; +import { ExternalLink, Server, BookOpenText, Zap } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useUserContext } from "@app/hooks/useUserContext"; @@ -20,6 +20,7 @@ import { TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { build } from "@server/build"; interface LayoutSidebarProps { orgId?: string; @@ -73,6 +74,35 @@ export function LayoutSidebar({
{!isAdminPage && user.serverAdmin && (
+ {build === "oss" && ( + + + + + {!isSidebarCollapsed && ( + {t("managedSelfhosted")} + )} + + )} +