mirror of
https://github.com/fosrl/pangolin.git
synced 2025-06-24 14:18:55 +02:00
add user checks in routes
This commit is contained in:
parent
f8e0219b49
commit
a9f0b9aa38
21 changed files with 302 additions and 133 deletions
15
README.md
15
README.md
|
@ -61,7 +61,8 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
|
|||
- **Temporary, self-destructing share links.**
|
||||
- Resource specific pin codes.
|
||||
- Resource specific passwords.
|
||||
- OIDC Support for IDPs like Authentik
|
||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
||||
- Auto-provision users and roles from your IdP.
|
||||
|
||||
### Simple Dashboard UI
|
||||
|
||||
|
@ -77,6 +78,8 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
|
|||
- 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 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
|
||||
|
||||
|
@ -113,14 +116,12 @@ _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.
|
||||
|
||||
<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
|
||||
|
@ -128,8 +129,8 @@ _Resources page of Pangolin dashboard (dark mode) showing HTTPS and TCP resource
|
|||
**Cloudflare Tunnels**:
|
||||
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
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 574 KiB |
|
@ -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) {
|
||||
|
|
|
@ -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,7 +194,9 @@ export async function listAccessTokens(
|
|||
);
|
||||
}
|
||||
|
||||
const accessibleResources = await db
|
||||
let accessibleResources;
|
||||
if (req.user) {
|
||||
accessibleResources = await db
|
||||
.select({
|
||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||
})
|
||||
|
@ -198,6 +211,12 @@ export async function listAccessTokens(
|
|||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleResources = await db
|
||||
.select({ resourceId: resources.resourceId })
|
||||
.from(resources)
|
||||
.where(eq(resources.orgId, orgId));
|
||||
}
|
||||
|
||||
const accessibleResourceIds = accessibleResources.map(
|
||||
(resource) => resource.resourceId
|
||||
|
|
|
@ -49,7 +49,7 @@ export async function createNewt(
|
|||
|
||||
const { newtId, secret } = parsedBody.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
|
|
@ -3,13 +3,16 @@ import { z } from "zod";
|
|||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
apiKeyOrg,
|
||||
apiKeys,
|
||||
domains,
|
||||
Org,
|
||||
orgDomains,
|
||||
orgs,
|
||||
roleActions,
|
||||
roles,
|
||||
userOrgs
|
||||
userOrgs,
|
||||
users
|
||||
} from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
@ -55,7 +58,7 @@ export async function createOrg(
|
|||
try {
|
||||
// should this be in a middleware?
|
||||
if (config.getRawConfig().flags?.disable_user_create_org) {
|
||||
if (!req.user?.serverAdmin) {
|
||||
if (req.user && !req.user?.serverAdmin) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -143,12 +146,33 @@ export async function createOrg(
|
|||
}))
|
||||
);
|
||||
|
||||
if (req.user) {
|
||||
await trx.insert(userOrgs).values({
|
||||
userId: req.user!.userId,
|
||||
orgId: newOrg[0].orgId,
|
||||
roleId: roleId,
|
||||
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
|
||||
.insert(roles)
|
||||
|
@ -166,6 +190,18 @@ export async function createOrg(
|
|||
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) {
|
||||
|
|
|
@ -39,6 +39,7 @@ const createHttpResourceSchema = z
|
|||
isBaseDomain: z.boolean().optional(),
|
||||
siteId: z.number(),
|
||||
http: z.boolean(),
|
||||
protocol: z.string(),
|
||||
domainId: z.string()
|
||||
})
|
||||
.strict()
|
||||
|
@ -129,7 +130,7 @@ export async function createResource(
|
|||
|
||||
const { siteId, orgId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
@ -202,7 +203,7 @@ async function createHttpResource(
|
|||
);
|
||||
}
|
||||
|
||||
const { name, subdomain, isBaseDomain, http, domainId } =
|
||||
const { name, subdomain, isBaseDomain, http, protocol, domainId } =
|
||||
parsedBody.data;
|
||||
|
||||
const [orgDomain] = await db
|
||||
|
@ -261,7 +262,7 @@ async function createHttpResource(
|
|||
name,
|
||||
subdomain,
|
||||
http,
|
||||
protocol: "tcp",
|
||||
protocol,
|
||||
ssl: true,
|
||||
isBaseDomain
|
||||
})
|
||||
|
@ -284,7 +285,7 @@ async function createHttpResource(
|
|||
resourceId: newResource[0].resourceId
|
||||
});
|
||||
|
||||
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
||||
// make sure the user can access the resource
|
||||
await trx.insert(userResources).values({
|
||||
userId: req.user?.userId!,
|
||||
|
|
|
@ -69,9 +69,7 @@ function queryResources(
|
|||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
enabled: resources.enabled,
|
||||
tlsServerName: resources.tlsServerName,
|
||||
setHostHeader: resources.setHostHeader
|
||||
enabled: resources.enabled
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
|
@ -105,9 +103,7 @@ function queryResources(
|
|||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
enabled: resources.enabled,
|
||||
tlsServerName: resources.tlsServerName,
|
||||
setHostHeader: resources.setHostHeader
|
||||
enabled: resources.enabled
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
|
@ -187,9 +183,17 @@ export async function listResources(
|
|||
)
|
||||
);
|
||||
}
|
||||
const { siteId, orgId } = parsedParams.data;
|
||||
const { siteId } = 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,
|
||||
|
@ -198,7 +202,9 @@ export async function listResources(
|
|||
);
|
||||
}
|
||||
|
||||
const accessibleResources = await db
|
||||
let accessibleResources;
|
||||
if (req.user) {
|
||||
accessibleResources = await db
|
||||
.select({
|
||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||
})
|
||||
|
@ -213,6 +219,11 @@ export async function listResources(
|
|||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleResources = await db.select({
|
||||
resourceId: resources.resourceId
|
||||
}).from(resources).where(eq(resources.orgId, orgId));
|
||||
}
|
||||
|
||||
const accessibleResourceIds = accessibleResources.map(
|
||||
(resource) => resource.resourceId
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roleResources, roles } from "@server/db/schemas";
|
||||
import { apiKeys, roleResources, roles } from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
@ -74,6 +74,17 @@ export async function setResourceRoles(
|
|||
|
||||
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
|
||||
const adminRole = await db
|
||||
.select()
|
||||
|
@ -81,7 +92,7 @@ export async function setResourceRoles(
|
|||
.where(
|
||||
and(
|
||||
eq(roles.name, "Admin"),
|
||||
eq(roles.orgId, req.userOrg!.orgId)
|
||||
eq(roles.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
@ -136,3 +147,4 @@ export async function setResourceRoles(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,8 +45,8 @@ const updateHttpResourceBodySchema = z
|
|||
domainId: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
stickySession: z.boolean().optional(),
|
||||
tlsServerName: z.string().optional(),
|
||||
setHostHeader: z.string().optional()
|
||||
tlsServerName: z.string().nullable().optional(),
|
||||
setHostHeader: z.string().nullable().optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roles, userSites, sites, roleSites, Site } from "@server/db/schemas";
|
||||
import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
@ -10,7 +10,6 @@ import { eq, and } from "drizzle-orm";
|
|||
import { getUniqueSiteName } from "@server/db/names";
|
||||
import { addPeer } from "../gerbil/peers";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import { newts } from "@server/db/schemas";
|
||||
import moment from "moment";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
@ -78,8 +77,15 @@ export async function createSite(
|
|||
);
|
||||
}
|
||||
|
||||
const { name, type, exitNodeId, pubKey, subnet, newtId, secret } =
|
||||
parsedBody.data;
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
exitNodeId,
|
||||
pubKey,
|
||||
subnet,
|
||||
newtId,
|
||||
secret
|
||||
} = parsedBody.data;
|
||||
|
||||
const parsedParams = createSiteParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
|
@ -93,12 +99,23 @@ export async function createSite(
|
|||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
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);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
|
@ -159,7 +176,7 @@ export async function createSite(
|
|||
siteId: newSite.siteId
|
||||
});
|
||||
|
||||
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
||||
// make sure the user can access the site
|
||||
trx.insert(userSites).values({
|
||||
userId: req.user?.userId!,
|
||||
|
|
|
@ -100,7 +100,7 @@ export async function listSites(
|
|||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (orgId && orgId !== req.userOrgId) {
|
||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -109,7 +109,9 @@ export async function listSites(
|
|||
);
|
||||
}
|
||||
|
||||
const accessibleSites = await db
|
||||
let accessibleSites;
|
||||
if (req.user) {
|
||||
accessibleSites = await db
|
||||
.select({
|
||||
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
|
||||
})
|
||||
|
@ -121,6 +123,12 @@ export async function listSites(
|
|||
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 baseQuery = querySites(orgId, accessibleSiteIds);
|
||||
|
|
|
@ -49,7 +49,7 @@ export async function addUserRole(
|
|||
|
||||
const { userId, roleId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrg) {
|
||||
if (req.user && !req.userOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -58,7 +58,13 @@ export async function addUserRole(
|
|||
);
|
||||
}
|
||||
|
||||
const orgId = req.userOrg.orgId;
|
||||
const orgId = req.userOrg?.orgId || req.apiKeyOrg?.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const existingUser = await db
|
||||
.select()
|
||||
|
|
|
@ -106,7 +106,7 @@ export async function getOrgUser(
|
|||
);
|
||||
}
|
||||
|
||||
if (user.userId !== req.userOrg.userId) {
|
||||
if (req.user && user.userId !== req.userOrg.userId) {
|
||||
const hasPermission = await checkUserActionPermission(
|
||||
ActionsEnum.getOrgUser,
|
||||
req
|
||||
|
|
|
@ -657,7 +657,7 @@ export default function ReverseProxyTargets(props: {
|
|||
loading={httpsTlsLoading}
|
||||
form="tls-settings-form"
|
||||
>
|
||||
Save HTTPS & TLS Settings
|
||||
Save Settings
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
@ -896,7 +896,7 @@ export default function ReverseProxyTargets(props: {
|
|||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The Host header to set when
|
||||
The host header to set when
|
||||
proxying requests. Leave
|
||||
empty to use the default.
|
||||
</FormDescription>
|
||||
|
|
|
@ -40,6 +40,21 @@ export function SitePriceCalculator({
|
|||
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 =
|
||||
mode === "license"
|
||||
? licenseFlatRate + siteCount * pricePerSite
|
||||
|
@ -141,7 +156,9 @@ export function SitePriceCalculator({
|
|||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</CredenzaClose>
|
||||
<Button>Continue to Payment</Button>
|
||||
<Button onClick={continueToPayment}>
|
||||
Continue to Payment
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
|
|
@ -121,7 +121,7 @@ export default function LicensePage() {
|
|||
);
|
||||
const keys = response.data.data;
|
||||
setRows(keys);
|
||||
const hostKey = keys.find((key) => key.type === "LICENSE");
|
||||
const hostKey = keys.find((key) => key.type === "HOST");
|
||||
if (hostKey) {
|
||||
setHostLicense(hostKey.licenseKey);
|
||||
} else {
|
||||
|
@ -285,17 +285,22 @@ export default function LicensePage() {
|
|||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
I have read and agree to the
|
||||
Fossorial Commercial License
|
||||
- Professional Edition
|
||||
Subscription Terms.{" "}
|
||||
By checking this box, you
|
||||
confirm that you have read
|
||||
and agree to the license
|
||||
terms corresponding to the
|
||||
tier associated with your
|
||||
license key.
|
||||
<br />
|
||||
<Link
|
||||
href="https://docs.fossorial.io/license.html"
|
||||
href="https://fossorial.io/license.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
View License & Terms
|
||||
View Fossorial
|
||||
Commercial License &
|
||||
Subscription Terms
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
|
@ -380,7 +385,13 @@ export default function LicensePage() {
|
|||
<div className="space-y-2 text-green-500">
|
||||
<div className="text-2xl flex items-center gap-2">
|
||||
<Check />
|
||||
Licensed
|
||||
{licenseStatus?.tier ===
|
||||
"PROFESSIONAL"
|
||||
? "Professional License"
|
||||
: licenseStatus?.tier ===
|
||||
"ENTERPRISE"
|
||||
? "Enterprise License"
|
||||
: "Licensed"}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
@ -173,7 +173,7 @@ export default function UsersTable({ users }: Props) {
|
|||
<div className="space-y-4">
|
||||
<p>
|
||||
Are you sure you want to permanently delete{" "}
|
||||
<b>
|
||||
<b className="break-all">
|
||||
{selected?.email ||
|
||||
selected?.name ||
|
||||
selected?.username}
|
||||
|
|
|
@ -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 (
|
||||
<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>
|
||||
Invalid or expired license keys detected. Follow license
|
||||
terms to continue using all features.
|
||||
</p>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className="hover:bg-yellow-500"
|
||||
onClick={() => setIsDismissed(true)}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -32,12 +44,21 @@ export default function LicenseViolation() {
|
|||
) {
|
||||
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="flex justify-between items-center">
|
||||
<p>
|
||||
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.
|
||||
{licenseStatus.usedSites} sites which exceeds its
|
||||
licensed limit of {licenseStatus.maxSites} sites. Follow
|
||||
license terms to continue using all features.
|
||||
</p>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className="hover:bg-yellow-500"
|
||||
onClick={() => setIsDismissed(true)}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ export default function InviteUserForm({
|
|||
<CredenzaTitle>{title}</CredenzaTitle>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className="mb-4">{dialog}</div>
|
||||
<div className="mb-4 break-all overflow-hidden">{dialog}</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
|
|
@ -27,7 +27,6 @@ function getActionsCategories(root: boolean) {
|
|||
"Get Organization User": "getOrgUser",
|
||||
"List Organization Domains": "listOrgDomains",
|
||||
"Check Org ID": "checkOrgId",
|
||||
"List Orgs": "listOrgs"
|
||||
},
|
||||
|
||||
Site: {
|
||||
|
@ -91,14 +90,12 @@ function getActionsCategories(root: boolean) {
|
|||
"List Resource Rules": "listResourceRules",
|
||||
"Update Resource Rule": "updateResourceRule"
|
||||
}
|
||||
|
||||
// "Newt": {
|
||||
// "Create Newt": "createNewt"
|
||||
// },
|
||||
};
|
||||
|
||||
if (root) {
|
||||
actionsByCategory["Organization"] = {
|
||||
"List Organizations": "listOrgs",
|
||||
"Check ID": "checkOrgId",
|
||||
"Create Organization": "createOrg",
|
||||
"Delete Organization": "deleteOrg",
|
||||
"List API Keys": "listApiKeys",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue