mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-24 11:15:31 +02:00
add hybrid splash
This commit is contained in:
parent
8c8a981452
commit
36c0d9aba2
8 changed files with 231 additions and 10 deletions
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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));
|
||||||
|
|
176
src/app/admin/managed/page.tsx
Normal file
176
src/app/admin/managed/page.tsx
Normal 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,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",
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -25,5 +25,5 @@ export type Env = {
|
||||||
disableBasicWireguardSites: boolean;
|
disableBasicWireguardSites: boolean;
|
||||||
enableClients: boolean;
|
enableClients: boolean;
|
||||||
hideSupporterKey: boolean;
|
hideSupporterKey: boolean;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue