@@ -90,8 +93,8 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
1. **Deploy the Central Server**:
- - Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs.
-
+ - Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs.
+
> [!TIP]
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can likely get a **VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**. That's a great deal!
> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone.
@@ -113,23 +116,21 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity.
-**Use Case Example - Deploying Services For Your Business**
- You can use Pangolin as an easy way to expose your business applications to your users behind a safe authentication portal you can integrate into your IDP solution. Expose resources on prem and on the cloud.
+**Use Case Example - Deploying Services For Your Business**:
+You can use Pangolin as an easy way to expose your business applications to your users behind a safe authentication portal you can integrate into your IdP solution. Expose resources on prem and on the cloud.
**Use Case Example - IoT Networks**:
IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups.
-
-
_Resources page of Pangolin dashboard (dark mode) showing HTTPS and TCP resources with access control rules._
## Similar Projects and Inspirations
**Cloudflare Tunnels**:
- A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
+ A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
-**Authentik and Authelia**:
- These projects inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
+**Authelia**:
+ This inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
## Project Development / Roadmap
diff --git a/public/screenshots/collage.png b/public/screenshots/collage.png
index 74fe6deb..c791e7ea 100644
Binary files a/public/screenshots/collage.png and b/public/screenshots/collage.png differ
diff --git a/server/license/license.ts b/server/license/license.ts
index 7887f451..e97b8f50 100644
--- a/server/license/license.ts
+++ b/server/license/license.ts
@@ -13,12 +13,19 @@ import moment from "moment";
import { setHostMeta } from "@server/setup/setHostMeta";
import { encrypt, decrypt } from "@server/lib/crypto";
+const keyTypes = ["HOST", "SITES"] as const;
+type KeyType = (typeof keyTypes)[number];
+
+const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const;
+type KeyTier = (typeof keyTiers)[number];
+
export type LicenseStatus = {
isHostLicensed: boolean; // Are there any license keys?
isLicenseValid: boolean; // Is the license key valid?
hostId: string; // Host ID
maxSites?: number;
usedSites?: number;
+ tier?: KeyTier;
};
export type LicenseKeyCache = {
@@ -26,7 +33,8 @@ export type LicenseKeyCache = {
licenseKeyEncrypted: string;
valid: boolean;
iat?: Date;
- type?: "LICENSE" | "SITES";
+ type?: KeyType;
+ tier?: KeyTier;
numSites?: number;
};
@@ -54,7 +62,8 @@ type ValidateLicenseAPIResponse = {
type TokenPayload = {
valid: boolean;
- type: "LICENSE" | "SITES";
+ type: KeyType;
+ tier: KeyTier;
quantity: number;
terminateAt: string; // ISO
iat: number; // Issued at
@@ -182,11 +191,12 @@ LQIDAQAB
licenseKeyEncrypted: key.licenseKeyId,
valid: payload.valid,
type: payload.type,
+ tier: payload.tier,
numSites: payload.quantity,
iat: new Date(payload.iat * 1000)
});
- if (payload.type === "LICENSE") {
+ if (payload.type === "HOST") {
foundHostKey = true;
}
} catch (e) {
@@ -273,6 +283,7 @@ LQIDAQAB
);
cached.valid = payload.valid;
cached.type = payload.type;
+ cached.tier = payload.tier;
cached.numSites = payload.quantity;
cached.iat = new Date(payload.iat * 1000);
@@ -311,8 +322,9 @@ LQIDAQAB
logger.debug("Checking key", cached);
- if (cached.type === "LICENSE") {
+ if (cached.type === "HOST") {
status.isLicenseValid = cached.valid;
+ status.tier = cached.tier;
}
if (!cached.valid) {
diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts
index 1ed7b14a..07ef9aa3 100644
--- a/server/routers/accessToken/listAccessTokens.ts
+++ b/server/routers/accessToken/listAccessTokens.ts
@@ -172,9 +172,20 @@ export async function listAccessTokens(
)
);
}
- const { orgId, resourceId } = parsedParams.data;
+ const { resourceId } = parsedParams.data;
- if (orgId && orgId !== req.userOrgId) {
+ const orgId =
+ parsedParams.data.orgId ||
+ req.userOrg?.orgId ||
+ req.apiKeyOrg?.orgId;
+
+ if (!orgId) {
+ return next(
+ createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
+ );
+ }
+
+ if (req.user && orgId && orgId !== req.userOrgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
@@ -183,21 +194,29 @@ export async function listAccessTokens(
);
}
- const accessibleResources = await db
- .select({
- resourceId: sql
Are you sure you want to permanently delete{" "}
-
+
{selected?.email ||
selected?.name ||
selected?.username}
diff --git a/src/app/components/LicenseViolation.tsx b/src/app/components/LicenseViolation.tsx
index 1771475c..75d544d3 100644
--- a/src/app/components/LicenseViolation.tsx
+++ b/src/app/components/LicenseViolation.tsx
@@ -5,21 +5,33 @@
"use client";
+import { Button } from "@app/components/ui/button";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
+import { useState } from "react";
export default function LicenseViolation() {
const { licenseStatus } = useLicenseStatusContext();
+ const [isDismissed, setIsDismissed] = useState(false);
- if (!licenseStatus) return null;
+ if (!licenseStatus || isDismissed) return null;
// Show invalid license banner
if (licenseStatus.isHostLicensed && !licenseStatus.isLicenseValid) {
return (
- Invalid or expired license keys detected. Follow license
- terms to continue using all features.
-
+ Invalid or expired license keys detected. Follow license
+ terms to continue using all features.
+
- License Violation: This server is using{" "}
- {licenseStatus.usedSites} sites which exceeds its licensed
- limit of {licenseStatus.maxSites} sites. Follow license
- terms to continue using all features.
-
+ License Violation: This server is using{" "}
+ {licenseStatus.usedSites} sites which exceeds its
+ licensed limit of {licenseStatus.maxSites} sites. Follow
+ license terms to continue using all features.
+