diff --git a/LICENSE b/LICENSE index 0ad25db..8c5cfb8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,35 @@ +Copyright (c) 2025 Fossorial, LLC. + +Portions of this software are licensed as follows: + +* All files that include a header specifying they are licensed under the + "Fossorial Commercial License" are governed by the Fossorial Commercial + License terms. The specific terms applicable to each customer depend on the + commercial license tier agreed upon in writing with Fossorial LLC. + Unauthorized use, copying, modification, or distribution is strictly + prohibited. + +* All files that include a header specifying they are licensed under the GNU + Affero General Public License, Version 3 ("AGPL-3"), are governed by the + AGPL-3 terms. A full copy of the AGPL-3 license is provided below. However, + these files are also available under the Fossorial Commercial License if a + separate commercial license agreement has been executed between the customer + and Fossorial LLC. + +* All files without a license header are, by default, licensed under the GNU + Affero General Public License, Version 3 (AGPL-3). These files may also be + made available under the Fossorial Commercial License upon agreement with + Fossorial LLC. + +* All third-party components included in this repository are licensed under + their respective original licenses, as provided by their authors. + +Please consult the header of each individual file to determine the applicable +license. For AGPL-3 licensed files, dual-licensing under the Fossorial +Commercial License is available subject to written agreement with Fossorial +LLC. + + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/install/main.go b/install/main.go index a0d74a4..abb67ac 100644 --- a/install/main.go +++ b/install/main.go @@ -64,15 +64,15 @@ func main() { } var config Config - + config.DoCrowdsecInstall = false + config.Secret = generateRandomSecretKey() + // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { config = collectUserInput(reader) - + loadVersions(&config) - config.DoCrowdsecInstall = false - config.Secret = generateRandomSecretKey() - + if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) diff --git a/newt b/newt deleted file mode 100755 index 3805c73..0000000 Binary files a/newt and /dev/null differ diff --git a/server/index.ts b/server/index.ts index 3350260..4c16caa 100644 --- a/server/index.ts +++ b/server/index.ts @@ -6,7 +6,7 @@ import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; import { createIntegrationApiServer } from "./integrationApiServer"; -import config from "@server/lib/config"; +import license from "./license/license.js"; async function startServers() { await runSetupFunctions(); @@ -17,7 +17,7 @@ async function startServers() { const nextServer = await createNextServer(); let integrationServer; - if (config.getRawConfig().flags?.enable_integration_api) { + if (await license.isUnlocked()) { integrationServer = createIntegrationApiServer(); } diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts index f3dfbbe..ff5dca5 100644 --- a/server/integrationApiServer.ts +++ b/server/integrationApiServer.ts @@ -1,3 +1,8 @@ +// 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 express from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; @@ -6,6 +11,7 @@ import logger from "@server/logger"; import { errorHandlerMiddleware, notFoundMiddleware, + verifyValidLicense } from "@server/middlewares"; import { authenticated, unauthenticated } from "@server/routers/integration"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; @@ -20,6 +26,8 @@ const externalPort = config.getRawConfig().server.integration_port; export function createIntegrationApiServer() { const apiServer = express(); + apiServer.use(verifyValidLicense); + if (config.getRawConfig().server.trust_proxy) { apiServer.set("trust proxy", 1); } diff --git a/server/lib/config.ts b/server/lib/config.ts index fe51af3..a19b4a2 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -29,12 +29,9 @@ const configSchema = z.object({ .optional() .pipe(z.string().url()) .transform((url) => url.toLowerCase()), - log_level: z - .enum(["debug", "info", "warn", "error"]) - .optional() - .default("info"), - save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false) + log_level: z.enum(["debug", "info", "warn", "error"]), + save_logs: z.boolean(), + log_failed_attempts: z.boolean().optional() }), domains: z .record( @@ -44,8 +41,8 @@ const configSchema = z.object({ .string() .nonempty("base_domain must not be empty") .transform((url) => url.toLowerCase()), - cert_resolver: z.string().optional().default("letsencrypt"), - prefer_wildcard_cert: z.boolean().optional().default(false) + cert_resolver: z.string().optional(), + prefer_wildcard_cert: z.boolean().optional() }) ) .refine( @@ -65,42 +62,19 @@ const configSchema = z.object({ server: z.object({ integration_port: portSchema .optional() - .default(3003) .transform(stoi) .pipe(portSchema.optional()), - external_port: portSchema - .optional() - .default(3000) - .transform(stoi) - .pipe(portSchema), - internal_port: portSchema - .optional() - .default(3001) - .transform(stoi) - .pipe(portSchema), - next_port: portSchema - .optional() - .default(3002) - .transform(stoi) - .pipe(portSchema), - internal_hostname: z - .string() - .optional() - .default("pangolin") - .transform((url) => url.toLowerCase()), - session_cookie_name: z.string().optional().default("p_session_token"), - resource_access_token_param: z.string().optional().default("p_token"), - resource_access_token_headers: z - .object({ - id: z.string().optional().default("P-Access-Token-Id"), - token: z.string().optional().default("P-Access-Token") - }) - .optional() - .default({}), - resource_session_request_param: z - .string() - .optional() - .default("resource_session_request_param"), + external_port: portSchema.optional().transform(stoi).pipe(portSchema), + internal_port: portSchema.optional().transform(stoi).pipe(portSchema), + next_port: portSchema.optional().transform(stoi).pipe(portSchema), + internal_hostname: z.string().transform((url) => url.toLowerCase()), + session_cookie_name: z.string(), + resource_access_token_param: z.string(), + resource_access_token_headers: z.object({ + id: z.string(), + token: z.string() + }), + resource_session_request_param: z.string(), dashboard_session_length_hours: z .number() .positive() @@ -128,61 +102,35 @@ const configSchema = z.object({ .transform(getEnvOrYaml("SERVER_SECRET")) .pipe(z.string().min(8)) }), - traefik: z - .object({ - http_entrypoint: z.string().optional().default("web"), - https_entrypoint: z.string().optional().default("websecure"), - additional_middlewares: z.array(z.string()).optional() - }) - .optional() - .default({}), - gerbil: z - .object({ - start_port: portSchema - .optional() - .default(51820) - .transform(stoi) - .pipe(portSchema), - base_endpoint: z - .string() - .optional() - .pipe(z.string()) - .transform((url) => url.toLowerCase()), - use_subdomain: z.boolean().optional().default(false), - subnet_group: z.string().optional().default("100.89.137.0/20"), - block_size: z.number().positive().gt(0).optional().default(24), - site_block_size: z.number().positive().gt(0).optional().default(30) - }) - .optional() - .default({}), - rate_limits: z - .object({ - global: z - .object({ - window_minutes: z - .number() - .positive() - .gt(0) - .optional() - .default(1), - max_requests: z - .number() - .positive() - .gt(0) - .optional() - .default(500) - }) - .optional() - .default({}), - auth: z - .object({ - window_minutes: z.number().positive().gt(0), - max_requests: z.number().positive().gt(0) - }) - .optional() - }) - .optional() - .default({}), + traefik: z.object({ + http_entrypoint: z.string(), + https_entrypoint: z.string().optional(), + additional_middlewares: z.array(z.string()).optional() + }), + gerbil: z.object({ + start_port: portSchema.optional().transform(stoi).pipe(portSchema), + base_endpoint: z + .string() + .optional() + .pipe(z.string()) + .transform((url) => url.toLowerCase()), + use_subdomain: z.boolean(), + subnet_group: z.string(), + block_size: z.number().positive().gt(0), + site_block_size: z.number().positive().gt(0) + }), + rate_limits: z.object({ + global: z.object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }), + auth: z + .object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }) + .optional() + }), email: z .object({ smtp_host: z.string().optional(), @@ -216,8 +164,7 @@ const configSchema = z.object({ disable_user_create_org: z.boolean().optional(), allow_raw_resources: z.boolean().optional(), allow_base_domain_resources: z.boolean().optional(), - allow_local_sites: z.boolean().optional(), - enable_integration_api: z.boolean().optional() + allow_local_sites: z.boolean().optional() }) .optional() }); diff --git a/server/lib/consts.ts b/server/lib/consts.ts index ed61e8c..94d2716 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.4.0"; +export const APP_VERSION = "1.3.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/validators.ts b/server/lib/validators.ts index 50ff567..e33c918 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -9,10 +9,6 @@ export function isValidIP(ip: string): boolean { } export function isValidUrlGlobPattern(pattern: string): boolean { - if (pattern === "/") { - return true; - } - // Remove leading slash if present pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; diff --git a/server/license/license.ts b/server/license/license.ts index bd596e4..e97b8f5 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -1,3 +1,8 @@ +// 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 db from "@server/db"; import { hostMeta, licenseKey, sites } from "@server/db/schemas"; import logger from "@server/logger"; diff --git a/server/license/licenseJwt.ts b/server/license/licenseJwt.ts index 3d148e5..ed7f4a0 100644 --- a/server/license/licenseJwt.ts +++ b/server/license/licenseJwt.ts @@ -1,3 +1,8 @@ +// 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 * as crypto from "crypto"; /** diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 19bf128..c16e129 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -1,3 +1,8 @@ +// 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 "./verifyApiKey"; export * from "./verifyApiKeyOrgAccess"; export * from "./verifyApiKeyHasAction"; diff --git a/server/middlewares/integration/verifyAccessTokenAccess.ts b/server/middlewares/integration/verifyAccessTokenAccess.ts index e9069ba..82badcd 100644 --- a/server/middlewares/integration/verifyAccessTokenAccess.ts +++ b/server/middlewares/integration/verifyAccessTokenAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKey.ts b/server/middlewares/integration/verifyApiKey.ts index 0b0602e..39fc3de 100644 --- a/server/middlewares/integration/verifyApiKey.ts +++ b/server/middlewares/integration/verifyApiKey.ts @@ -1,3 +1,8 @@ +// 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 { verifyPassword } from "@server/auth/password"; import db from "@server/db"; import { apiKeys } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts index 435f01d..aedc60c 100644 --- a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts +++ b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { apiKeys, apiKeyOrg } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKeyHasAction.ts b/server/middlewares/integration/verifyApiKeyHasAction.ts index 35f4398..0326c46 100644 --- a/server/middlewares/integration/verifyApiKeyHasAction.ts +++ b/server/middlewares/integration/verifyApiKeyHasAction.ts @@ -1,3 +1,8 @@ +// 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 createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/integration/verifyApiKeyIsRoot.ts b/server/middlewares/integration/verifyApiKeyIsRoot.ts index 2ce9c84..35cd0fa 100644 --- a/server/middlewares/integration/verifyApiKeyIsRoot.ts +++ b/server/middlewares/integration/verifyApiKeyIsRoot.ts @@ -1,3 +1,8 @@ +// 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 logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import { Request, Response, NextFunction } from "express"; diff --git a/server/middlewares/integration/verifyApiKeyOrgAccess.ts b/server/middlewares/integration/verifyApiKeyOrgAccess.ts index 902ccf5..e1e1e0d 100644 --- a/server/middlewares/integration/verifyApiKeyOrgAccess.ts +++ b/server/middlewares/integration/verifyApiKeyOrgAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { apiKeyOrg } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKeyResourceAccess.ts b/server/middlewares/integration/verifyApiKeyResourceAccess.ts index f4e3ed0..49180b5 100644 --- a/server/middlewares/integration/verifyApiKeyResourceAccess.ts +++ b/server/middlewares/integration/verifyApiKeyResourceAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { resources, apiKeyOrg } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKeyRoleAccess.ts b/server/middlewares/integration/verifyApiKeyRoleAccess.ts index 4d76941..a7abf9a 100644 --- a/server/middlewares/integration/verifyApiKeyRoleAccess.ts +++ b/server/middlewares/integration/verifyApiKeyRoleAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { roles, apiKeyOrg } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts index 1c3b5b1..d43021b 100644 --- a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts +++ b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { userOrgs } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKeySiteAccess.ts b/server/middlewares/integration/verifyApiKeySiteAccess.ts index 2c83ead..7d10dde 100644 --- a/server/middlewares/integration/verifyApiKeySiteAccess.ts +++ b/server/middlewares/integration/verifyApiKeySiteAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { diff --git a/server/middlewares/integration/verifyApiKeyTargetAccess.ts b/server/middlewares/integration/verifyApiKeyTargetAccess.ts index 7da1f29..bd6e5bc 100644 --- a/server/middlewares/integration/verifyApiKeyTargetAccess.ts +++ b/server/middlewares/integration/verifyApiKeyTargetAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { resources, targets, apiKeyOrg } from "@server/db/schemas"; diff --git a/server/middlewares/integration/verifyApiKeyUserAccess.ts b/server/middlewares/integration/verifyApiKeyUserAccess.ts index 69f27e9..e1b5d3d 100644 --- a/server/middlewares/integration/verifyApiKeyUserAccess.ts +++ b/server/middlewares/integration/verifyApiKeyUserAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { userOrgs } from "@server/db/schemas"; diff --git a/server/middlewares/verifyApiKeyAccess.ts b/server/middlewares/verifyApiKeyAccess.ts index ad21b37..0bba8f4 100644 --- a/server/middlewares/verifyApiKeyAccess.ts +++ b/server/middlewares/verifyApiKeyAccess.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas"; diff --git a/server/middlewares/verifyValidLicense.ts b/server/middlewares/verifyValidLicense.ts index 7e3bfee..7f4de34 100644 --- a/server/middlewares/verifyValidLicense.ts +++ b/server/middlewares/verifyValidLicense.ts @@ -1,3 +1,8 @@ +// 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 createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/apiKeys/createOrgApiKey.ts b/server/routers/apiKeys/createOrgApiKey.ts index bf8ff8c..2fb9fd2 100644 --- a/server/routers/apiKeys/createOrgApiKey.ts +++ b/server/routers/apiKeys/createOrgApiKey.ts @@ -1,3 +1,8 @@ +// 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 { NextFunction, Request, Response } from "express"; import db from "@server/db"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts index 7a5d2d8..775ae57 100644 --- a/server/routers/apiKeys/createRootApiKey.ts +++ b/server/routers/apiKeys/createRootApiKey.ts @@ -1,3 +1,8 @@ +// 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 { NextFunction, Request, Response } from "express"; import db from "@server/db"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/apiKeys/deleteApiKey.ts b/server/routers/apiKeys/deleteApiKey.ts index e1a74a4..2af4ae2 100644 --- a/server/routers/apiKeys/deleteApiKey.ts +++ b/server/routers/apiKeys/deleteApiKey.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/apiKeys/deleteOrgApiKey.ts b/server/routers/apiKeys/deleteOrgApiKey.ts index dbaf47f..1834c82 100644 --- a/server/routers/apiKeys/deleteOrgApiKey.ts +++ b/server/routers/apiKeys/deleteOrgApiKey.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/apiKeys/getApiKey.ts b/server/routers/apiKeys/getApiKey.ts index e0354cf..bd495bd 100644 --- a/server/routers/apiKeys/getApiKey.ts +++ b/server/routers/apiKeys/getApiKey.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/apiKeys/index.ts b/server/routers/apiKeys/index.ts index 62ede75..84d4ee6 100644 --- a/server/routers/apiKeys/index.ts +++ b/server/routers/apiKeys/index.ts @@ -1,3 +1,8 @@ +// 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 "./createRootApiKey"; export * from "./deleteApiKey"; export * from "./getApiKey"; diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts index 5bd1441..0cf694a 100644 --- a/server/routers/apiKeys/listApiKeyActions.ts +++ b/server/routers/apiKeys/listApiKeyActions.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas"; import logger from "@server/logger"; diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts index 9833ef0..a016907 100644 --- a/server/routers/apiKeys/listOrgApiKeys.ts +++ b/server/routers/apiKeys/listOrgApiKeys.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { apiKeyOrg, apiKeys } from "@server/db/schemas"; import logger from "@server/logger"; diff --git a/server/routers/apiKeys/listRootApiKeys.ts b/server/routers/apiKeys/listRootApiKeys.ts index c639ce5..7feca73 100644 --- a/server/routers/apiKeys/listRootApiKeys.ts +++ b/server/routers/apiKeys/listRootApiKeys.ts @@ -1,3 +1,8 @@ +// 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 { db } from "@server/db"; import { apiKeys } from "@server/db/schemas"; import logger from "@server/logger"; diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts index 602c779..187dd11 100644 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ b/server/routers/apiKeys/setApiKeyActions.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/apiKeys/setApiKeyOrgs.ts b/server/routers/apiKeys/setApiKeyOrgs.ts index c42046d..ee0611d 100644 --- a/server/routers/apiKeys/setApiKeyOrgs.ts +++ b/server/routers/apiKeys/setApiKeyOrgs.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index b0ad987..0a459dc 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -1,136 +1,61 @@ +import { isPathAllowed } from './verifySession'; import { assertEquals } from '@test/assert'; -function isPathAllowed(pattern: string, path: string): boolean { - - // Normalize and split paths into segments - const normalize = (p: string) => p.split("/").filter(Boolean); - const patternParts = normalize(pattern); - const pathParts = normalize(path); - - - // Recursive function to try different wildcard matches - function matchSegments(patternIndex: number, pathIndex: number): boolean { - const indent = " ".repeat(pathIndex); // Indent based on recursion depth - const currentPatternPart = patternParts[patternIndex]; - const currentPathPart = pathParts[pathIndex]; - - // If we've consumed all pattern parts, we should have consumed all path parts - if (patternIndex >= patternParts.length) { - const result = pathIndex >= pathParts.length; - return result; - } - - // If we've consumed all path parts but still have pattern parts - if (pathIndex >= pathParts.length) { - // The only way this can match is if all remaining pattern parts are wildcards - const remainingPattern = patternParts.slice(patternIndex); - const result = remainingPattern.every((p) => p === "*"); - return result; - } - - // For full segment wildcards, try consuming different numbers of path segments - if (currentPatternPart === "*") { - - // Try consuming 0 segments (skip the wildcard) - if (matchSegments(patternIndex + 1, pathIndex)) { - return true; - } - - // Try consuming current segment and recursively try rest - if (matchSegments(patternIndex, pathIndex + 1)) { - return true; - } - - return false; - } - - // Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix") - if (currentPatternPart.includes("*")) { - // Convert the pattern segment to a regex pattern - const regexPattern = currentPatternPart - .replace(/\*/g, ".*") // Replace * with .* for regex wildcard - .replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed - - const regex = new RegExp(`^${regexPattern}$`); - - if (regex.test(currentPathPart)) { - return matchSegments(patternIndex + 1, pathIndex + 1); - } - - return false; - } - - // For regular segments, they must match exactly - if (currentPatternPart !== currentPathPart) { - return false; - } - - // Move to next segments in both pattern and path - return matchSegments(patternIndex + 1, pathIndex + 1); - } - - const result = matchSegments(0, 0); - return result; -} - function runTests() { console.log('Running path matching tests...'); - + // Test exact matching assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed'); assertEquals(isPathAllowed('foo', 'bar'), false, 'Different segments should not match'); assertEquals(isPathAllowed('foo/bar', 'foo/bar'), true, 'Exact multi-segment match should be allowed'); assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed'); - + // Test with leading and trailing slashes assertEquals(isPathAllowed('/foo', 'foo'), true, 'Pattern with leading slash should match'); assertEquals(isPathAllowed('foo/', 'foo'), true, 'Pattern with trailing slash should match'); assertEquals(isPathAllowed('/foo/', 'foo'), true, 'Pattern with both leading and trailing slashes should match'); assertEquals(isPathAllowed('foo', '/foo/'), true, 'Path with leading and trailing slashes should match'); - + // Test simple wildcard matching assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment'); assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments'); assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match'); assertEquals(isPathAllowed('foo/*', 'foo/bar'), true, 'Wildcard suffix should match'); assertEquals(isPathAllowed('foo/*/baz', 'foo/bar/baz'), true, 'Wildcard in middle should match'); - + // Test multiple wildcards assertEquals(isPathAllowed('*/*', 'foo/bar'), true, 'Multiple wildcards should match corresponding segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar/baz'), true, 'Three wildcards should match three segments'); assertEquals(isPathAllowed('foo/*/*', 'foo/bar/baz'), true, 'Specific prefix with wildcards should match'); assertEquals(isPathAllowed('*/*/baz', 'foo/bar/baz'), true, 'Wildcards with specific suffix should match'); - + // Test wildcard consumption behavior assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments'); assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional'); assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped'); - + // Test complex nested paths assertEquals(isPathAllowed('api/*/users', 'api/v1/users'), true, 'API versioning pattern should match'); assertEquals(isPathAllowed('api/*/users/*', 'api/v1/users/123'), true, 'API resource pattern should match'); assertEquals(isPathAllowed('api/*/users/*/profile', 'api/v1/users/123/profile'), true, 'Nested API pattern should match'); - + // Test for the requested padbootstrap* pattern assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrapv1'), true, 'padbootstrap* should match padbootstrapv1'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap/files'), false, 'padbootstrap* should not match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/*', 'padbootstrap/files'), true, 'padbootstrap*/* should match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/files', 'padbootstrapv1/files'), true, 'padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)'); - + // Test wildcard edge cases assertEquals(isPathAllowed('*/*/*/*/*/*', 'a/b'), true, 'Many wildcards can match few segments'); assertEquals(isPathAllowed('a/*/b/*/c', 'a/anything/b/something/c'), true, 'Multiple wildcards in pattern should match corresponding segments'); - + // Test patterns with partial segment matches assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap-123'), true, 'Wildcards in isPathAllowed should be segment-based, not character-based'); assertEquals(isPathAllowed('test*', 'testuser'), true, 'Asterisk as part of segment name is treated as a literal, not a wildcard'); assertEquals(isPathAllowed('my*app', 'myapp'), true, 'Asterisk in middle of segment name is treated as a literal, not a wildcard'); - assertEquals(isPathAllowed('/', '/'), true, 'Root path should match root path'); - assertEquals(isPathAllowed('/', '/test'), false, 'Root path should not match non-root path'); - console.log('All tests passed!'); } @@ -139,4 +64,4 @@ try { runTests(); } catch (error) { console.error('Test failed:', error); -} +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 4197965..d631c37 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -30,6 +30,7 @@ import { verifyUserIsServerAdmin, verifyIsLoggedInUser, verifyApiKeyAccess, + verifyValidLicense } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -530,24 +531,28 @@ authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); authenticated.put( "/idp/:idpId/org/:orgId", + verifyValidLicense, verifyUserIsServerAdmin, idp.createIdpOrgPolicy ); authenticated.post( "/idp/:idpId/org/:orgId", + verifyValidLicense, verifyUserIsServerAdmin, idp.updateIdpOrgPolicy ); authenticated.delete( "/idp/:idpId/org/:orgId", + verifyValidLicense, verifyUserIsServerAdmin, idp.deleteIdpOrgPolicy ); authenticated.get( "/idp/:idpId/org", + verifyValidLicense, verifyUserIsServerAdmin, idp.listIdpOrgPolicies ); @@ -581,42 +586,49 @@ authenticated.post( authenticated.get( `/api-key/:apiKeyId`, + verifyValidLicense, verifyUserIsServerAdmin, apiKeys.getApiKey ); authenticated.put( `/api-key`, + verifyValidLicense, verifyUserIsServerAdmin, apiKeys.createRootApiKey ); authenticated.delete( `/api-key/:apiKeyId`, + verifyValidLicense, verifyUserIsServerAdmin, apiKeys.deleteApiKey ); authenticated.get( `/api-keys`, + verifyValidLicense, verifyUserIsServerAdmin, apiKeys.listRootApiKeys ); authenticated.get( `/api-key/:apiKeyId/actions`, + verifyValidLicense, verifyUserIsServerAdmin, apiKeys.listApiKeyActions ); authenticated.post( `/api-key/:apiKeyId/actions`, + verifyValidLicense, verifyUserIsServerAdmin, apiKeys.setApiKeyActions ); authenticated.get( `/org/:orgId/api-keys`, + verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.listApiKeys), apiKeys.listOrgApiKeys @@ -624,6 +636,7 @@ authenticated.get( authenticated.post( `/org/:orgId/api-key/:apiKeyId/actions`, + verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.setApiKeyActions), @@ -632,6 +645,7 @@ authenticated.post( authenticated.get( `/org/:orgId/api-key/:apiKeyId/actions`, + verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.listApiKeyActions), @@ -640,6 +654,7 @@ authenticated.get( authenticated.put( `/org/:orgId/api-key`, + verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.createApiKey), apiKeys.createOrgApiKey @@ -647,6 +662,7 @@ authenticated.put( authenticated.delete( `/org/:orgId/api-key/:apiKeyId`, + verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.deleteApiKey), @@ -655,6 +671,7 @@ authenticated.delete( authenticated.get( `/org/:orgId/api-key/:apiKeyId`, + verifyValidLicense, verifyOrgAccess, verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.getApiKey), diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts index 808c7ca..ae5acce 100644 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 22c569f..d663afe 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -81,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); diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts index 9a6f6e7..5c41c95 100644 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts index 08ad110..9ff9c97 100644 --- a/server/routers/idp/listIdpOrgPolicies.ts +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/idp/oidcAutoProvision.ts b/server/routers/idp/oidcAutoProvision.ts new file mode 100644 index 0000000..7861fc4 --- /dev/null +++ b/server/routers/idp/oidcAutoProvision.ts @@ -0,0 +1,233 @@ +// 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 { + createSession, + generateId, + generateSessionToken, + serializeSessionCookie +} from "@server/auth/sessions/app"; +import db from "@server/db"; +import { Idp, idpOrg, orgs, roles, User, userOrgs, users } from "@server/db/schemas"; +import logger from "@server/logger"; +import { UserType } from "@server/types/UserTypes"; +import { eq, and, inArray } from "drizzle-orm"; +import jmespath from "jmespath"; +import { Request, Response } from "express"; + +export async function oidcAutoProvision({ + idp, + claims, + existingUser, + userIdentifier, + email, + name, + req, + res +}: { + idp: Idp; + claims: any; + existingUser?: User; + userIdentifier: string; + email?: string; + name?: string; + req: Request; + res: Response; +}) { + const allOrgs = await db.select().from(orgs); + + const defaultRoleMapping = idp.defaultRoleMapping; + const defaultOrgMapping = idp.defaultOrgMapping; + + let userOrgInfo: { orgId: string; roleId: number }[] = []; + for (const org of allOrgs) { + const [idpOrgRes] = await db + .select() + .from(idpOrg) + .where( + and(eq(idpOrg.idpId, idp.idpId), eq(idpOrg.orgId, org.orgId)) + ); + + let roleId: number | undefined = undefined; + + const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping; + const hydratedOrgMapping = hydrateOrgMapping(orgMapping, org.orgId); + + if (hydratedOrgMapping) { + logger.debug("Hydrated Org Mapping", { + hydratedOrgMapping + }); + const orgId = jmespath.search(claims, hydratedOrgMapping); + logger.debug("Extraced Org ID", { orgId }); + if (orgId !== true && orgId !== org.orgId) { + // user not allowed to access this org + continue; + } + } + + const roleMapping = idpOrgRes?.roleMapping || defaultRoleMapping; + if (roleMapping) { + logger.debug("Role Mapping", { roleMapping }); + const roleName = jmespath.search(claims, roleMapping); + + if (!roleName) { + logger.error("Role name not found in the ID token", { + roleName + }); + continue; + } + + const [roleRes] = await db + .select() + .from(roles) + .where( + and(eq(roles.orgId, org.orgId), eq(roles.name, roleName)) + ); + + if (!roleRes) { + logger.error("Role not found", { + orgId: org.orgId, + roleName + }); + continue; + } + + roleId = roleRes.roleId; + + userOrgInfo.push({ + orgId: org.orgId, + roleId + }); + } + } + + logger.debug("User org info", { userOrgInfo }); + + let existingUserId = existingUser?.userId; + + // sync the user with the orgs and roles + await db.transaction(async (trx) => { + let userId = existingUser?.userId; + + // create user if not exists + if (!existingUser) { + userId = generateId(15); + + await trx.insert(users).values({ + userId, + username: userIdentifier, + email: email || null, + name: name || null, + type: UserType.OIDC, + idpId: idp.idpId, + emailVerified: true, // OIDC users are always verified + dateCreated: new Date().toISOString() + }); + } else { + // set the name and email + await trx + .update(users) + .set({ + username: userIdentifier, + email: email || null, + name: name || null + }) + .where(eq(users.userId, userId!)); + } + + existingUserId = userId; + + // get all current user orgs + const currentUserOrgs = await trx + .select() + .from(userOrgs) + .where(eq(userOrgs.userId, userId!)); + + // Delete orgs that are no longer valid + const orgsToDelete = currentUserOrgs.filter( + (currentOrg) => + !userOrgInfo.some((newOrg) => newOrg.orgId === currentOrg.orgId) + ); + + if (orgsToDelete.length > 0) { + await trx.delete(userOrgs).where( + and( + eq(userOrgs.userId, userId!), + inArray( + userOrgs.orgId, + orgsToDelete.map((org) => org.orgId) + ) + ) + ); + } + + // Update roles for existing orgs where the role has changed + const orgsToUpdate = currentUserOrgs.filter((currentOrg) => { + const newOrg = userOrgInfo.find( + (newOrg) => newOrg.orgId === currentOrg.orgId + ); + return newOrg && newOrg.roleId !== currentOrg.roleId; + }); + + if (orgsToUpdate.length > 0) { + for (const org of orgsToUpdate) { + const newRole = userOrgInfo.find( + (newOrg) => newOrg.orgId === org.orgId + ); + if (newRole) { + await trx + .update(userOrgs) + .set({ roleId: newRole.roleId }) + .where( + and( + eq(userOrgs.userId, userId!), + eq(userOrgs.orgId, org.orgId) + ) + ); + } + } + } + + // Add new orgs that don't exist yet + const orgsToAdd = userOrgInfo.filter( + (newOrg) => + !currentUserOrgs.some( + (currentOrg) => currentOrg.orgId === newOrg.orgId + ) + ); + + if (orgsToAdd.length > 0) { + await trx.insert(userOrgs).values( + orgsToAdd.map((org) => ({ + userId: userId!, + orgId: org.orgId, + roleId: org.roleId, + dateCreated: new Date().toISOString() + })) + ); + } + }); + + const token = generateSessionToken(); + const sess = await createSession(token, existingUserId!); + const isSecure = req.protocol === "https"; + const cookie = serializeSessionCookie( + token, + isSecure, + new Date(sess.expiresAt) + ); + + res.appendHeader("Set-Cookie", cookie); +} + +function hydrateOrgMapping( + orgMapping: string | null, + orgId: string +): string | undefined { + if (!orgMapping) { + return undefined; + } + return orgMapping.split("{{orgId}}").join(orgId); +} diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts index a589894..6f8580a 100644 --- a/server/routers/idp/updateIdpOrgPolicy.ts +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -1,3 +1,8 @@ +// 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 { z } from "zod"; import { db } from "@server/db"; diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 1104067..d24e319 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -100,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() diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index d0c847d..7d588fe 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -6,15 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { - idp, - idpOidcConfig, - idpOrg, - orgs, - roles, - userOrgs, - users -} from "@server/db/schemas"; +import { idp, idpOidcConfig, users } from "@server/db/schemas"; import { and, eq, inArray } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; @@ -23,12 +15,12 @@ import jsonwebtoken from "jsonwebtoken"; import config from "@server/lib/config"; import { createSession, - generateId, generateSessionToken, serializeSessionCookie } from "@server/auth/sessions/app"; import { decrypt } from "@server/lib/crypto"; -import { UserType } from "@server/types/UserTypes"; +import { oidcAutoProvision } from "./oidcAutoProvision"; +import license from "@server/license/license"; const ensureTrailingSlash = (url: string): string => { return url; @@ -168,9 +160,7 @@ export async function validateOidcCallback( ); const idToken = tokens.idToken(); - logger.debug("ID token", { idToken }); const claims = arctic.decodeIdToken(idToken); - logger.debug("ID token claims", { claims }); const userIdentifier = jmespath.search( claims, @@ -220,203 +210,25 @@ export async function validateOidcCallback( ); if (existingIdp.idp.autoProvision) { - const allOrgs = await db.select().from(orgs); - - const defaultRoleMapping = existingIdp.idp.defaultRoleMapping; - const defaultOrgMapping = existingIdp.idp.defaultOrgMapping; - - let userOrgInfo: { orgId: string; roleId: number }[] = []; - for (const org of allOrgs) { - const [idpOrgRes] = await db - .select() - .from(idpOrg) - .where( - and( - eq(idpOrg.idpId, existingIdp.idp.idpId), - eq(idpOrg.orgId, org.orgId) - ) - ); - - let roleId: number | undefined = undefined; - - const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping; - const hydratedOrgMapping = hydrateOrgMapping( - orgMapping, - org.orgId + if (!(await license.isUnlocked())) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Auto-provisioning is not available" + ) ); - - if (hydratedOrgMapping) { - logger.debug("Hydrated Org Mapping", { - hydratedOrgMapping - }); - const orgId = jmespath.search(claims, hydratedOrgMapping); - logger.debug("Extraced Org ID", { orgId }); - if (orgId !== true && orgId !== org.orgId) { - // user not allowed to access this org - continue; - } - } - - const roleMapping = - idpOrgRes?.roleMapping || defaultRoleMapping; - if (roleMapping) { - logger.debug("Role Mapping", { roleMapping }); - const roleName = jmespath.search(claims, roleMapping); - - if (!roleName) { - logger.error("Role name not found in the ID token", { - roleName - }); - continue; - } - - const [roleRes] = await db - .select() - .from(roles) - .where( - and( - eq(roles.orgId, org.orgId), - eq(roles.name, roleName) - ) - ); - - if (!roleRes) { - logger.error("Role not found", { - orgId: org.orgId, - roleName - }); - continue; - } - - roleId = roleRes.roleId; - - userOrgInfo.push({ - orgId: org.orgId, - roleId - }); - } } - - logger.debug("User org info", { userOrgInfo }); - - let existingUserId = existingUser?.userId; - - // sync the user with the orgs and roles - await db.transaction(async (trx) => { - let userId = existingUser?.userId; - - // create user if not exists - if (!existingUser) { - userId = generateId(15); - - await trx.insert(users).values({ - userId, - username: userIdentifier, - email: email || null, - name: name || null, - type: UserType.OIDC, - idpId: existingIdp.idp.idpId, - emailVerified: true, // OIDC users are always verified - dateCreated: new Date().toISOString() - }); - } else { - // set the name and email - await trx - .update(users) - .set({ - username: userIdentifier, - email: email || null, - name: name || null - }) - .where(eq(users.userId, userId!)); - } - - existingUserId = userId; - - // get all current user orgs - const currentUserOrgs = await trx - .select() - .from(userOrgs) - .where(eq(userOrgs.userId, userId!)); - - // Delete orgs that are no longer valid - const orgsToDelete = currentUserOrgs.filter( - (currentOrg) => - !userOrgInfo.some( - (newOrg) => newOrg.orgId === currentOrg.orgId - ) - ); - - if (orgsToDelete.length > 0) { - await trx.delete(userOrgs).where( - and( - eq(userOrgs.userId, userId!), - inArray( - userOrgs.orgId, - orgsToDelete.map((org) => org.orgId) - ) - ) - ); - } - - // Update roles for existing orgs where the role has changed - const orgsToUpdate = currentUserOrgs.filter((currentOrg) => { - const newOrg = userOrgInfo.find( - (newOrg) => newOrg.orgId === currentOrg.orgId - ); - return newOrg && newOrg.roleId !== currentOrg.roleId; - }); - - if (orgsToUpdate.length > 0) { - for (const org of orgsToUpdate) { - const newRole = userOrgInfo.find( - (newOrg) => newOrg.orgId === org.orgId - ); - if (newRole) { - await trx - .update(userOrgs) - .set({ roleId: newRole.roleId }) - .where( - and( - eq(userOrgs.userId, userId!), - eq(userOrgs.orgId, org.orgId) - ) - ); - } - } - } - - // Add new orgs that don't exist yet - const orgsToAdd = userOrgInfo.filter( - (newOrg) => - !currentUserOrgs.some( - (currentOrg) => currentOrg.orgId === newOrg.orgId - ) - ); - - if (orgsToAdd.length > 0) { - await trx.insert(userOrgs).values( - orgsToAdd.map((org) => ({ - userId: userId!, - orgId: org.orgId, - roleId: org.roleId, - dateCreated: new Date().toISOString() - })) - ); - } + await oidcAutoProvision({ + idp: existingIdp.idp, + userIdentifier, + email, + name, + claims, + existingUser, + req, + res }); - const token = generateSessionToken(); - const sess = await createSession(token, existingUserId!); - const isSecure = req.protocol === "https"; - const cookie = serializeSessionCookie( - token, - isSecure, - new Date(sess.expiresAt) - ); - - res.appendHeader("Set-Cookie", cookie); - return response(res, { data: { redirectUrl: postAuthRedirectUrl @@ -464,13 +276,3 @@ export async function validateOidcCallback( ); } } - -function hydrateOrgMapping( - orgMapping: string | null, - orgId: string -): string | undefined { - if (!orgMapping) { - return undefined; - } - return orgMapping.split("{{orgId}}").join(orgId); -} diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 8fa5c25..40ab9aa 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -1,3 +1,8 @@ +// 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 * as site from "./site"; import * as org from "./org"; import * as resource from "./resource"; diff --git a/server/routers/license/activateLicense.ts b/server/routers/license/activateLicense.ts index 3826277..da2b76c 100644 --- a/server/routers/license/activateLicense.ts +++ b/server/routers/license/activateLicense.ts @@ -1,3 +1,8 @@ +// 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"; diff --git a/server/routers/license/deleteLicenseKey.ts b/server/routers/license/deleteLicenseKey.ts index 6ae5ca2..bea7f9a 100644 --- a/server/routers/license/deleteLicenseKey.ts +++ b/server/routers/license/deleteLicenseKey.ts @@ -1,3 +1,8 @@ +// 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"; diff --git a/server/routers/license/getLicenseStatus.ts b/server/routers/license/getLicenseStatus.ts index e46b4ca..a4e4151 100644 --- a/server/routers/license/getLicenseStatus.ts +++ b/server/routers/license/getLicenseStatus.ts @@ -1,3 +1,8 @@ +// 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"; diff --git a/server/routers/license/index.ts b/server/routers/license/index.ts index 486ca6b..6c848c2 100644 --- a/server/routers/license/index.ts +++ b/server/routers/license/index.ts @@ -1,3 +1,8 @@ +// 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"; diff --git a/server/routers/license/listLicenseKeys.ts b/server/routers/license/listLicenseKeys.ts index 915690d..12a1956 100644 --- a/server/routers/license/listLicenseKeys.ts +++ b/server/routers/license/listLicenseKeys.ts @@ -1,3 +1,8 @@ +// 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"; diff --git a/server/routers/license/recheckStatus.ts b/server/routers/license/recheckStatus.ts index d2ab793..5f0bd94 100644 --- a/server/routers/license/recheckStatus.ts +++ b/server/routers/license/recheckStatus.ts @@ -1,3 +1,8 @@ +// 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"; diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 9198bb8..a857e10 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -318,8 +318,8 @@ async function updateHttpResource( domainId: updatePayload.domainId, enabled: updatePayload.enabled, stickySession: updatePayload.stickySession, - tlsServerName: updatePayload.tlsServerName, - setHostHeader: updatePayload.setHostHeader, + tlsServerName: updatePayload.tlsServerName || null, + setHostHeader: updatePayload.setHostHeader || null, fullDomain: updatePayload.fullDomain }) .where(eq(resources.resourceId, resource.resourceId)) diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index f95fb25..002febc 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -42,7 +42,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; const formSchema = z.object({ - username: z.string(), + email: z.string().email({ message: "Please enter a valid email" }), roleId: z.string().min(1, { message: "Please select a role" }) }); @@ -59,7 +59,7 @@ export default function AccessControlsPage() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - username: user.username!, + email: user.email!, roleId: user.roleId?.toString() } }); diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx index c10a82d..69fe717 100644 --- a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx +++ b/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { DataTable } from "@app/components/ui/data-table"; diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx index 893f6d7..89e4784 100644 --- a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx +++ b/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx index 4a7b319..a4c13c9 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx @@ -1,3 +1,8 @@ +// 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 { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx index e54f442..7df37cd 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx @@ -1,3 +1,8 @@ +// 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 { redirect } from "next/navigation"; export default async function ApiKeysPage(props: { diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx index 624d13a..d1e6f51 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; diff --git a/src/app/[orgId]/settings/api-keys/create/page.tsx b/src/app/[orgId]/settings/api-keys/create/page.tsx index d3e7e34..3ede2ac 100644 --- a/src/app/[orgId]/settings/api-keys/create/page.tsx +++ b/src/app/[orgId]/settings/api-keys/create/page.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx index c6e48d2..ef1e3dd 100644 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ b/src/app/[orgId]/settings/api-keys/page.tsx @@ -1,3 +1,8 @@ +// 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 { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; diff --git a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx index ce6133d..a9db3e7 100644 --- a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx @@ -1,6 +1,8 @@ "use client"; -import { ColumnDef } from "@tanstack/react-table"; +import { + ColumnDef, +} from "@tanstack/react-table"; import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { @@ -23,10 +25,6 @@ export function ResourcesDataTable({ searchColumn="name" onAdd={createResource} addButtonText="Add Resource" - defaultSort={{ - id: "name", - desc: false - }} /> ); } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index ddf255e..90e05ff 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -320,10 +320,8 @@ export default function ReverseProxyTargets(props: { AxiosResponse >(`/resource/${params.resourceId}/target`, data); target.targetId = res.data.data.targetId; - target.new = false; } else if (target.updated) { await api.post(`/target/${target.targetId}`, data); - target.updated = false; } } @@ -798,12 +796,6 @@ export default function ReverseProxyTargets(props: { type="submit" variant="outlinePrimary" className="mt-6" - disabled={ - !( - addTargetForm.getValues("ip") && - addTargetForm.getValues("port") - ) - } > Add Target diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx index f7b3914..2a9fa00 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx @@ -64,6 +64,7 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; +import { Separator } from "@app/components/ui/separator"; import { InfoPopup } from "@app/components/ui/info-popup"; import { isValidCIDR, diff --git a/src/app/[orgId]/settings/sites/SitesDataTable.tsx b/src/app/[orgId]/settings/sites/SitesDataTable.tsx index 76beca6..08d9795 100644 --- a/src/app/[orgId]/settings/sites/SitesDataTable.tsx +++ b/src/app/[orgId]/settings/sites/SitesDataTable.tsx @@ -1,6 +1,8 @@ "use client"; -import { ColumnDef } from "@tanstack/react-table"; +import { + ColumnDef, +} from "@tanstack/react-table"; import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { @@ -23,10 +25,6 @@ export function SitesDataTable({ searchColumn="name" onAdd={createSite} addButtonText="Add Site" - defaultSort={{ - id: "name", - desc: false - }} /> ); } diff --git a/src/app/admin/api-keys/ApiKeysDataTable.tsx b/src/app/admin/api-keys/ApiKeysDataTable.tsx index b7e2ed0..f65949a 100644 --- a/src/app/admin/api-keys/ApiKeysDataTable.tsx +++ b/src/app/admin/api-keys/ApiKeysDataTable.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { diff --git a/src/app/admin/api-keys/ApiKeysTable.tsx b/src/app/admin/api-keys/ApiKeysTable.tsx index a89157b..c44d43f 100644 --- a/src/app/admin/api-keys/ApiKeysTable.tsx +++ b/src/app/admin/api-keys/ApiKeysTable.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/app/admin/api-keys/[apiKeyId]/layout.tsx b/src/app/admin/api-keys/[apiKeyId]/layout.tsx index 768ad30..be3147e 100644 --- a/src/app/admin/api-keys/[apiKeyId]/layout.tsx +++ b/src/app/admin/api-keys/[apiKeyId]/layout.tsx @@ -1,3 +1,8 @@ +// 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 { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; diff --git a/src/app/admin/api-keys/[apiKeyId]/page.tsx b/src/app/admin/api-keys/[apiKeyId]/page.tsx index 910d1b5..b0e4c3e 100644 --- a/src/app/admin/api-keys/[apiKeyId]/page.tsx +++ b/src/app/admin/api-keys/[apiKeyId]/page.tsx @@ -1,3 +1,8 @@ +// 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 { redirect } from "next/navigation"; export default async function ApiKeysPage(props: { diff --git a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx index 70c2c55..c468c13 100644 --- a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx +++ b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx index c762fed..c76b185 100644 --- a/src/app/admin/api-keys/create/page.tsx +++ b/src/app/admin/api-keys/create/page.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx index 077c9be..b4a0080 100644 --- a/src/app/admin/api-keys/page.tsx +++ b/src/app/admin/api-keys/page.tsx @@ -1,3 +1,8 @@ +// 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 { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 857c7e4..f7844c7 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -232,6 +232,7 @@ export default function GeneralPage() { defaultChecked={form.getValues( "autoProvision" )} + disabled={!isUnlocked()} onCheckedChange={(checked) => { form.setValue( "autoProvision", @@ -239,6 +240,14 @@ export default function GeneralPage() { ); }} /> + {!isUnlocked() && ( + + Professional + + )} When enabled, users will be diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index 9913c8d..d244e13 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -43,7 +43,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { }, { title: "Organization Policies", - href: `/admin/idp/${params.idpId}/policies` + href: `/admin/idp/${params.idpId}/policies`, + showProfessional: true } ]; diff --git a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx index 2873b80..222e98e 100644 --- a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx +++ b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx index 09ba309..df78c64 100644 --- a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx +++ b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index ba10806..9fb9b49 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { useEffect, useState } from "react"; @@ -372,7 +377,9 @@ export default function PoliciesPage() { - The result of this + JMESPath to extract role + information from the ID + token. The result of this expression must return the role name as defined in the organization as a string. @@ -394,10 +401,13 @@ export default function PoliciesPage() { - This expression must return - thr org ID or true for the - user to be allowed to access - the organization. + JMESPath to extract + organization information + from the ID token. This + expression must return thr + org ID or true for the user + to be allowed to access the + organization. @@ -568,6 +578,8 @@ export default function PoliciesPage() { + JMESPath to extract role + information from the ID token. The result of this expression must return the role name as defined in the organization as a @@ -591,6 +603,8 @@ export default function PoliciesPage() { + JMESPath to extract organization + information from the ID token. This expression must return the org ID or true for the user to be allowed to access the diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 85ec0f7..034cc69 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -192,6 +192,7 @@ export default function Page() { defaultChecked={form.getValues( "autoProvision" )} + disabled={!isUnlocked()} onCheckedChange={(checked) => { form.setValue( "autoProvision", @@ -199,6 +200,14 @@ export default function Page() { ); }} /> + {!isUnlocked() && ( + + Professional + + )} When enabled, users will be @@ -412,7 +421,7 @@ export default function Page() { - The path to the user + The JMESPath to the user identifier in the ID token @@ -433,7 +442,7 @@ export default function Page() { - The path to the + The JMESPath to the user's email in the ID token @@ -454,7 +463,7 @@ export default function Page() { - The path to the + The JMESPath to the user's name in the ID token diff --git a/src/app/admin/license/LicenseKeysDataTable.tsx b/src/app/admin/license/LicenseKeysDataTable.tsx index d07164f..98ed814 100644 --- a/src/app/admin/license/LicenseKeysDataTable.tsx +++ b/src/app/admin/license/LicenseKeysDataTable.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/src/app/admin/license/components/SitePriceCalculator.tsx b/src/app/admin/license/components/SitePriceCalculator.tsx index 7f28b2c..cf771b5 100644 --- a/src/app/admin/license/components/SitePriceCalculator.tsx +++ b/src/app/admin/license/components/SitePriceCalculator.tsx @@ -1,3 +1,8 @@ +// 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 { useState } from "react"; import { Button } from "@app/components/ui/button"; import { MinusCircle, PlusCircle } from "lucide-react"; @@ -102,6 +107,35 @@ export function SitePriceCalculator({
+ {mode === "license" && ( +
+ + License fee: + + + ${licenseFlatRate.toFixed(2)} + +
+ )} +
+ + Price per site: + + + ${pricePerSite.toFixed(2)} + +
+
+ + Number of sites: + + {siteCount} +
+
+ Total: + ${totalCost.toFixed(2)} / mo +
+

For the most up-to-date pricing and discounts, please visit the{" "} @@ -123,7 +157,7 @@ export function SitePriceCalculator({ diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index b86c18b..a967889 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { useState, useEffect } from "react"; @@ -44,7 +49,7 @@ import { } from "@app/components/Settings"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Badge } from "@app/components/ui/badge"; -import { Check, Heart, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react"; +import { Check, ShieldCheck, ShieldOff } from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; import { Progress } from "@app/components/ui/progress"; import { MinusCircle, PlusCircle } from "lucide-react"; @@ -52,8 +57,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { SitePriceCalculator } from "./components/SitePriceCalculator"; import Link from "next/link"; import { Checkbox } from "@app/components/ui/checkbox"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; const formSchema = z.object({ licenseKey: z @@ -92,7 +95,6 @@ export default function LicensePage() { const [isActivatingLicense, setIsActivatingLicense] = useState(false); const [isDeletingLicense, setIsDeletingLicense] = useState(false); const [isRecheckingLicense, setIsRecheckingLicense] = useState(false); - const { supporterStatus } = useSupporterStatusContext(); const form = useForm>({ resolver: zodResolver(formSchema), @@ -368,18 +370,6 @@ export default function LicensePage() { description="View and manage license keys in the system" /> - - - - About Licensing - - - This is for business and enterprise users who are using - Pangolin in a commercial environment. If you are using - Pangolin for personal use, you can ignore this section. - - - @@ -397,25 +387,18 @@ export default function LicensePage() { {licenseStatus?.tier === "PROFESSIONAL" - ? "Commercial License" + ? "Professional License" : licenseStatus?.tier === "ENTERPRISE" - ? "Commercial License" + ? "Enterprise License" : "Licensed"}

) : (
- {supporterStatus?.visible ? ( -
- Community Edition -
- ) : ( -
- - Community Edition -
- )} +
+ Not Licensed +
)} diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 79f3294..9a149f7 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -1,11 +1,6 @@ import ProfileIcon from "@app/components/ProfileIcon"; -import { Separator } from "@app/components/ui/separator"; -import { priv } from "@app/lib/api"; import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; -import { GetLicenseStatusResponse } from "@server/routers/license"; -import { AxiosResponse } from "axios"; -import { ExternalLink } from "lucide-react"; import { Metadata } from "next"; import { cache } from "react"; @@ -22,68 +17,21 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { const getUser = cache(verifySession); const user = await getUser(); - const licenseStatusRes = await cache( - async () => - await priv.get>( - "/license/status" - ) - )(); - const licenseStatus = licenseStatusRes.data.data; - return (
{user && ( -
+
)}
-
{children}
+
+ {children} +
- - {!( - licenseStatus.isHostLicensed && licenseStatus.isLicenseValid - ) && ( - - )}
); } diff --git a/src/app/components/LicenseViolation.tsx b/src/app/components/LicenseViolation.tsx index 4dd6cbf..75d544d 100644 --- a/src/app/components/LicenseViolation.tsx +++ b/src/app/components/LicenseViolation.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { Button } from "@app/components/ui/button"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 22b478b..e0089bc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,7 +12,6 @@ import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey"; import LicenseStatusProvider from "@app/providers/LicenseStatusProvider"; import { GetLicenseStatusResponse } from "@server/routers/license"; import LicenseViolation from "./components/LicenseViolation"; -import { cache } from "react"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, @@ -41,12 +40,10 @@ export default async function RootLayout({ supporterData.visible = res.data.data.visible; supporterData.tier = res.data.data.tier; - const licenseStatusRes = await cache( - async () => - await priv.get>( - "/license/status" - ) - )(); + const licenseStatusRes = + await priv.get>( + "/license/status" + ); const licenseStatus = licenseStatusRes.data.data; return ( diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 8ea3c08..b05bf30 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -68,7 +68,8 @@ export const orgNavItems: SidebarNavItem[] = [ { title: "API Keys", href: "/{orgId}/settings/api-keys", - icon: + icon: , + showProfessional: true }, { title: "Settings", @@ -86,7 +87,8 @@ export const adminNavItems: SidebarNavItem[] = [ { title: "API Keys", href: "/admin/api-keys", - icon: + icon: , + showProfessional: true }, { title: "Identity Providers", diff --git a/src/components/AuthFooter.tsx b/src/components/AuthFooter.tsx deleted file mode 100644 index a1c0795..0000000 --- a/src/components/AuthFooter.tsx +++ /dev/null @@ -1,3 +0,0 @@ -"use client"; - -export function AuthFooter() {} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index e092552..12cb09d 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -22,7 +22,6 @@ import { Breadcrumbs } from "@app/components/Breadcrumbs"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useUserContext } from "@app/hooks/useUserContext"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; interface LayoutProps { children: React.ReactNode; @@ -59,7 +58,6 @@ export function Layout({ const pathname = usePathname(); const isAdminPage = pathname?.startsWith("/admin"); const { user } = useUserContext(); - const { isUnlocked } = useLicenseStatusContext(); return (
@@ -209,9 +207,7 @@ export function Layout({ rel="noopener noreferrer" className="flex items-center justify-center gap-1" > - {!isUnlocked() - ? "Community Edition" - : "Commercial Edition"} + Open Source
diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index c0b9a4e..a6f9add 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { CheckboxWithLabel } from "@app/components/ui/checkbox"; @@ -21,6 +26,7 @@ function getActionsCategories(root: boolean) { "Update Organization": "updateOrg", "Get Organization User": "getOrgUser", "List Organization Domains": "listOrgDomains", + "Check Org ID": "checkOrgId", }, Site: { diff --git a/src/components/ProfessionalContentOverlay.tsx b/src/components/ProfessionalContentOverlay.tsx index d35b09b..cd484a2 100644 --- a/src/components/ProfessionalContentOverlay.tsx +++ b/src/components/ProfessionalContentOverlay.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import { cn } from "@app/lib/cn"; diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 410d309..7fa689f 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -55,7 +55,7 @@ export function SettingsSectionFooter({ }: { children: React.ReactNode; }) { - return
{children}
; + return
{children}
; } export function SettingsSectionGrid({ diff --git a/src/components/SupporterStatus.tsx b/src/components/SupporterStatus.tsx index bd09200..48abf7b 100644 --- a/src/components/SupporterStatus.tsx +++ b/src/components/SupporterStatus.tsx @@ -193,7 +193,7 @@ export default function SupporterStatus() { contribution allows us to commit more time to maintain and add new features to the application for everyone. We will never use this to paywall - features. This is separate from any Commercial + features. This is separate from the Professional Edition.

diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index c955605..b544b75 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -31,7 +31,7 @@ import { CardTitle } from "@app/components/ui/card"; -type DataTableProps = { +interface DataTableProps { columns: ColumnDef[]; data: TData[]; title?: string; @@ -39,11 +39,7 @@ type DataTableProps = { onAdd?: () => void; searchPlaceholder?: string; searchColumn?: string; - defaultSort?: { - id: string; - desc: boolean; - }; -}; +} export function DataTable({ columns, @@ -52,12 +48,9 @@ export function DataTable({ addButtonText, onAdd, searchPlaceholder = "Search...", - searchColumn = "name", - defaultSort + searchColumn = "name" }: DataTableProps) { - const [sorting, setSorting] = useState( - defaultSort ? [defaultSort] : [] - ); + const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState([]); diff --git a/src/contexts/apiKeyContext.ts b/src/contexts/apiKeyContext.ts index 58b091f..dd6c9b8 100644 --- a/src/contexts/apiKeyContext.ts +++ b/src/contexts/apiKeyContext.ts @@ -1,3 +1,8 @@ +// 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 { GetApiKeyResponse } from "@server/routers/apiKeys"; import { createContext } from "react"; diff --git a/src/contexts/licenseStatusContext.ts b/src/contexts/licenseStatusContext.ts index ad58959..eca6357 100644 --- a/src/contexts/licenseStatusContext.ts +++ b/src/contexts/licenseStatusContext.ts @@ -1,3 +1,8 @@ +// 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 { LicenseStatus } from "@server/license/license"; import { createContext } from "react"; diff --git a/src/hooks/useApikeyContext.ts b/src/hooks/useApikeyContext.ts index 6c9a829..3ebcbdd 100644 --- a/src/hooks/useApikeyContext.ts +++ b/src/hooks/useApikeyContext.ts @@ -1,3 +1,8 @@ +// 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 ApiKeyContext from "@app/contexts/apiKeyContext"; import { useContext } from "react"; diff --git a/src/hooks/useLicenseStatusContext.ts b/src/hooks/useLicenseStatusContext.ts index bc4a776..b1da343 100644 --- a/src/hooks/useLicenseStatusContext.ts +++ b/src/hooks/useLicenseStatusContext.ts @@ -1,3 +1,8 @@ +// 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 LicenseStatusContext from "@app/contexts/licenseStatusContext"; import { useContext } from "react"; diff --git a/src/providers/ApiKeyProvider.tsx b/src/providers/ApiKeyProvider.tsx index 43a2a9b..13061da 100644 --- a/src/providers/ApiKeyProvider.tsx +++ b/src/providers/ApiKeyProvider.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import ApiKeyContext from "@app/contexts/apiKeyContext"; diff --git a/src/providers/LicenseStatusProvider.tsx b/src/providers/LicenseStatusProvider.tsx index 1f8d99c..c3fe968 100644 --- a/src/providers/LicenseStatusProvider.tsx +++ b/src/providers/LicenseStatusProvider.tsx @@ -1,3 +1,8 @@ +// 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. + "use client"; import LicenseStatusContext from "@app/contexts/licenseStatusContext";