add hybrid splash

This commit is contained in:
miloschwartz
2025-08-17 21:29:21 -07:00
parent 8c8a981452
commit 36c0d9aba2
8 changed files with 231 additions and 10 deletions
+1
View File
@@ -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",
+1 -3
View File
@@ -103,9 +103,7 @@ export class Config {
private async checkKeyStatus() {
const licenseStatus = await license.check();
if (
!licenseStatus.isHostLicensed
) {
if (!licenseStatus.isHostLicensed) {
this.checkSupporterKey();
}
}
+1
View File
@@ -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(),
+7 -3
View File
@@ -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));
+176
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>
</>
);
}
+13 -2
View File
@@ -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: <Zap className="h-4 w-4" />
}
]
: []),
{
title: "sidebarAllUsers",
href: "/admin/users",
+31 -1
View File
@@ -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({
<div className="px-2 pt-1">
{!isAdminPage && user.serverAdmin && (
<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
href="/admin"
className={cn(
+1 -1
View File
@@ -25,5 +25,5 @@ export type Env = {
disableBasicWireguardSites: boolean;
enableClients: boolean;
hideSupporterKey: boolean;
},
}
};