add hybrid splash

This commit is contained in:
miloschwartz 2025-08-17 21:29:07 -07:00
parent 8c8a981452
commit 36c0d9aba2
No known key found for this signature in database
8 changed files with 231 additions and 10 deletions

View file

@ -973,6 +973,7 @@
"logoutError": "Error logging out", "logoutError": "Error logging out",
"signingAs": "Signed in as", "signingAs": "Signed in as",
"serverAdmin": "Server Admin", "serverAdmin": "Server Admin",
"managedSelfhosted": "Managed Self-Hosted",
"otpEnable": "Enable Two-factor", "otpEnable": "Enable Two-factor",
"otpDisable": "Disable Two-factor", "otpDisable": "Disable Two-factor",
"logout": "Log Out", "logout": "Log Out",

View file

@ -103,9 +103,7 @@ export class Config {
private async checkKeyStatus() { private async checkKeyStatus() {
const licenseStatus = await license.check(); const licenseStatus = await license.check();
if ( if (!licenseStatus.isHostLicensed) {
!licenseStatus.isHostLicensed
) {
this.checkSupporterKey(); this.checkSupporterKey();
} }
} }

View file

@ -34,6 +34,7 @@ export const configSchema = z
}), }),
hybrid: z hybrid: z
.object({ .object({
name: z.string().optional(),
id: z.string().optional(), id: z.string().optional(),
secret: z.string().optional(), secret: z.string().optional(),
endpoint: z.string().optional(), endpoint: z.string().optional(),

View file

@ -15,6 +15,7 @@ import * as accessToken from "./accessToken";
import * as idp from "./idp"; import * as idp from "./idp";
import * as license from "./license"; import * as license from "./license";
import * as apiKeys from "./apiKeys"; import * as apiKeys from "./apiKeys";
import * as hybrid from "./hybrid";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { import {
verifyAccessTokenAccess, verifyAccessTokenAccess,
@ -951,7 +952,8 @@ authRouter.post(
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 15, max: 15,
keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email || req.ip}`, keyGenerator: (req) =>
`requestEmailVerificationCode:${req.body.email || req.ip}`,
handler: (req, res, next) => { handler: (req, res, next) => {
const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`; 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
@ -972,7 +974,8 @@ authRouter.post(
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 15, max: 15,
keyGenerator: (req) => `requestPasswordReset:${req.body.email || req.ip}`, keyGenerator: (req) =>
`requestPasswordReset:${req.body.email || req.ip}`,
handler: (req, res, next) => { handler: (req, res, next) => {
const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`; 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
@ -1066,7 +1069,8 @@ authRouter.post(
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Allow 5 security key registrations per 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) => { handler: (req, res, next) => {
const message = `You can only register a security key ${5} times every ${15} minutes. Please try again later.`; 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));

View file

@ -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 (
<>
<SettingsSectionTitle
title="Managed Self-Hosted"
description="More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles"
/>
<SettingsContainer>
<SettingsSection>
<SettingsSectionBody>
<p className="text-muted-foreground mb-4">
<strong>Managed Self-Hosted Pangolin</strong> is a
deployment option designed for people who want
simplicity and extra reliability while still keeping
their data private and self-hosted.
</p>
<p className="text-muted-foreground mb-6">
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:
</p>
<div className="grid gap-4 md:grid-cols-2 py-4">
<div className="space-y-3">
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">
Simpler operations
</h4>
<p className="text-sm text-muted-foreground">
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.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<RefreshCw className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">
Automatic updates
</h4>
<p className="text-sm text-muted-foreground">
The cloud dashboard evolves quickly,
so you get new features and bug
fixes without having to manually
pull new containers every time.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Wrench className="w-5 h-5 text-orange-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">
Less maintenance
</h4>
<p className="text-sm text-muted-foreground">
No database migrations, backups, or
extra infrastructure to manage. We
handle that in the cloud.
</p>
</div>
</div>
</div>
<div className="space-y-3">
<div className="flex items-start gap-3">
<Activity className="w-5 h-5 text-purple-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">
Cloud failover
</h4>
<p className="text-sm text-muted-foreground">
If your node goes down, your tunnels
can temporarily fail over to our
cloud points of presence until you
bring it back online.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Shield className="w-5 h-5 text-indigo-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">
High availability (PoPs)
</h4>
<p className="text-sm text-muted-foreground">
You can also attach multiple nodes
to your account for redundancy and
better performance.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Zap className="w-5 h-5 text-yellow-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">
Future enhancements
</h4>
<p className="text-sm text-muted-foreground">
We're planning to add more
analytics, alerting, and management
tools to make your deployment even
more robust.
</p>
</div>
</div>
</div>
</div>
<Alert
variant="neutral"
className="flex items-center gap-1"
>
Read the docs to learn more about the Managed
Self-Hosted option in our{" "}
<Link
href="https://docs.digpangolin.com/self-host/advanced/convert-to-managed"
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-primary flex items-center gap-1"
>
documentation
<ExternalLink className="w-4 h-4" />
</Link>
.
</Alert>
</SettingsSectionBody>
<SettingsSectionFooter>
<Link
href="https://docs.digpangolin.com/self-host/advanced/convert-to-managed"
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-primary flex items-center gap-1"
>
<Button>
Convert This Node to Managed Self-Hosted
</Button>
</Link>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
</>
);
}

View file

@ -13,10 +13,12 @@ import {
TicketCheck, TicketCheck,
User, User,
Globe, // Added from 'dev' branch Globe, // Added from 'dev' branch
MonitorUp // Added from 'dev' branch MonitorUp, // Added from 'dev' branch
Zap
} from "lucide-react"; } from "lucide-react";
export type SidebarNavSection = { // Added from 'dev' branch export type SidebarNavSection = {
// Added from 'dev' branch
heading: string; heading: string;
items: SidebarNavItem[]; items: SidebarNavItem[];
}; };
@ -108,6 +110,15 @@ export const adminNavSections: SidebarNavSection[] = [
{ {
heading: "Admin", heading: "Admin",
items: [ items: [
...(build == "oss"
? [
{
title: "managedSelfhosted",
href: "/admin/managed",
icon: <Zap className="h-4 w-4" />
}
]
: []),
{ {
title: "sidebarAllUsers", title: "sidebarAllUsers",
href: "/admin/users", href: "/admin/users",

View file

@ -6,7 +6,7 @@ import { OrgSelector } from "@app/components/OrgSelector";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org"; import { ListUserOrgsResponse } from "@server/routers/org";
import SupporterStatus from "@app/components/SupporterStatus"; 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 Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
@ -20,6 +20,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger TooltipTrigger
} from "@app/components/ui/tooltip"; } from "@app/components/ui/tooltip";
import { build } from "@server/build";
interface LayoutSidebarProps { interface LayoutSidebarProps {
orgId?: string; orgId?: string;
@ -73,6 +74,35 @@ export function LayoutSidebar({
<div className="px-2 pt-1"> <div className="px-2 pt-1">
{!isAdminPage && user.serverAdmin && ( {!isAdminPage && user.serverAdmin && (
<div className="pb-4"> <div className="pb-4">
{build === "oss" && (
<Link
href="/admin/managed"
className={cn(
"flex items-center rounded transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
isSidebarCollapsed
? "px-2 py-2 justify-center"
: "px-3 py-1.5"
)}
title={
isSidebarCollapsed
? t("managedSelfhosted")
: undefined
}
>
<span
className={cn(
"flex-shrink-0",
!isSidebarCollapsed && "mr-2"
)}
>
<Zap className="h-4 w-4" />
</span>
{!isSidebarCollapsed && (
<span>{t("managedSelfhosted")}</span>
)}
</Link>
)}
<Link <Link
href="/admin" href="/admin"
className={cn( className={cn(

View file

@ -25,5 +25,5 @@ export type Env = {
disableBasicWireguardSites: boolean; disableBasicWireguardSites: boolean;
enableClients: boolean; enableClients: boolean;
hideSupporterKey: boolean; hideSupporterKey: boolean;
}, }
}; };