Compare commits
No commits in common. "21f13260456b45bd24bae3cfa7dfbe5225ad1630" and "cb431f3574ae7a431be7a5528084fcadb79439b8" have entirely different histories.
21f1326045
...
cb431f3574
33 changed files with 191 additions and 369 deletions
18
README.md
18
README.md
|
@ -61,8 +61,7 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
|
||||||
- **Temporary, self-destructing share links.**
|
- **Temporary, self-destructing share links.**
|
||||||
- Resource specific pin codes.
|
- Resource specific pin codes.
|
||||||
- Resource specific passwords.
|
- Resource specific passwords.
|
||||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
- OIDC Support for IDPs like Authentik
|
||||||
- Auto-provision users and roles from your IdP.
|
|
||||||
|
|
||||||
### Simple Dashboard UI
|
### Simple Dashboard UI
|
||||||
|
|
||||||
|
@ -77,9 +76,6 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
|
||||||
- **Docker Compose based setup** for simplified deployment.
|
- **Docker Compose based setup** for simplified deployment.
|
||||||
- Future-proof installation script for streamlined setup and feature additions.
|
- Future-proof installation script for streamlined setup and feature additions.
|
||||||
- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience.
|
- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience.
|
||||||
- Use the API to create custom integrations and scripts.
|
|
||||||
- Fine-grained access control to the API via scoped API keys.
|
|
||||||
- Comprehensive Swagger documentation for the API.
|
|
||||||
|
|
||||||
### Modular Design
|
### Modular Design
|
||||||
|
|
||||||
|
@ -116,19 +112,23 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
|
||||||
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
|
**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.
|
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**:
|
**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.
|
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**:
|
**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.
|
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.
|
||||||
|
|
||||||
|
<img src="public/screenshots/resources.png" alt="Resources"/>
|
||||||
|
|
||||||
|
_Resources page of Pangolin dashboard (dark mode) showing HTTPS and TCP resources with access control rules._
|
||||||
|
|
||||||
## Similar Projects and Inspirations
|
## Similar Projects and Inspirations
|
||||||
|
|
||||||
**Cloudflare Tunnels**:
|
**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.
|
||||||
|
|
||||||
**Authelia**:
|
**Authentik and Authelia**:
|
||||||
This inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
These projects inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
||||||
|
|
||||||
## Project Development / Roadmap
|
## Project Development / Roadmap
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ server:
|
||||||
internal_hostname: "pangolin"
|
internal_hostname: "pangolin"
|
||||||
session_cookie_name: "p_session_token"
|
session_cookie_name: "p_session_token"
|
||||||
resource_access_token_param: "p_token"
|
resource_access_token_param: "p_token"
|
||||||
secret: "your_secret_key_here"
|
|
||||||
resource_access_token_headers:
|
resource_access_token_headers:
|
||||||
id: "P-Access-Token-Id"
|
id: "P-Access-Token-Id"
|
||||||
token: "P-Access-Token"
|
token: "P-Access-Token"
|
||||||
|
|
|
@ -35,7 +35,7 @@ services:
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.3.6
|
image: traefik:v3.3.5
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
|
|
|
@ -202,7 +202,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunned connections", true)
|
||||||
|
|
||||||
// Admin user configuration
|
// Admin user configuration
|
||||||
fmt.Println("\n=== Admin User Configuration ===")
|
fmt.Println("\n=== Admin User Configuration ===")
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 1.1 MiB |
|
@ -197,10 +197,8 @@ export function isIpInCidr(ip: string, cidr: string): boolean {
|
||||||
const ipVersion = detectIpVersion(ip);
|
const ipVersion = detectIpVersion(ip);
|
||||||
const cidrVersion = detectIpVersion(cidr.split('/')[0]);
|
const cidrVersion = detectIpVersion(cidr.split('/')[0]);
|
||||||
|
|
||||||
// If IP versions don't match, the IP cannot be in the CIDR range
|
|
||||||
if (ipVersion !== cidrVersion) {
|
if (ipVersion !== cidrVersion) {
|
||||||
// throw new Erorr
|
throw new Error('IP address and CIDR must be of the same version');
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ipBigInt = ipToBigInt(ip);
|
const ipBigInt = ipToBigInt(ip);
|
||||||
|
|
|
@ -13,19 +13,12 @@ import moment from "moment";
|
||||||
import { setHostMeta } from "@server/setup/setHostMeta";
|
import { setHostMeta } from "@server/setup/setHostMeta";
|
||||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
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 = {
|
export type LicenseStatus = {
|
||||||
isHostLicensed: boolean; // Are there any license keys?
|
isHostLicensed: boolean; // Are there any license keys?
|
||||||
isLicenseValid: boolean; // Is the license key valid?
|
isLicenseValid: boolean; // Is the license key valid?
|
||||||
hostId: string; // Host ID
|
hostId: string; // Host ID
|
||||||
maxSites?: number;
|
maxSites?: number;
|
||||||
usedSites?: number;
|
usedSites?: number;
|
||||||
tier?: KeyTier;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LicenseKeyCache = {
|
export type LicenseKeyCache = {
|
||||||
|
@ -33,8 +26,7 @@ export type LicenseKeyCache = {
|
||||||
licenseKeyEncrypted: string;
|
licenseKeyEncrypted: string;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
iat?: Date;
|
iat?: Date;
|
||||||
type?: KeyType;
|
type?: "LICENSE" | "SITES";
|
||||||
tier?: KeyTier;
|
|
||||||
numSites?: number;
|
numSites?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,8 +54,7 @@ type ValidateLicenseAPIResponse = {
|
||||||
|
|
||||||
type TokenPayload = {
|
type TokenPayload = {
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
type: KeyType;
|
type: "LICENSE" | "SITES";
|
||||||
tier: KeyTier;
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
terminateAt: string; // ISO
|
terminateAt: string; // ISO
|
||||||
iat: number; // Issued at
|
iat: number; // Issued at
|
||||||
|
@ -191,12 +182,11 @@ LQIDAQAB
|
||||||
licenseKeyEncrypted: key.licenseKeyId,
|
licenseKeyEncrypted: key.licenseKeyId,
|
||||||
valid: payload.valid,
|
valid: payload.valid,
|
||||||
type: payload.type,
|
type: payload.type,
|
||||||
tier: payload.tier,
|
|
||||||
numSites: payload.quantity,
|
numSites: payload.quantity,
|
||||||
iat: new Date(payload.iat * 1000)
|
iat: new Date(payload.iat * 1000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.type === "HOST") {
|
if (payload.type === "LICENSE") {
|
||||||
foundHostKey = true;
|
foundHostKey = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -283,7 +273,6 @@ LQIDAQAB
|
||||||
);
|
);
|
||||||
cached.valid = payload.valid;
|
cached.valid = payload.valid;
|
||||||
cached.type = payload.type;
|
cached.type = payload.type;
|
||||||
cached.tier = payload.tier;
|
|
||||||
cached.numSites = payload.quantity;
|
cached.numSites = payload.quantity;
|
||||||
cached.iat = new Date(payload.iat * 1000);
|
cached.iat = new Date(payload.iat * 1000);
|
||||||
|
|
||||||
|
@ -322,9 +311,8 @@ LQIDAQAB
|
||||||
|
|
||||||
logger.debug("Checking key", cached);
|
logger.debug("Checking key", cached);
|
||||||
|
|
||||||
if (cached.type === "HOST") {
|
if (cached.type === "LICENSE") {
|
||||||
status.isLicenseValid = cached.valid;
|
status.isLicenseValid = cached.valid;
|
||||||
status.tier = cached.tier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cached.valid) {
|
if (!cached.valid) {
|
||||||
|
|
|
@ -172,20 +172,9 @@ export async function listAccessTokens(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { resourceId } = parsedParams.data;
|
const { orgId, resourceId } = parsedParams.data;
|
||||||
|
|
||||||
const orgId =
|
if (orgId && orgId !== req.userOrgId) {
|
||||||
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(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
@ -194,9 +183,7 @@ export async function listAccessTokens(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accessibleResources;
|
const accessibleResources = await db
|
||||||
if (req.user) {
|
|
||||||
accessibleResources = await db
|
|
||||||
.select({
|
.select({
|
||||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||||
})
|
})
|
||||||
|
@ -211,12 +198,6 @@ export async function listAccessTokens(
|
||||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
accessibleResources = await db
|
|
||||||
.select({ resourceId: resources.resourceId })
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.orgId, orgId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessibleResourceIds = accessibleResources.map(
|
const accessibleResourceIds = accessibleResources.map(
|
||||||
(resource) => resource.resourceId
|
(resource) => resource.resourceId
|
||||||
|
|
|
@ -28,7 +28,7 @@ const bodySchema = z
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
const ensureTrailingSlash = (url: string): string => {
|
const ensureTrailingSlash = (url: string): string => {
|
||||||
return url;
|
return url.endsWith('/') ? url : `${url}/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GenerateOidcUrlResponse = {
|
export type GenerateOidcUrlResponse = {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { oidcAutoProvision } from "./oidcAutoProvision";
|
||||||
import license from "@server/license/license";
|
import license from "@server/license/license";
|
||||||
|
|
||||||
const ensureTrailingSlash = (url: string): string => {
|
const ensureTrailingSlash = (url: string): string => {
|
||||||
return url;
|
return url.endsWith("/") ? url : `${url}/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
|
@ -243,7 +243,7 @@ export async function validateOidcCallback(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.UNAUTHORIZED,
|
HttpCode.UNAUTHORIZED,
|
||||||
`User with username ${userIdentifier} is unprovisioned. This user must be added to an organization before logging in.`
|
"User not provisioned in the system"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ export async function createNewt(
|
||||||
|
|
||||||
const { newtId, secret } = parsedBody.data;
|
const { newtId, secret } = parsedBody.data;
|
||||||
|
|
||||||
if (req.user && !req.userOrgRoleId) {
|
if (!req.userOrgRoleId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,16 +3,13 @@ import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
apiKeyOrg,
|
|
||||||
apiKeys,
|
|
||||||
domains,
|
domains,
|
||||||
Org,
|
Org,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
orgs,
|
orgs,
|
||||||
roleActions,
|
roleActions,
|
||||||
roles,
|
roles,
|
||||||
userOrgs,
|
userOrgs
|
||||||
users
|
|
||||||
} from "@server/db/schemas";
|
} from "@server/db/schemas";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
@ -58,7 +55,7 @@ export async function createOrg(
|
||||||
try {
|
try {
|
||||||
// should this be in a middleware?
|
// should this be in a middleware?
|
||||||
if (config.getRawConfig().flags?.disable_user_create_org) {
|
if (config.getRawConfig().flags?.disable_user_create_org) {
|
||||||
if (req.user && !req.user?.serverAdmin) {
|
if (!req.user?.serverAdmin) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
@ -146,33 +143,12 @@ export async function createOrg(
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (req.user) {
|
|
||||||
await trx.insert(userOrgs).values({
|
await trx.insert(userOrgs).values({
|
||||||
userId: req.user!.userId,
|
userId: req.user!.userId,
|
||||||
orgId: newOrg[0].orgId,
|
orgId: newOrg[0].orgId,
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
isOwner: true
|
isOwner: true
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// if org created by root api key, set the server admin as the owner
|
|
||||||
const [serverAdmin] = await trx
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.serverAdmin, true));
|
|
||||||
|
|
||||||
if (!serverAdmin) {
|
|
||||||
error = "Server admin not found";
|
|
||||||
trx.rollback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await trx.insert(userOrgs).values({
|
|
||||||
userId: serverAdmin.userId,
|
|
||||||
orgId: newOrg[0].orgId,
|
|
||||||
roleId: roleId,
|
|
||||||
isOwner: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberRole = await trx
|
const memberRole = await trx
|
||||||
.insert(roles)
|
.insert(roles)
|
||||||
|
@ -190,18 +166,6 @@ export async function createOrg(
|
||||||
orgId
|
orgId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const rootApiKeys = await trx
|
|
||||||
.select()
|
|
||||||
.from(apiKeys)
|
|
||||||
.where(eq(apiKeys.isRoot, true));
|
|
||||||
|
|
||||||
for (const apiKey of rootApiKeys) {
|
|
||||||
await trx.insert(apiKeyOrg).values({
|
|
||||||
apiKeyId: apiKey.apiKeyId,
|
|
||||||
orgId: newOrg[0].orgId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
|
|
|
@ -39,7 +39,6 @@ const createHttpResourceSchema = z
|
||||||
isBaseDomain: z.boolean().optional(),
|
isBaseDomain: z.boolean().optional(),
|
||||||
siteId: z.number(),
|
siteId: z.number(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.string(),
|
|
||||||
domainId: z.string()
|
domainId: z.string()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
@ -130,7 +129,7 @@ export async function createResource(
|
||||||
|
|
||||||
const { siteId, orgId } = parsedParams.data;
|
const { siteId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && !req.userOrgRoleId) {
|
if (!req.userOrgRoleId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
);
|
);
|
||||||
|
@ -203,7 +202,7 @@ async function createHttpResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, subdomain, isBaseDomain, http, protocol, domainId } =
|
const { name, subdomain, isBaseDomain, http, domainId } =
|
||||||
parsedBody.data;
|
parsedBody.data;
|
||||||
|
|
||||||
const [orgDomain] = await db
|
const [orgDomain] = await db
|
||||||
|
@ -262,7 +261,7 @@ async function createHttpResource(
|
||||||
name,
|
name,
|
||||||
subdomain,
|
subdomain,
|
||||||
http,
|
http,
|
||||||
protocol,
|
protocol: "tcp",
|
||||||
ssl: true,
|
ssl: true,
|
||||||
isBaseDomain
|
isBaseDomain
|
||||||
})
|
})
|
||||||
|
@ -285,7 +284,7 @@ async function createHttpResource(
|
||||||
resourceId: newResource[0].resourceId
|
resourceId: newResource[0].resourceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||||
// make sure the user can access the resource
|
// make sure the user can access the resource
|
||||||
await trx.insert(userResources).values({
|
await trx.insert(userResources).values({
|
||||||
userId: req.user?.userId!,
|
userId: req.user?.userId!,
|
||||||
|
|
|
@ -69,7 +69,9 @@ function queryResources(
|
||||||
http: resources.http,
|
http: resources.http,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
proxyPort: resources.proxyPort,
|
proxyPort: resources.proxyPort,
|
||||||
enabled: resources.enabled
|
enabled: resources.enabled,
|
||||||
|
tlsServerName: resources.tlsServerName,
|
||||||
|
setHostHeader: resources.setHostHeader
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||||
|
@ -103,7 +105,9 @@ function queryResources(
|
||||||
http: resources.http,
|
http: resources.http,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
proxyPort: resources.proxyPort,
|
proxyPort: resources.proxyPort,
|
||||||
enabled: resources.enabled
|
enabled: resources.enabled,
|
||||||
|
tlsServerName: resources.tlsServerName,
|
||||||
|
setHostHeader: resources.setHostHeader
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||||
|
@ -183,17 +187,9 @@ export async function listResources(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { siteId } = parsedParams.data;
|
const { siteId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
const orgId = parsedParams.data.orgId || req.userOrg?.orgId || req.apiKeyOrg?.orgId;
|
if (orgId && orgId !== req.userOrgId) {
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
@ -202,9 +198,7 @@ export async function listResources(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accessibleResources;
|
const accessibleResources = await db
|
||||||
if (req.user) {
|
|
||||||
accessibleResources = await db
|
|
||||||
.select({
|
.select({
|
||||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||||
})
|
})
|
||||||
|
@ -219,11 +213,6 @@ export async function listResources(
|
||||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
accessibleResources = await db.select({
|
|
||||||
resourceId: resources.resourceId
|
|
||||||
}).from(resources).where(eq(resources.orgId, orgId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessibleResourceIds = accessibleResources.map(
|
const accessibleResourceIds = accessibleResources.map(
|
||||||
(resource) => resource.resourceId
|
(resource) => resource.resourceId
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { apiKeys, roleResources, roles } from "@server/db/schemas";
|
import { roleResources, roles } from "@server/db/schemas";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
|
@ -74,17 +74,6 @@ export async function setResourceRoles(
|
||||||
|
|
||||||
const { resourceId } = parsedParams.data;
|
const { resourceId } = parsedParams.data;
|
||||||
|
|
||||||
const orgId = req.userOrg?.orgId || req.apiKeyOrg?.orgId;
|
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Organization not found"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get this org's admin role
|
// get this org's admin role
|
||||||
const adminRole = await db
|
const adminRole = await db
|
||||||
.select()
|
.select()
|
||||||
|
@ -92,7 +81,7 @@ export async function setResourceRoles(
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(roles.name, "Admin"),
|
eq(roles.name, "Admin"),
|
||||||
eq(roles.orgId, orgId)
|
eq(roles.orgId, req.userOrg!.orgId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
@ -147,4 +136,3 @@ export async function setResourceRoles(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,8 +45,8 @@ const updateHttpResourceBodySchema = z
|
||||||
domainId: z.string().optional(),
|
domainId: z.string().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
stickySession: z.boolean().optional(),
|
stickySession: z.boolean().optional(),
|
||||||
tlsServerName: z.string().nullable().optional(),
|
tlsServerName: z.string().optional(),
|
||||||
setHostHeader: z.string().nullable().optional()
|
setHostHeader: z.string().optional()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db/schemas";
|
import { roles, userSites, sites, roleSites, Site } from "@server/db/schemas";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
|
@ -10,6 +10,7 @@ import { eq, and } from "drizzle-orm";
|
||||||
import { getUniqueSiteName } from "@server/db/names";
|
import { getUniqueSiteName } from "@server/db/names";
|
||||||
import { addPeer } from "../gerbil/peers";
|
import { addPeer } from "../gerbil/peers";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { hash } from "@node-rs/argon2";
|
||||||
import { newts } from "@server/db/schemas";
|
import { newts } from "@server/db/schemas";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
@ -77,15 +78,8 @@ export async function createSite(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { name, type, exitNodeId, pubKey, subnet, newtId, secret } =
|
||||||
name,
|
parsedBody.data;
|
||||||
type,
|
|
||||||
exitNodeId,
|
|
||||||
pubKey,
|
|
||||||
subnet,
|
|
||||||
newtId,
|
|
||||||
secret
|
|
||||||
} = parsedBody.data;
|
|
||||||
|
|
||||||
const parsedParams = createSiteParamsSchema.safeParse(req.params);
|
const parsedParams = createSiteParamsSchema.safeParse(req.params);
|
||||||
if (!parsedParams.success) {
|
if (!parsedParams.success) {
|
||||||
|
@ -99,23 +93,12 @@ export async function createSite(
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && !req.userOrgRoleId) {
|
if (!req.userOrgRoleId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
|
||||||
|
|
||||||
if (!org) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Organization with ID ${orgId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const niceId = await getUniqueSiteName(orgId);
|
const niceId = await getUniqueSiteName(orgId);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
|
@ -176,7 +159,7 @@ export async function createSite(
|
||||||
siteId: newSite.siteId
|
siteId: newSite.siteId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||||
// make sure the user can access the site
|
// make sure the user can access the site
|
||||||
trx.insert(userSites).values({
|
trx.insert(userSites).values({
|
||||||
userId: req.user?.userId!,
|
userId: req.user?.userId!,
|
||||||
|
|
|
@ -100,7 +100,7 @@ export async function listSites(
|
||||||
}
|
}
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
if (orgId && orgId !== req.userOrgId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
@ -109,9 +109,7 @@ export async function listSites(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accessibleSites;
|
const accessibleSites = await db
|
||||||
if (req.user) {
|
|
||||||
accessibleSites = await db
|
|
||||||
.select({
|
.select({
|
||||||
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
|
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
|
||||||
})
|
})
|
||||||
|
@ -123,12 +121,6 @@ export async function listSites(
|
||||||
eq(roleSites.roleId, req.userOrgRoleId!)
|
eq(roleSites.roleId, req.userOrgRoleId!)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
accessibleSites = await db
|
|
||||||
.select({ siteId: sites.siteId })
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.orgId, orgId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
||||||
const baseQuery = querySites(orgId, accessibleSiteIds);
|
const baseQuery = querySites(orgId, accessibleSiteIds);
|
||||||
|
|
|
@ -49,7 +49,7 @@ export async function addUserRole(
|
||||||
|
|
||||||
const { userId, roleId } = parsedParams.data;
|
const { userId, roleId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && !req.userOrg) {
|
if (!req.userOrg) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
@ -58,13 +58,7 @@ export async function addUserRole(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgId = req.userOrg?.orgId || req.apiKeyOrg?.orgId;
|
const orgId = req.userOrg.orgId;
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingUser = await db
|
const existingUser = await db
|
||||||
.select()
|
.select()
|
||||||
|
|
|
@ -106,7 +106,7 @@ export async function getOrgUser(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.user && user.userId !== req.userOrg.userId) {
|
if (user.userId !== req.userOrg.userId) {
|
||||||
const hasPermission = await checkUserActionPermission(
|
const hasPermission = await checkUserActionPermission(
|
||||||
ActionsEnum.getOrgUser,
|
ActionsEnum.getOrgUser,
|
||||||
req
|
req
|
||||||
|
|
|
@ -363,12 +363,12 @@ export default function ReverseProxyTargets(props: {
|
||||||
setHttpsTlsLoading(true);
|
setHttpsTlsLoading(true);
|
||||||
await api.post(`/resource/${params.resourceId}`, {
|
await api.post(`/resource/${params.resourceId}`, {
|
||||||
ssl: data.ssl,
|
ssl: data.ssl,
|
||||||
tlsServerName: data.tlsServerName || null
|
tlsServerName: data.tlsServerName || undefined
|
||||||
});
|
});
|
||||||
updateResource({
|
updateResource({
|
||||||
...resource,
|
...resource,
|
||||||
ssl: data.ssl,
|
ssl: data.ssl,
|
||||||
tlsServerName: data.tlsServerName || null
|
tlsServerName: data.tlsServerName || undefined
|
||||||
});
|
});
|
||||||
toast({
|
toast({
|
||||||
title: "TLS settings updated",
|
title: "TLS settings updated",
|
||||||
|
@ -393,11 +393,11 @@ export default function ReverseProxyTargets(props: {
|
||||||
try {
|
try {
|
||||||
setProxySettingsLoading(true);
|
setProxySettingsLoading(true);
|
||||||
await api.post(`/resource/${params.resourceId}`, {
|
await api.post(`/resource/${params.resourceId}`, {
|
||||||
setHostHeader: data.setHostHeader || null
|
setHostHeader: data.setHostHeader || undefined
|
||||||
});
|
});
|
||||||
updateResource({
|
updateResource({
|
||||||
...resource,
|
...resource,
|
||||||
setHostHeader: data.setHostHeader || null
|
setHostHeader: data.setHostHeader || undefined
|
||||||
});
|
});
|
||||||
toast({
|
toast({
|
||||||
title: "Proxy settings updated",
|
title: "Proxy settings updated",
|
||||||
|
@ -657,7 +657,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
loading={httpsTlsLoading}
|
loading={httpsTlsLoading}
|
||||||
form="tls-settings-form"
|
form="tls-settings-form"
|
||||||
>
|
>
|
||||||
Save Settings
|
Save HTTPS & TLS Settings
|
||||||
</Button>
|
</Button>
|
||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
@ -896,7 +896,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The host header to set when
|
The Host header to set when
|
||||||
proxying requests. Leave
|
proxying requests. Leave
|
||||||
empty to use the default.
|
empty to use the default.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|
|
@ -173,15 +173,13 @@ export default function Page() {
|
||||||
if (httpData.isBaseDomain) {
|
if (httpData.isBaseDomain) {
|
||||||
Object.assign(payload, {
|
Object.assign(payload, {
|
||||||
domainId: httpData.domainId,
|
domainId: httpData.domainId,
|
||||||
isBaseDomain: true,
|
isBaseDomain: true
|
||||||
protocol: "tcp"
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.assign(payload, {
|
Object.assign(payload, {
|
||||||
subdomain: httpData.subdomain,
|
subdomain: httpData.subdomain,
|
||||||
domainId: httpData.domainId,
|
domainId: httpData.domainId,
|
||||||
isBaseDomain: false,
|
isBaseDomain: false
|
||||||
protocol: "tcp"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -40,21 +40,6 @@ export function SitePriceCalculator({
|
||||||
setSiteCount((prev) => (prev > 1 ? prev - 1 : 1));
|
setSiteCount((prev) => (prev > 1 ? prev - 1 : 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
function continueToPayment() {
|
|
||||||
if (mode === "license") {
|
|
||||||
// open in new tab
|
|
||||||
window.open(
|
|
||||||
`https://payment.fossorial.io/buy/dab98d3d-9976-49b1-9e55-1580059d833f?quantity=${siteCount}`,
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.open(
|
|
||||||
`https://payment.fossorial.io/buy/2b881c36-ea5d-4c11-8652-9be6810a054f?quantity=${siteCount}`,
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalCost =
|
const totalCost =
|
||||||
mode === "license"
|
mode === "license"
|
||||||
? licenseFlatRate + siteCount * pricePerSite
|
? licenseFlatRate + siteCount * pricePerSite
|
||||||
|
@ -137,8 +122,8 @@ export function SitePriceCalculator({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm mt-2 text-center">
|
<p className="text-muted-foreground text-sm mt-2 text-center">
|
||||||
For the most up-to-date pricing and discounts,
|
For the most up-to-date pricing, please visit
|
||||||
please visit the{" "}
|
our{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.fossorial.io/pricing"
|
href="https://docs.fossorial.io/pricing"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -156,9 +141,7 @@ export function SitePriceCalculator({
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="outline">Cancel</Button>
|
<Button variant="outline">Cancel</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
<Button onClick={continueToPayment}>
|
<Button>Continue to Payment</Button>
|
||||||
Continue to Payment
|
|
||||||
</Button>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -121,7 +121,7 @@ export default function LicensePage() {
|
||||||
);
|
);
|
||||||
const keys = response.data.data;
|
const keys = response.data.data;
|
||||||
setRows(keys);
|
setRows(keys);
|
||||||
const hostKey = keys.find((key) => key.type === "HOST");
|
const hostKey = keys.find((key) => key.type === "LICENSE");
|
||||||
if (hostKey) {
|
if (hostKey) {
|
||||||
setHostLicense(hostKey.licenseKey);
|
setHostLicense(hostKey.licenseKey);
|
||||||
} else {
|
} else {
|
||||||
|
@ -285,22 +285,17 @@ export default function LicensePage() {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="space-y-1 leading-none">
|
<div className="space-y-1 leading-none">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
By checking this box, you
|
I have read and agree to the
|
||||||
confirm that you have read
|
Fossorial Commercial License
|
||||||
and agree to the license
|
- Professional Edition
|
||||||
terms corresponding to the
|
Subscription Terms.{" "}
|
||||||
tier associated with your
|
|
||||||
license key.
|
|
||||||
<br />
|
|
||||||
<Link
|
<Link
|
||||||
href="https://fossorial.io/license.html"
|
href="https://docs.fossorial.io/license.html"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline"
|
className="text-primary hover:underline"
|
||||||
>
|
>
|
||||||
View Fossorial
|
View License & Terms
|
||||||
Commercial License &
|
|
||||||
Subscription Terms
|
|
||||||
</Link>
|
</Link>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -385,13 +380,7 @@ export default function LicensePage() {
|
||||||
<div className="space-y-2 text-green-500">
|
<div className="space-y-2 text-green-500">
|
||||||
<div className="text-2xl flex items-center gap-2">
|
<div className="text-2xl flex items-center gap-2">
|
||||||
<Check />
|
<Check />
|
||||||
{licenseStatus?.tier ===
|
Licensed
|
||||||
"PROFESSIONAL"
|
|
||||||
? "Professional License"
|
|
||||||
: licenseStatus?.tier ===
|
|
||||||
"ENTERPRISE"
|
|
||||||
? "Enterprise License"
|
|
||||||
: "Licensed"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -452,12 +441,6 @@ export default function LicensePage() {
|
||||||
in system
|
in system
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!licenseStatus?.isHostLicensed && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
There is no limit on the number of sites
|
|
||||||
using an unlicensed host.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{licenseStatus?.maxSites && (
|
{licenseStatus?.maxSites && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
|
|
|
@ -173,7 +173,7 @@ export default function UsersTable({ users }: Props) {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to permanently delete{" "}
|
Are you sure you want to permanently delete{" "}
|
||||||
<b className="break-all">
|
<b>
|
||||||
{selected?.email ||
|
{selected?.email ||
|
||||||
selected?.name ||
|
selected?.name ||
|
||||||
selected?.username}
|
selected?.username}
|
||||||
|
|
|
@ -5,33 +5,21 @@
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export default function LicenseViolation() {
|
export default function LicenseViolation() {
|
||||||
const { licenseStatus } = useLicenseStatusContext();
|
const { licenseStatus } = useLicenseStatusContext();
|
||||||
const [isDismissed, setIsDismissed] = useState(false);
|
|
||||||
|
|
||||||
if (!licenseStatus || isDismissed) return null;
|
if (!licenseStatus) return null;
|
||||||
|
|
||||||
// Show invalid license banner
|
// Show invalid license banner
|
||||||
if (licenseStatus.isHostLicensed && !licenseStatus.isLicenseValid) {
|
if (licenseStatus.isHostLicensed && !licenseStatus.isLicenseValid) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 w-full bg-red-500 text-white p-4 text-center z-50">
|
<div className="fixed bottom-0 left-0 right-0 w-full bg-red-500 text-white p-4 text-center z-50">
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<p>
|
<p>
|
||||||
Invalid or expired license keys detected. Follow license
|
Invalid or expired license keys detected. Follow license
|
||||||
terms to continue using all features.
|
terms to continue using all features.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
|
||||||
variant={"ghost"}
|
|
||||||
className="hover:bg-yellow-500"
|
|
||||||
onClick={() => setIsDismissed(true)}
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -44,21 +32,12 @@ export default function LicenseViolation() {
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 w-full bg-yellow-500 text-black p-4 text-center z-50">
|
<div className="fixed bottom-0 left-0 right-0 w-full bg-yellow-500 text-black p-4 text-center z-50">
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<p>
|
<p>
|
||||||
License Violation: This server is using{" "}
|
License Violation: This server is using{" "}
|
||||||
{licenseStatus.usedSites} sites which exceeds its
|
{licenseStatus.usedSites} sites which exceeds its licensed
|
||||||
licensed limit of {licenseStatus.maxSites} sites. Follow
|
limit of {licenseStatus.maxSites} sites. Follow license
|
||||||
license terms to continue using all features.
|
terms to continue using all features.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
|
||||||
variant={"ghost"}
|
|
||||||
className="hover:bg-yellow-500"
|
|
||||||
onClick={() => setIsDismissed(true)}
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,33 @@ export function Breadcrumbs() {
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItem[] = segments.map((segment, index) => {
|
const breadcrumbs: BreadcrumbItem[] = segments.map((segment, index) => {
|
||||||
const href = `/${segments.slice(0, index + 1).join("/")}`;
|
const href = `/${segments.slice(0, index + 1).join("/")}`;
|
||||||
let label = decodeURIComponent(segment);
|
let label = segment;
|
||||||
|
|
||||||
|
// // Format labels
|
||||||
|
// if (segment === "settings") {
|
||||||
|
// label = "Settings";
|
||||||
|
// } else if (segment === "sites") {
|
||||||
|
// label = "Sites";
|
||||||
|
// } else if (segment === "resources") {
|
||||||
|
// label = "Resources";
|
||||||
|
// } else if (segment === "access") {
|
||||||
|
// label = "Access Control";
|
||||||
|
// } else if (segment === "general") {
|
||||||
|
// label = "General";
|
||||||
|
// } else if (segment === "share-links") {
|
||||||
|
// label = "Shareable Links";
|
||||||
|
// } else if (segment === "users") {
|
||||||
|
// label = "Users";
|
||||||
|
// } else if (segment === "roles") {
|
||||||
|
// label = "Roles";
|
||||||
|
// } else if (segment === "invitations") {
|
||||||
|
// label = "Invitations";
|
||||||
|
// } else if (segment === "proxy") {
|
||||||
|
// label = "proxy";
|
||||||
|
// } else if (segment === "authentication") {
|
||||||
|
// label = "Authentication";
|
||||||
|
// }
|
||||||
|
|
||||||
return { label, href };
|
return { label, href };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default function InviteUserForm({
|
||||||
<CredenzaTitle>{title}</CredenzaTitle>
|
<CredenzaTitle>{title}</CredenzaTitle>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<div className="mb-4 break-all overflow-hidden">{dialog}</div>
|
<div className="mb-4">{dialog}</div>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
|
|
@ -248,12 +248,6 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
||||||
pattern={
|
pattern={
|
||||||
REGEXP_ONLY_DIGITS_AND_CHARS
|
REGEXP_ONLY_DIGITS_AND_CHARS
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(e);
|
|
||||||
if (e.length === 6) {
|
|
||||||
mfaForm.handleSubmit(onSubmit)();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<InputOTPGroup>
|
<InputOTPGroup>
|
||||||
<InputOTPSlot
|
<InputOTPSlot
|
||||||
|
|
|
@ -27,6 +27,7 @@ function getActionsCategories(root: boolean) {
|
||||||
"Get Organization User": "getOrgUser",
|
"Get Organization User": "getOrgUser",
|
||||||
"List Organization Domains": "listOrgDomains",
|
"List Organization Domains": "listOrgDomains",
|
||||||
"Check Org ID": "checkOrgId",
|
"Check Org ID": "checkOrgId",
|
||||||
|
"List Orgs": "listOrgs"
|
||||||
},
|
},
|
||||||
|
|
||||||
Site: {
|
Site: {
|
||||||
|
@ -90,12 +91,14 @@ function getActionsCategories(root: boolean) {
|
||||||
"List Resource Rules": "listResourceRules",
|
"List Resource Rules": "listResourceRules",
|
||||||
"Update Resource Rule": "updateResourceRule"
|
"Update Resource Rule": "updateResourceRule"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Newt": {
|
||||||
|
// "Create Newt": "createNewt"
|
||||||
|
// },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (root) {
|
if (root) {
|
||||||
actionsByCategory["Organization"] = {
|
actionsByCategory["Organization"] = {
|
||||||
"List Organizations": "listOrgs",
|
|
||||||
"Check ID": "checkOrgId",
|
|
||||||
"Create Organization": "createOrg",
|
"Create Organization": "createOrg",
|
||||||
"Delete Organization": "deleteOrg",
|
"Delete Organization": "deleteOrg",
|
||||||
"List API Keys": "listApiKeys",
|
"List API Keys": "listApiKeys",
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
export default function QRContainer({
|
|
||||||
children = <div/>,
|
|
||||||
outline = true
|
|
||||||
}) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`relative w-fit border-2 rounded-md`}
|
|
||||||
>
|
|
||||||
<div className="bg-white p-6 rounded-md">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -189,12 +189,10 @@ export default function SupporterStatus() {
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<p>
|
<p>
|
||||||
Purchase a supporter key to help us continue
|
Purchase a supporter key to help us continue
|
||||||
developing Pangolin for the community. Your
|
developing Pangolin. Your contribution allows us
|
||||||
contribution allows us to commit more time to
|
commit more time to maintain and add new features to
|
||||||
maintain and add new features to the application for
|
the application for everyone. We will never use this
|
||||||
everyone. We will never use this to paywall
|
to paywall features.
|
||||||
features. This is separate from the Professional
|
|
||||||
Edition.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
|
||||||
<ToastPrimitives.Viewport
|
<ToastPrimitives.Viewport
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-0 right-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 md:max-w-[420px]",
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
Loading…
Add table
Reference in a new issue