);
})}
>
)}
>
)}
-
);
From 05bf77da295aaf2a93c3cfb4dbd5f4161898e949 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Fri, 3 Jul 2026 21:05:47 +0200
Subject: [PATCH 02/94] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20remove=20`cache`=20o?=
=?UTF-8?q?n=20`verifySession`=20calls=20as=20it's=20already=20wrapped=20i?=
=?UTF-8?q?n=20`cache`?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/auth/login/page.tsx | 3 +--
src/app/page.tsx | 5 +----
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index 274cab561..cdb05c71d 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -29,8 +29,7 @@ export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
- const getUser = cache(verifySession);
- const user = await getUser({ skipCheckVerifyEmail: true });
+ const user = await verifySession({ skipCheckVerifyEmail: true });
const isInvite = searchParams?.redirect?.includes("/invite");
const forceLoginParam = searchParams?.forceLogin;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 7f0f05b57..84a14cb5c 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -5,7 +5,6 @@ import UserProvider from "@app/providers/UserProvider";
import { ListUserOrgsResponse } from "@server/routers/org";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
-import { cache } from "react";
import OrganizationLanding from "@app/components/OrganizationLanding";
import { pullEnv } from "@app/lib/pullEnv";
import { cleanRedirect } from "@app/lib/cleanRedirect";
@@ -13,7 +12,6 @@ import { Layout } from "@app/components/Layout";
import RedirectToOrg from "@app/components/RedirectToOrg";
import { InitialSetupCompleteResponse } from "@server/routers/auth";
import { cookies } from "next/headers";
-import { build } from "@server/build";
export const dynamic = "force-dynamic";
@@ -27,8 +25,7 @@ export default async function Page(props: {
const env = pullEnv();
- const getUser = cache(verifySession);
- const user = await getUser({ skipCheckVerifyEmail: true });
+ const user = await verifySession({ skipCheckVerifyEmail: true });
let complete = false;
try {
From a74c0c227c1df7d3909fe48223f82a05053a85f3 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Fri, 3 Jul 2026 21:05:53 +0200
Subject: [PATCH 03/94] =?UTF-8?q?=F0=9F=8C=90=20text?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
messages/en-US.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/messages/en-US.json b/messages/en-US.json
index 4701d4da1..5d774c50b 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -1503,6 +1503,7 @@
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
"otpAuthSubmit": "Submit Code",
"idpContinue": "Or continue with",
+ "idpLastUsed": "Last used",
"otpAuthBack": "Back to Password",
"navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application",
From 289be30e6b17a28f4dd216889e1e39508cae3c50 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Fri, 3 Jul 2026 22:33:51 +0200
Subject: [PATCH 04/94] =?UTF-8?q?=E2=9C=A8=20show=20last=20used=20login=20?=
=?UTF-8?q?idp=20in=20smart=20login=20form?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/auth/login/page.tsx | 44 ++++++++++++++--
src/components/IdpLoginButtons.tsx | 80 ++++++++++++++++++++----------
src/components/SmartLoginForm.tsx | 15 +++++-
src/lib/consts.ts | 1 +
src/lib/setClientCookie.ts | 32 ++++++++++++
5 files changed, 139 insertions(+), 33 deletions(-)
create mode 100644 src/lib/consts.ts
create mode 100644 src/lib/setClientCookie.ts
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index cdb05c71d..92097512c 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -16,8 +16,11 @@ import LoginCardHeader from "@app/components/LoginCardHeader";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { LoginFormIDP } from "@app/components/LoginForm";
-import { ListIdpsResponse } from "@server/routers/idp";
+import { ListIdpsResponse, type GetIdpResponse } from "@server/routers/idp";
import type { Metadata } from "next";
+import { cookies } from "next/headers";
+import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts";
+import z from "zod";
export const metadata: Metadata = {
title: "Log In"
@@ -31,6 +34,8 @@ export default async function Page(props: {
const searchParams = await props.searchParams;
const user = await verifySession({ skipCheckVerifyEmail: true });
+ const lastUsedIdpCookie = (await cookies()).get(LAST_USED_IDP_COOKIE_NAME);
+
const isInvite = searchParams?.redirect?.includes("/invite");
const forceLoginParam = searchParams?.forceLogin;
const forceLogin = forceLoginParam === "true";
@@ -84,19 +89,47 @@ export default async function Page(props: {
(build === "enterprise" && env.app.identityProviderMode === "org");
let loginIdps: LoginFormIDP[] = [];
+ let lastUsedIdpForSmartLogin: (LoginFormIDP & { orgId?: string }) | null =
+ null;
if (!useSmartLogin) {
// Load IdPs for DashboardLoginForm (OSS or org-only IdP mode)
if (build === "oss" || env.app.identityProviderMode !== "org") {
- const idpsRes = await cache(
- async () =>
- await priv.get>("/idp")
- )();
+ const idpsRes =
+ await priv.get>("/idp");
loginIdps = idpsRes.data.data.idps.map((idp) => ({
idpId: idp.idpId,
name: idp.name,
variant: idp.type
})) as LoginFormIDP[];
}
+ } else {
+ if (lastUsedIdpCookie) {
+ const lastUsedIdpSchema = z.object({
+ orgId: z.string().optional(),
+ idpId: z.number()
+ });
+ try {
+ const persistedData = lastUsedIdpSchema.parse(
+ JSON.parse(lastUsedIdpCookie.value)
+ );
+
+ const idpRes = await priv.get>(
+ `/idp/${persistedData.idpId}`
+ );
+
+ const idp = idpRes.data.data.idp;
+
+ lastUsedIdpForSmartLogin = {
+ idpId: idp.idpId,
+ name: idp.name,
+ variant: idp.type,
+ orgId: persistedData.orgId,
+ lastUsed: true
+ };
+ } catch (error) {
+ // the idp might not exist or the data is malformatted, skip this
+ }
+ }
}
const t = await getTranslations();
@@ -159,6 +192,7 @@ export default async function Page(props: {
redirect={redirectUrl}
forceLogin={forceLogin}
defaultUser={defaultUser}
+ lastUsedIdp={lastUsedIdpForSmartLogin}
orgSignIn={
!isInvite &&
(build === "saas" ||
diff --git a/src/components/IdpLoginButtons.tsx b/src/components/IdpLoginButtons.tsx
index d015dce52..0851a0dd0 100644
--- a/src/components/IdpLoginButtons.tsx
+++ b/src/components/IdpLoginButtons.tsx
@@ -1,26 +1,25 @@
"use client";
-import { useEffect, useState } from "react";
-import { Button } from "@app/components/ui/button";
-import { Alert, AlertDescription } from "@app/components/ui/alert";
-import { useTranslations } from "next-intl";
+import { generateOidcUrlProxy } from "@app/actions/server";
import IdpTypeIcon from "@app/components/IdpTypeIcon";
-import {
- generateOidcUrlProxy,
- type GenerateOidcUrlResponse
-} from "@app/actions/server";
+import { Alert, AlertDescription } from "@app/components/ui/alert";
+import { Button } from "@app/components/ui/button";
+import { cleanRedirect } from "@app/lib/cleanRedirect";
+import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts";
+import { setClientCookie } from "@app/lib/setClientCookie";
+import { useTranslations } from "next-intl";
import {
redirect as redirectTo,
- useParams,
+ useRouter,
useSearchParams
} from "next/navigation";
-import { useRouter } from "next/navigation";
-import { cleanRedirect } from "@app/lib/cleanRedirect";
+import { useEffect, useState, useTransition } from "react";
export type LoginFormIDP = {
idpId: number;
name: string;
variant?: string;
+ lastUsed?: boolean;
};
type IdpLoginButtonsProps = {
@@ -35,7 +34,6 @@ export default function IdpLoginButtons({
orgId
}: IdpLoginButtonsProps) {
const [error, setError] = useState(null);
- const [loading, setLoading] = useState(false);
const t = useTranslations();
const params = useSearchParams();
@@ -52,10 +50,22 @@ export default function IdpLoginButtons({
}
}, []);
+ const [loading, startTransition] = useTransition();
+
async function loginWithIdp(idpId: number) {
- setLoading(true);
setError(null);
+ setClientCookie(
+ LAST_USED_IDP_COOKIE_NAME,
+ JSON.stringify({
+ orgId,
+ idpId
+ }),
+ {
+ sameSite: "Lax"
+ }
+ );
+
let redirectToUrl: string | undefined;
try {
console.log("generating", idpId, redirect || "/", orgId);
@@ -68,7 +78,6 @@ export default function IdpLoginButtons({
if (response.error) {
setError(response.message);
- setLoading(false);
return;
}
@@ -84,7 +93,6 @@ export default function IdpLoginButtons({
"An unexpected error occurred. Please try again."
})
);
- setLoading(false);
}
if (redirectToUrl) {
@@ -124,20 +132,38 @@ export default function IdpLoginButtons({
idp.variant || idp.name.toLowerCase();
return (
-
+
+
+ {idp.lastUsed && (
+
+
+ {t("idpLastUsed")}
+
+
+ )}
+
);
})}
>
diff --git a/src/components/SmartLoginForm.tsx b/src/components/SmartLoginForm.tsx
index dd0e131df..de1955771 100644
--- a/src/components/SmartLoginForm.tsx
+++ b/src/components/SmartLoginForm.tsx
@@ -27,6 +27,8 @@ import UserProfileCard from "@app/components/UserProfileCard";
import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton";
import { Separator } from "@app/components/ui/separator";
import OrgSignInLink from "@app/components/OrgSignInLink";
+import type { LoginFormIDP } from "./LoginForm";
+import IdpLoginButtons from "./IdpLoginButtons";
const identifierSchema = z.object({
identifier: z.string().min(1, "Username or email is required")
@@ -53,6 +55,7 @@ type SmartLoginFormProps = {
forceLogin?: boolean;
defaultUser?: string;
orgSignIn?: OrgSignInConfig;
+ lastUsedIdp?: (LoginFormIDP & { orgId?: string }) | null;
};
type ViewState =
@@ -89,7 +92,8 @@ export default function SmartLoginForm({
redirect,
forceLogin,
defaultUser,
- orgSignIn
+ orgSignIn,
+ lastUsedIdp
}: SmartLoginFormProps) {
const router = useRouter();
const { env } = useEnvContext();
@@ -294,6 +298,15 @@ export default function SmartLoginForm({
+
+ {lastUsedIdp && (
+
+ )}
+
Date: Fri, 3 Jul 2026 22:33:58 +0200
Subject: [PATCH 05/94] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix=20types?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/LoginForm.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx
index 40685f846..eef7406a7 100644
--- a/src/components/LoginForm.tsx
+++ b/src/components/LoginForm.tsx
@@ -44,6 +44,7 @@ export type LoginFormIDP = {
idpId: number;
name: string;
variant?: string;
+ lastUsed?: boolean;
};
type LoginFormProps = {
From 2fe85ebda2d43c543d13b9c5e6730e7649e05bfb Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Wed, 8 Jul 2026 03:11:12 +0200
Subject: [PATCH 06/94] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20set=20last=20IDP=20t?=
=?UTF-8?q?o=20null=20if=20login=20with=20email=20+=20password?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/LoginForm.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx
index eef7406a7..82f948ca9 100644
--- a/src/components/LoginForm.tsx
+++ b/src/components/LoginForm.tsx
@@ -137,6 +137,7 @@ export default function LoginForm({
const { email, password } = form.getValues();
const { code } = mfaForm.getValues();
+ setLastUsedIdpId(null);
setLoading(true);
setError(null);
From 34d5c9535d3a9c4f029088cf2a4db905bd3ab2ab Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Fri, 10 Jul 2026 03:22:28 +0200
Subject: [PATCH 07/94] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20delete=20last=20used?=
=?UTF-8?q?=20IDP=20coookie=20when=20logging=20in=20with=20email/password?=
=?UTF-8?q?=20on=20smart=20login=20form?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/LoginPasswordForm.tsx | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/components/LoginPasswordForm.tsx b/src/components/LoginPasswordForm.tsx
index d6e0c0b79..efbc8d727 100644
--- a/src/components/LoginPasswordForm.tsx
+++ b/src/components/LoginPasswordForm.tsx
@@ -22,6 +22,8 @@ import Link from "next/link";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import MfaInputForm from "@app/components/MfaInputForm";
+import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts";
+import { setClientCookie } from "@app/lib/setClientCookie";
type LoginPasswordFormProps = {
identifier: string;
@@ -82,6 +84,12 @@ export default function LoginPasswordForm({
const { password } = values;
const { code } = mfaForm.getValues();
+ // delete last used auth cookie by setting it in the past
+ setClientCookie(LAST_USED_IDP_COOKIE_NAME, JSON.stringify(null), {
+ sameSite: "Lax",
+ days: -1
+ });
+
setLoading(true);
setError(null);
From 8d018fe47da9ea50d5ab44e28d2679dfb74524bb Mon Sep 17 00:00:00 2001
From: Owen
Date: Thu, 9 Jul 2026 21:28:48 -0400
Subject: [PATCH 08/94] Clean up
---
server/lib/blueprints/applyBlueprint.ts | 6 ------
server/lib/blueprints/privateResources.ts | 3 ---
server/routers/siteResource/createSiteResource.ts | 3 ---
3 files changed, 12 deletions(-)
diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts
index c62dd4735..44646e0bf 100644
--- a/server/lib/blueprints/applyBlueprint.ts
+++ b/server/lib/blueprints/applyBlueprint.ts
@@ -34,12 +34,6 @@ import {
rebuildClientAssociationsFromSiteResource,
waitForSiteResourceRebuildIdle
} from "../rebuildClientAssociations";
-import { build } from "@server/build";
-import HttpCode from "@server/types/HttpCode";
-import createHttpError from "http-errors";
-import next from "next";
-import { LimitId } from "../billing";
-import { usageService } from "../billing/usageService";
type ApplyBlueprintArgs = {
orgId: string;
diff --git a/server/lib/blueprints/privateResources.ts b/server/lib/blueprints/privateResources.ts
index 74a1f618f..aa146679f 100644
--- a/server/lib/blueprints/privateResources.ts
+++ b/server/lib/blueprints/privateResources.ts
@@ -26,9 +26,6 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
import { build } from "@server/build";
-import HttpCode from "@server/types/HttpCode";
-import createHttpError from "http-errors";
-import next from "next";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts
index eaf4cf130..365053cf1 100644
--- a/server/routers/siteResource/createSiteResource.ts
+++ b/server/routers/siteResource/createSiteResource.ts
@@ -56,7 +56,6 @@ const createSiteResourceSchema = z
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
destinationPort: z.int().positive().optional(),
destination: z.string().min(1).nullish(),
- enabled: z.boolean().default(true),
alias: z
.string()
.regex(
@@ -275,7 +274,6 @@ export async function createSiteResource(
scheme,
destinationPort,
destination,
- enabled,
ssl,
alias,
userIds,
@@ -539,7 +537,6 @@ export async function createSiteResource(
destination: destination, // the ssh can be null
scheme,
destinationPort,
- enabled,
alias: alias ? alias.trim() : null,
aliasAddress,
tcpPortRangeString: tcpPortRangeStringAdjusted,
From d38f9ac2bb46ceaf720d97a059bf7d91098f5237 Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Thu, 9 Jul 2026 21:32:35 -0400
Subject: [PATCH 09/94] update readme
---
README.md | 42 +++++++++++++++++++++--
public/screenshots/resource-launcher.png | Bin 0 -> 568945 bytes
2 files changed, 39 insertions(+), 3 deletions(-)
create mode 100644 public/screenshots/resource-launcher.png
diff --git a/README.md b/README.md
index ab857350e..1827f2570 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@
-Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
+Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure connectivity to infrastructure anywhere. It combines reverse-proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to private resources with NAT traversal, all with granular access control.
## Installation
@@ -63,11 +63,26 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
Pangolin's site connectors provide gateways into networks so you can access any networked resources. Sites use outbound tunnels and intelligent NAT traversal to make networks behind restrictive firewalls available for authorized access without public IPs or open ports. Easily deploy a site as a binary or container on any platform.
+* Lightweight user-space connector runs anywhere
+* Punches through any firewall
+* Doesn't require open ports or a public IP
+* Strict network segmentation
+* WireGuard-based
+* Get alerts when a device or network resource goes down
+
### Browser-based reverse proxy access
-Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
+Expose HTTPS web applications and connect to VNC, RDP, and SSH entirely in the browser through identity and context-aware tunneled reverse proxies. Users access resources with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
+
+* Expose a web panel anywhere
+* Access via any web browser
+* Single sign-on across all resources
+* HTTPS resources
+* Remote desktop in the browser with VNC and RDP
+* In-browser SSH terminal with privileged access management (PAM)
+* PIN codes, passcodes, email OTP, geoblocking, allow-lists, and more
@@ -75,14 +90,35 @@ Expose web applications through identity and context-aware tunneled reverse prox
Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. Add redundancy by routing traffic through multiple connectors in your network.
+* Peer-to-peer with intelligent NAT traversal
+* Hosts/IPs and port ranges
+* Network ranges/CIDRs
+* Friendly DNS aliases for network addresses
+* Privileged access management (PAM) with SSH resources
+* Private HTTPS resources only accessible on the private network
+
### Give users and roles access to resources
-Use Pangolin's built in users or bring your own identity provider and set up role based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
+Use Pangolin's built-in users or bring your own identity provider and set up role-based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
+
+* Bring your existing identity provider (IdP) or use Pangolin identities
+* Sync users and roles from your IdP
+* User- and role-based access control
+* Full network audit and access logs
+### Find and launch resources from a personalized home page
+
+Give users a landing page to quickly find and open the resources they can access. Resources are grouped by site or label, searchable, and filterable, with grid or list views. Saved views capture filters, grouping, and layout as personal or organization-wide defaults.
+
+* Single place for admins and non-admins to see accessible resources
+* Create reusable views for common access patterns
+
+
+
## Download Clients
Download the Pangolin client for your platform:
diff --git a/public/screenshots/resource-launcher.png b/public/screenshots/resource-launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..93b803ee66d236eba80036a4120024b1cd2fe767
GIT binary patch
literal 568945
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