mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-21 12:09:04 +02:00
License machines
This commit is contained in:
@@ -604,6 +604,10 @@
|
||||
"licenseNoUserLimit": "There is no limit on the number of users for this host.",
|
||||
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
|
||||
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
|
||||
"licenseUsageMachines": "Machine Clients",
|
||||
"licenseNoMachineLimit": "There is no limit on the number of machine clients for this host.",
|
||||
"licenseMachinesUsedMax": "{usedMachines} of {maxMachines} machine clients used",
|
||||
"licenseMachinesUsed": "{count, plural, =0 {# machine clients} one {# machine client} other {# machine clients}}",
|
||||
"licenseUnlimited": "Unlimited",
|
||||
"licenseTierLabel": "Tier",
|
||||
"licensePurchaseDescription": "Choose how many sites you want to {selectedMode, select, license {purchase a license for. You can always add more sites later.} other {add to your existing license.}}",
|
||||
|
||||
@@ -20,6 +20,7 @@ type EnterpriseEditionKeyGeneratedProps = {
|
||||
personalUseOnly: boolean;
|
||||
users: number;
|
||||
sites: number;
|
||||
machines: number;
|
||||
modifySubscriptionLink?: string;
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ export const EnterpriseEditionKeyGenerated = ({
|
||||
personalUseOnly,
|
||||
users,
|
||||
sites,
|
||||
machines,
|
||||
modifySubscriptionLink
|
||||
}: EnterpriseEditionKeyGeneratedProps) => {
|
||||
const previewText = personalUseOnly
|
||||
@@ -69,6 +71,10 @@ export const EnterpriseEditionKeyGenerated = ({
|
||||
{
|
||||
label: "Licensed sites",
|
||||
value: sites
|
||||
},
|
||||
{
|
||||
label: "Licensed machine clients",
|
||||
value: machines
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,8 @@ export type LicenseStatus = {
|
||||
usedSites?: number;
|
||||
maxUsers?: number;
|
||||
usedUsers?: number;
|
||||
maxMachines?: number;
|
||||
usedMachines?: number;
|
||||
};
|
||||
|
||||
export type LicenseKeyCache = {
|
||||
@@ -28,6 +30,7 @@ export type LicenseKeyCache = {
|
||||
terminateAt?: Date;
|
||||
quantity?: number;
|
||||
quantity_2?: number;
|
||||
quantity_3?: number;
|
||||
};
|
||||
|
||||
export class License {
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { db, HostMeta, primaryDb, sites, users } from "@server/db";
|
||||
import { clients, db, HostMeta, primaryDb, sites, users } from "@server/db";
|
||||
import { hostMeta, licenseKey } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { createLocalCache } from "@server/lib/createLocalCache";
|
||||
import { validateJWT } from "./licenseJwt";
|
||||
import { count, eq } from "drizzle-orm";
|
||||
import { count, eq, isNull } from "drizzle-orm";
|
||||
import moment from "moment";
|
||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||
import {
|
||||
@@ -77,6 +77,7 @@ type TokenPayload = {
|
||||
tier: LicenseKeyTier;
|
||||
quantity: number;
|
||||
quantity_2: number;
|
||||
quantity_3: number;
|
||||
terminateAt: string; // ISO
|
||||
iat: number; // Issued at
|
||||
};
|
||||
@@ -190,9 +191,13 @@ LQIDAQAB
|
||||
};
|
||||
}
|
||||
|
||||
// Count used sites and users for license comparison
|
||||
// Count used sites, users, and machine clients for license comparison
|
||||
const [siteCountRes] = await db.select({ value: count() }).from(sites);
|
||||
const [userCountRes] = await db.select({ value: count() }).from(users);
|
||||
const [machineCountRes] = await db
|
||||
.select({ value: count() })
|
||||
.from(clients)
|
||||
.where(isNull(clients.userId));
|
||||
const keyIdsRes = await db
|
||||
.select({ licenseKeyId: licenseKey.licenseKeyId })
|
||||
.from(licenseKey);
|
||||
@@ -202,7 +207,8 @@ LQIDAQAB
|
||||
isHostLicensed: true,
|
||||
isLicenseValid: false,
|
||||
usedSites: siteCountRes?.value ?? 0,
|
||||
usedUsers: userCountRes?.value ?? 0
|
||||
usedUsers: userCountRes?.value ?? 0,
|
||||
usedMachines: machineCountRes?.value ?? 0
|
||||
};
|
||||
|
||||
this.checkInProgress = true;
|
||||
@@ -216,6 +222,7 @@ LQIDAQAB
|
||||
const res = this.statusCache.get("status") as LicenseStatus;
|
||||
res.usedSites = status.usedSites;
|
||||
res.usedUsers = status.usedUsers;
|
||||
res.usedMachines = status.usedMachines;
|
||||
return res;
|
||||
}
|
||||
logger.debug("Checking license status...");
|
||||
@@ -273,7 +280,8 @@ LQIDAQAB
|
||||
iat: new Date(payload.iat * 1000),
|
||||
terminateAt: new Date(payload.terminateAt),
|
||||
quantity: payload.quantity,
|
||||
quantity_2: payload.quantity_2
|
||||
quantity_2: payload.quantity_2,
|
||||
quantity_3: payload.quantity_3
|
||||
});
|
||||
|
||||
if (payload.type === "host") {
|
||||
@@ -391,6 +399,7 @@ LQIDAQAB
|
||||
cached.terminateAt = new Date(payload.terminateAt);
|
||||
cached.quantity = payload.quantity;
|
||||
cached.quantity_2 = payload.quantity_2;
|
||||
cached.quantity_3 = payload.quantity_3;
|
||||
|
||||
// Encrypt the updated token before storing
|
||||
const encryptedKey = encrypt(
|
||||
@@ -445,7 +454,8 @@ LQIDAQAB
|
||||
}
|
||||
}
|
||||
|
||||
// Compute host status: quantity = users, quantity_2 = sites
|
||||
// Compute host status: quantity = users, quantity_2 = sites,
|
||||
// quantity_3 = machine clients
|
||||
// When multiple host keys are active, prefer a valid key over an
|
||||
// invalid one, and among equally-valid keys prefer the highest tier.
|
||||
let selectedHostKey: LicenseKeyCache | undefined;
|
||||
@@ -468,7 +478,10 @@ LQIDAQAB
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only consider quantity if defined and >= 0 (quantity = users, quantity_2 = sites)
|
||||
// Only consider quantity if defined and >= 0 (quantity = users,
|
||||
// quantity_2 = sites, quantity_3 = machine clients). Keys issued
|
||||
// before machine limits existed omit quantity_3 and so impose no
|
||||
// machine limit.
|
||||
if (cached.quantity_2 !== undefined && cached.quantity_2 >= 0) {
|
||||
status.maxSites =
|
||||
(status.maxSites ?? 0) + cached.quantity_2;
|
||||
@@ -476,6 +489,10 @@ LQIDAQAB
|
||||
if (cached.quantity !== undefined && cached.quantity >= 0) {
|
||||
status.maxUsers = (status.maxUsers ?? 0) + cached.quantity;
|
||||
}
|
||||
if (cached.quantity_3 !== undefined && cached.quantity_3 >= 0) {
|
||||
status.maxMachines =
|
||||
(status.maxMachines ?? 0) + cached.quantity_3;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedHostKey) {
|
||||
@@ -483,12 +500,14 @@ LQIDAQAB
|
||||
status.tier = selectedHostKey.tier;
|
||||
}
|
||||
|
||||
// Invalidate license if over user or site limits
|
||||
// Invalidate license if over user, site, or machine client limits
|
||||
if (
|
||||
(status.maxSites !== undefined &&
|
||||
(status.usedSites ?? 0) > status.maxSites) ||
|
||||
(status.maxUsers !== undefined &&
|
||||
(status.usedUsers ?? 0) > status.maxUsers)
|
||||
(status.usedUsers ?? 0) > status.maxUsers) ||
|
||||
(status.maxMachines !== undefined &&
|
||||
(status.usedMachines ?? 0) > status.maxMachines)
|
||||
) {
|
||||
status.isLicenseValid = false;
|
||||
}
|
||||
|
||||
@@ -222,15 +222,18 @@ export async function handleSubscriptionCreated(
|
||||
|
||||
let numUsers: number;
|
||||
let numSites: number;
|
||||
let numMachines: number;
|
||||
let tier = "enterprise";
|
||||
|
||||
if (subscriptionPriceId === priceSet[LicenseId.TIER1]) {
|
||||
numUsers = 25;
|
||||
numSites = 25;
|
||||
numMachines = 25;
|
||||
tier = "tier1";
|
||||
} else if (subscriptionPriceId === priceSet[LicenseId.TIER2]) {
|
||||
numUsers = 50;
|
||||
numSites = 100;
|
||||
numMachines = 50;
|
||||
tier = "tier2";
|
||||
} else {
|
||||
logger.error(
|
||||
@@ -240,7 +243,7 @@ export async function handleSubscriptionCreated(
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`License type determined: ${numUsers} users, ${numSites} sites for subscription ${subscription.id}`
|
||||
`License type determined: ${numUsers} users, ${numSites} sites, ${numMachines} machines for subscription ${subscription.id}`
|
||||
);
|
||||
|
||||
// Grace period of 5 days added on top of the current billing
|
||||
@@ -266,6 +269,7 @@ export async function handleSubscriptionCreated(
|
||||
paidFor: true,
|
||||
users: numUsers,
|
||||
sites: numSites,
|
||||
machines: numMachines,
|
||||
tier: tier,
|
||||
expiresAt: expiresAt
|
||||
})
|
||||
@@ -293,6 +297,7 @@ export async function handleSubscriptionCreated(
|
||||
personalUseOnly: false,
|
||||
users: numUsers,
|
||||
sites: numSites,
|
||||
machines: numMachines,
|
||||
modifySubscriptionLink: `${config.getRawConfig().app.dashboard_url}/${customer.orgId}/settings/billing`
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface LicenseKey {
|
||||
type: string;
|
||||
quantity: number;
|
||||
quantity_2: number;
|
||||
quantity_3: number;
|
||||
isValid: boolean;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type GeneratedLicenseKey = {
|
||||
type: string;
|
||||
users: number;
|
||||
sites: number;
|
||||
machines: number;
|
||||
};
|
||||
|
||||
export type ListGeneratedLicenseKeysResponse = GeneratedLicenseKey[];
|
||||
@@ -22,6 +23,7 @@ export type NewLicenseKey = {
|
||||
type: string;
|
||||
quantity: number;
|
||||
quantity_2: number;
|
||||
quantity_3: number;
|
||||
isValid: boolean;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
|
||||
@@ -536,6 +536,53 @@ export default function LicensePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
{t("licenseUsageMachines")}
|
||||
</div>
|
||||
<div className="text-2xl">
|
||||
{t("licenseMachinesUsed", {
|
||||
count: licenseStatus?.usedMachines || 0
|
||||
})}
|
||||
</div>
|
||||
{licenseStatus?.maxMachines ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t("licenseMachinesUsedMax", {
|
||||
usedMachines:
|
||||
licenseStatus.usedMachines ||
|
||||
0,
|
||||
maxMachines:
|
||||
licenseStatus.maxMachines
|
||||
})}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(
|
||||
((licenseStatus.usedMachines ||
|
||||
0) /
|
||||
licenseStatus.maxMachines) *
|
||||
100
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
((licenseStatus.usedMachines ||
|
||||
0) /
|
||||
licenseStatus.maxMachines) *
|
||||
100
|
||||
}
|
||||
className="h-5"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("licenseNoMachineLimit")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<SettingsSection>
|
||||
|
||||
@@ -255,6 +255,27 @@ export default function GenerateLicenseKeysTable({
|
||||
return sites === -1 ? "∞" : sites;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "machines",
|
||||
friendlyName: t("machineClients"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("machineClients")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const machines = row.original.machines;
|
||||
return machines === -1 ? "∞" : machines;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "terminateAt",
|
||||
friendlyName: t("licenseTableValidUntil"),
|
||||
|
||||
@@ -160,6 +160,30 @@ export function LicenseKeysDataTable({
|
||||
return quantity < 0 ? t("licenseUnlimited") : quantity;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity_3",
|
||||
friendlyName: t("machineClients"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("machineClients")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const quantity = row.original.quantity_3;
|
||||
if (quantity === undefined) {
|
||||
return "-";
|
||||
}
|
||||
return quantity < 0 ? t("licenseUnlimited") : quantity;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "terminateAt",
|
||||
friendlyName: t("licenseTableValidUntil"),
|
||||
|
||||
Reference in New Issue
Block a user