add license system and ui

This commit is contained in:
miloschwartz
2025-04-27 13:03:00 -04:00
parent 80d76befc9
commit 4819f410e6
46 changed files with 2159 additions and 94 deletions

View File

@@ -11,6 +11,7 @@ import * as role from "./role";
import * as supporterKey from "./supporterKey";
import * as accessToken from "./accessToken";
import * as idp from "./idp";
import * as license from "./license";
import HttpCode from "@server/types/HttpCode";
import {
verifyAccessTokenAccess,
@@ -524,6 +525,30 @@ authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
authenticated.post(
"/license/activate",
verifyUserIsServerAdmin,
license.activateLicense
);
authenticated.get(
"/license/keys",
verifyUserIsServerAdmin,
license.listLicenseKeys
);
authenticated.delete(
"/license/:licenseKey",
verifyUserIsServerAdmin,
license.deleteLicenseKey
);
authenticated.post(
"/license/recheck",
verifyUserIsServerAdmin,
license.recheckStatus
);
// Auth routes
export const authRouter = Router();
unauthenticated.use("/auth", authRouter);

View File

@@ -11,6 +11,7 @@ import { idp, idpOidcConfig } from "@server/db/schemas";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import license from "@server/license/license";
const paramsSchema = z.object({}).strict();
@@ -67,7 +68,7 @@ export async function createOidcIdp(
);
}
const {
let {
clientId,
clientSecret,
authUrl,
@@ -80,6 +81,10 @@ export async function createOidcIdp(
autoProvision
} = parsedBody.data;
if (!(await license.isUnlocked())) {
autoProvision = false;
}
const key = config.getRawConfig().server.secret;
const encryptedSecret = encrypt(clientSecret, key);

View File

@@ -11,6 +11,7 @@ import { idp, idpOidcConfig } from "@server/db/schemas";
import { eq } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import license from "@server/license/license";
const paramsSchema = z
.object({
@@ -84,7 +85,7 @@ export async function updateOidcIdp(
}
const { idpId } = parsedParams.data;
const {
let {
clientId,
clientSecret,
authUrl,
@@ -99,6 +100,10 @@ export async function updateOidcIdp(
defaultOrgMapping
} = parsedBody.data;
if (!(await license.isUnlocked())) {
autoProvision = false;
}
// Check if IDP exists and is of type OIDC
const [existingIdp] = await db
.select()

View File

@@ -5,6 +5,7 @@ import * as resource from "./resource";
import * as badger from "./badger";
import * as auth from "@server/routers/auth";
import * as supporterKey from "@server/routers/supporterKey";
import * as license from "@server/routers/license";
import HttpCode from "@server/types/HttpCode";
import {
verifyResourceAccess,
@@ -37,6 +38,11 @@ internalRouter.get(
supporterKey.isSupporterKeyVisible
);
internalRouter.get(
`/license/status`,
license.getLicenseStatus
);
// Gerbil routes
const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter);

View File

@@ -0,0 +1,62 @@
// This file is licensed under the Fossorial Commercial License.
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
//
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
import { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib";
import license, { LicenseStatus } from "@server/license/license";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const bodySchema = z
.object({
licenseKey: z.string().min(1).max(255)
})
.strict();
export type ActivateLicenseStatus = LicenseStatus;
export async function activateLicense(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { licenseKey } = parsedBody.data;
try {
const status = await license.activateLicenseKey(licenseKey);
return sendResponse(res, {
data: status,
success: true,
error: false,
message: "License key activated successfully",
status: HttpCode.OK
});
} catch (e) {
logger.error(e);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `${e}`)
);
}
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,76 @@
// This file is licensed under the Fossorial Commercial License.
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
//
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
import { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import db from "@server/db";
import { eq } from "drizzle-orm";
import { licenseKey } from "@server/db/schemas";
import license, { LicenseStatus } from "@server/license/license";
const paramsSchema = z
.object({
licenseKey: z.string().min(1).max(255)
})
.strict();
export type DeleteLicenseKeyResponse = LicenseStatus;
export async function deleteLicenseKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { licenseKey: key } = parsedParams.data;
const [existing] = await db
.select()
.from(licenseKey)
.where(eq(licenseKey.licenseKeyId, key))
.limit(1);
if (!existing) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`License key ${key} not found`
)
);
}
await db.delete(licenseKey).where(eq(licenseKey.licenseKeyId, key));
const status = await license.forceRecheck();
return sendResponse(res, {
data: status,
success: true,
error: false,
message: "License key deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,36 @@
// This file is licensed under the Fossorial Commercial License.
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
//
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
import { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib";
import license, { LicenseStatus } from "@server/license/license";
export type GetLicenseStatusResponse = LicenseStatus;
export async function getLicenseStatus(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const status = await license.check();
return sendResponse<GetLicenseStatusResponse>(res, {
data: status,
success: true,
error: false,
message: "Got status",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,10 @@
// This file is licensed under the Fossorial Commercial License.
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
//
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
export * from "./getLicenseStatus";
export * from "./activateLicense";
export * from "./listLicenseKeys";
export * from "./deleteLicenseKey";
export * from "./recheckStatus";

View File

@@ -0,0 +1,36 @@
// This file is licensed under the Fossorial Commercial License.
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
//
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
import { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib";
import license, { LicenseKeyCache } from "@server/license/license";
export type ListLicenseKeysResponse = LicenseKeyCache[];
export async function listLicenseKeys(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const keys = license.listKeys();
return sendResponse<ListLicenseKeysResponse>(res, {
data: keys,
success: true,
error: false,
message: "Successfully retrieved license keys",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,42 @@
// This file is licensed under the Fossorial Commercial License.
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
//
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
import { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib";
import license, { LicenseStatus } from "@server/license/license";
export type RecheckStatusResponse = LicenseStatus;
export async function recheckStatus(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
try {
const status = await license.forceRecheck();
return sendResponse(res, {
data: status,
success: true,
error: false,
message: "License status rechecked successfully",
status: HttpCode.OK
});
} catch (e) {
logger.error(e);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `${e}`)
);
}
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -7,6 +7,7 @@ import config from "@server/lib/config";
import db from "@server/db";
import { count } from "drizzle-orm";
import { users } from "@server/db/schemas";
import license from "@server/license/license";
export type IsSupporterKeyVisibleResponse = {
visible: boolean;
@@ -26,6 +27,12 @@ export async function isSupporterKeyVisible(
let visible = !hidden && key?.valid !== true;
const licenseStatus = await license.check();
if (licenseStatus.isLicenseValid) {
visible = false;
}
if (key?.tier === "Limited Supporter") {
const [numUsers] = await db.select({ count: count() }).from(users);