Compare commits

...

14 commits

Author SHA1 Message Date
Milo Schwartz
1f584bf3e8
Merge pull request #717 from fosrl/dev
1.4.0
2025-05-13 11:12:03 -04:00
miloschwartz
5b0200154a
add feature parity 2025-05-13 11:09:38 -04:00
Milo Schwartz
a512148348
Merge pull request #701 from fosrl/dev
1.3.2
2025-05-10 11:30:07 -04:00
miloschwartz
d9eccd6c13
bump version 2025-05-10 11:28:22 -04:00
miloschwartz
492669f68a
set default congig values 2025-05-09 18:32:14 -04:00
miloschwartz
caded23b51
allow root path 2025-05-09 17:37:55 -04:00
miloschwartz
e9cc48a3ae
fix bug causing duplicate targets 2025-05-09 17:18:42 -04:00
miloschwartz
4ed98c227b
fix setting tlsServerName and hostHeader conflict 2025-05-09 17:12:01 -04:00
miloschwartz
f66fb7d4a3
fix justification for profile icon 2025-05-09 17:09:22 -04:00
miloschwartz
f25990a9a7
add id token and claims to debug logs 2025-05-09 16:46:51 -04:00
miloschwartz
21d5b67ef1
Merge branch 'main' into dev 2025-05-09 16:44:09 -04:00
Milo Schwartz
198810121c
Update README.md 2025-05-09 16:44:01 -04:00
Owen
83c0379c6b
Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-05-03 22:04:29 -04:00
Owen
9a167b5acb
Dont overwrite the secret and crowdsec vars 2025-05-02 14:16:10 -04:00
99 changed files with 559 additions and 825 deletions

32
LICENSE
View file

@ -1,35 +1,3 @@
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 GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007

View file

@ -64,15 +64,15 @@ func main() {
} }
var config Config var config Config
config.DoCrowdsecInstall = false
config.Secret = generateRandomSecretKey()
// check if there is already a config file // check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil { if _, err := os.Stat("config/config.yml"); err != nil {
config = collectUserInput(reader) config = collectUserInput(reader)
loadVersions(&config) loadVersions(&config)
config.DoCrowdsecInstall = false
config.Secret = generateRandomSecretKey()
if err := createConfigFiles(config); err != nil { if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err) fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1) os.Exit(1)

BIN
newt Executable file

Binary file not shown.

View file

@ -6,7 +6,7 @@ import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer"; import { createInternalServer } from "./internalServer";
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas";
import { createIntegrationApiServer } from "./integrationApiServer"; import { createIntegrationApiServer } from "./integrationApiServer";
import license from "./license/license.js"; import config from "@server/lib/config";
async function startServers() { async function startServers() {
await runSetupFunctions(); await runSetupFunctions();
@ -17,7 +17,7 @@ async function startServers() {
const nextServer = await createNextServer(); const nextServer = await createNextServer();
let integrationServer; let integrationServer;
if (await license.isUnlocked()) { if (config.getRawConfig().flags?.enable_integration_api) {
integrationServer = createIntegrationApiServer(); integrationServer = createIntegrationApiServer();
} }

View file

@ -1,8 +1,3 @@
// 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 express from "express";
import cors from "cors"; import cors from "cors";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
@ -11,7 +6,6 @@ import logger from "@server/logger";
import { import {
errorHandlerMiddleware, errorHandlerMiddleware,
notFoundMiddleware, notFoundMiddleware,
verifyValidLicense
} from "@server/middlewares"; } from "@server/middlewares";
import { authenticated, unauthenticated } from "@server/routers/integration"; import { authenticated, unauthenticated } from "@server/routers/integration";
import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { logIncomingMiddleware } from "./middlewares/logIncoming";
@ -26,8 +20,6 @@ const externalPort = config.getRawConfig().server.integration_port;
export function createIntegrationApiServer() { export function createIntegrationApiServer() {
const apiServer = express(); const apiServer = express();
apiServer.use(verifyValidLicense);
if (config.getRawConfig().server.trust_proxy) { if (config.getRawConfig().server.trust_proxy) {
apiServer.set("trust proxy", 1); apiServer.set("trust proxy", 1);
} }

View file

@ -29,9 +29,12 @@ const configSchema = z.object({
.optional() .optional()
.pipe(z.string().url()) .pipe(z.string().url())
.transform((url) => url.toLowerCase()), .transform((url) => url.toLowerCase()),
log_level: z.enum(["debug", "info", "warn", "error"]), log_level: z
save_logs: z.boolean(), .enum(["debug", "info", "warn", "error"])
log_failed_attempts: z.boolean().optional() .optional()
.default("info"),
save_logs: z.boolean().optional().default(false),
log_failed_attempts: z.boolean().optional().default(false)
}), }),
domains: z domains: z
.record( .record(
@ -41,8 +44,8 @@ const configSchema = z.object({
.string() .string()
.nonempty("base_domain must not be empty") .nonempty("base_domain must not be empty")
.transform((url) => url.toLowerCase()), .transform((url) => url.toLowerCase()),
cert_resolver: z.string().optional(), cert_resolver: z.string().optional().default("letsencrypt"),
prefer_wildcard_cert: z.boolean().optional() prefer_wildcard_cert: z.boolean().optional().default(false)
}) })
) )
.refine( .refine(
@ -62,19 +65,42 @@ const configSchema = z.object({
server: z.object({ server: z.object({
integration_port: portSchema integration_port: portSchema
.optional() .optional()
.default(3003)
.transform(stoi) .transform(stoi)
.pipe(portSchema.optional()), .pipe(portSchema.optional()),
external_port: portSchema.optional().transform(stoi).pipe(portSchema), external_port: portSchema
internal_port: portSchema.optional().transform(stoi).pipe(portSchema), .optional()
next_port: portSchema.optional().transform(stoi).pipe(portSchema), .default(3000)
internal_hostname: z.string().transform((url) => url.toLowerCase()), .transform(stoi)
session_cookie_name: z.string(), .pipe(portSchema),
resource_access_token_param: z.string(), internal_port: portSchema
resource_access_token_headers: z.object({ .optional()
id: z.string(), .default(3001)
token: z.string() .transform(stoi)
}), .pipe(portSchema),
resource_session_request_param: z.string(), 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"),
dashboard_session_length_hours: z dashboard_session_length_hours: z
.number() .number()
.positive() .positive()
@ -102,35 +128,61 @@ const configSchema = z.object({
.transform(getEnvOrYaml("SERVER_SECRET")) .transform(getEnvOrYaml("SERVER_SECRET"))
.pipe(z.string().min(8)) .pipe(z.string().min(8))
}), }),
traefik: z.object({ traefik: z
http_entrypoint: z.string(), .object({
https_entrypoint: z.string().optional(), http_entrypoint: z.string().optional().default("web"),
additional_middlewares: z.array(z.string()).optional() https_entrypoint: z.string().optional().default("websecure"),
}), additional_middlewares: z.array(z.string()).optional()
gerbil: z.object({ })
start_port: portSchema.optional().transform(stoi).pipe(portSchema), .optional()
base_endpoint: z .default({}),
.string() gerbil: z
.optional() .object({
.pipe(z.string()) start_port: portSchema
.transform((url) => url.toLowerCase()), .optional()
use_subdomain: z.boolean(), .default(51820)
subnet_group: z.string(), .transform(stoi)
block_size: z.number().positive().gt(0), .pipe(portSchema),
site_block_size: z.number().positive().gt(0) base_endpoint: z
}), .string()
rate_limits: z.object({ .optional()
global: z.object({ .pipe(z.string())
window_minutes: z.number().positive().gt(0), .transform((url) => url.toLowerCase()),
max_requests: z.number().positive().gt(0) use_subdomain: z.boolean().optional().default(false),
}), subnet_group: z.string().optional().default("100.89.137.0/20"),
auth: z block_size: z.number().positive().gt(0).optional().default(24),
.object({ site_block_size: z.number().positive().gt(0).optional().default(30)
window_minutes: z.number().positive().gt(0), })
max_requests: z.number().positive().gt(0) .optional()
}) .default({}),
.optional() 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({}),
email: z email: z
.object({ .object({
smtp_host: z.string().optional(), smtp_host: z.string().optional(),
@ -164,7 +216,8 @@ const configSchema = z.object({
disable_user_create_org: z.boolean().optional(), disable_user_create_org: z.boolean().optional(),
allow_raw_resources: z.boolean().optional(), allow_raw_resources: z.boolean().optional(),
allow_base_domain_resources: z.boolean().optional(), allow_base_domain_resources: z.boolean().optional(),
allow_local_sites: z.boolean().optional() allow_local_sites: z.boolean().optional(),
enable_integration_api: z.boolean().optional()
}) })
.optional() .optional()
}); });

View file

@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // This is a placeholder value replaced by the build process
export const APP_VERSION = "1.3.0"; export const APP_VERSION = "1.4.0";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);

View file

@ -9,6 +9,10 @@ export function isValidIP(ip: string): boolean {
} }
export function isValidUrlGlobPattern(pattern: string): boolean { export function isValidUrlGlobPattern(pattern: string): boolean {
if (pattern === "/") {
return true;
}
// Remove leading slash if present // Remove leading slash if present
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;

View file

@ -1,8 +1,3 @@
// 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 db from "@server/db";
import { hostMeta, licenseKey, sites } from "@server/db/schemas"; import { hostMeta, licenseKey, sites } from "@server/db/schemas";
import logger from "@server/logger"; import logger from "@server/logger";

View file

@ -1,8 +1,3 @@
// 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"; import * as crypto from "crypto";
/** /**

View file

@ -1,8 +1,3 @@
// 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 "./verifyApiKey";
export * from "./verifyApiKeyOrgAccess"; export * from "./verifyApiKeyOrgAccess";
export * from "./verifyApiKeyHasAction"; export * from "./verifyApiKeyHasAction";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas"; import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { verifyPassword } from "@server/auth/password";
import db from "@server/db"; import db from "@server/db";
import { apiKeys } from "@server/db/schemas"; import { apiKeys } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { apiKeys, apiKeyOrg } from "@server/db/schemas"; import { apiKeys, apiKeyOrg } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,8 +1,3 @@
// 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 logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { apiKeyOrg } from "@server/db/schemas"; import { apiKeyOrg } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { resources, apiKeyOrg } from "@server/db/schemas"; import { resources, apiKeyOrg } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { roles, apiKeyOrg } from "@server/db/schemas"; import { roles, apiKeyOrg } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schemas"; import { userOrgs } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { import {

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { resources, targets, apiKeyOrg } from "@server/db/schemas"; import { resources, targets, apiKeyOrg } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schemas"; import { userOrgs } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas"; import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,8 +1,3 @@
// 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 { NextFunction, Request, Response } from "express";
import db from "@server/db"; import db from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,8 +1,3 @@
// 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 { NextFunction, Request, Response } from "express";
import db from "@server/db"; import db from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -1,8 +1,3 @@
// 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 "./createRootApiKey";
export * from "./deleteApiKey"; export * from "./deleteApiKey";
export * from "./getApiKey"; export * from "./getApiKey";

View file

@ -1,8 +1,3 @@
// 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 { db } from "@server/db";
import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas"; import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas";
import logger from "@server/logger"; import logger from "@server/logger";

View file

@ -1,8 +1,3 @@
// 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 { db } from "@server/db";
import { apiKeyOrg, apiKeys } from "@server/db/schemas"; import { apiKeyOrg, apiKeys } from "@server/db/schemas";
import logger from "@server/logger"; import logger from "@server/logger";

View file

@ -1,8 +1,3 @@
// 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 { db } from "@server/db";
import { apiKeys } from "@server/db/schemas"; import { apiKeys } from "@server/db/schemas";
import logger from "@server/logger"; import logger from "@server/logger";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -1,61 +1,136 @@
import { isPathAllowed } from './verifySession';
import { assertEquals } from '@test/assert'; 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() { function runTests() {
console.log('Running path matching tests...'); console.log('Running path matching tests...');
// Test exact matching // Test exact matching
assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed'); assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed');
assertEquals(isPathAllowed('foo', 'bar'), false, 'Different segments should not match'); 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/bar'), true, 'Exact multi-segment match should be allowed');
assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed'); assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed');
// Test with leading and trailing slashes // Test with leading and trailing slashes
assertEquals(isPathAllowed('/foo', 'foo'), true, 'Pattern with leading slash should match'); 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 trailing slash should match');
assertEquals(isPathAllowed('/foo/', 'foo'), true, 'Pattern with both leading and trailing slashes 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'); assertEquals(isPathAllowed('foo', '/foo/'), true, 'Path with leading and trailing slashes should match');
// Test simple wildcard matching // Test simple wildcard matching
assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment'); assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment');
assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments'); assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments');
assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match'); assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match');
assertEquals(isPathAllowed('foo/*', 'foo/bar'), true, 'Wildcard suffix 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'); assertEquals(isPathAllowed('foo/*/baz', 'foo/bar/baz'), true, 'Wildcard in middle should match');
// Test multiple wildcards // Test multiple wildcards
assertEquals(isPathAllowed('*/*', 'foo/bar'), true, 'Multiple wildcards should match corresponding segments'); 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/bar/baz'), true, 'Three wildcards should match three segments');
assertEquals(isPathAllowed('foo/*/*', 'foo/bar/baz'), true, 'Specific prefix with wildcards should match'); 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'); assertEquals(isPathAllowed('*/*/baz', 'foo/bar/baz'), true, 'Wildcards with specific suffix should match');
// Test wildcard consumption behavior // Test wildcard consumption behavior
assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments'); assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments');
assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional'); assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional');
assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments'); assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments');
assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped'); assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped');
// Test complex nested paths // Test complex nested paths
assertEquals(isPathAllowed('api/*/users', 'api/v1/users'), true, 'API versioning pattern should match'); 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/*', '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'); assertEquals(isPathAllowed('api/*/users/*/profile', 'api/v1/users/123/profile'), true, 'Nested API pattern should match');
// Test for the requested padbootstrap* pattern // Test for the requested padbootstrap* pattern
assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap');
assertEquals(isPathAllowed('padbootstrap*', 'padbootstrapv1'), true, 'padbootstrap* should match padbootstrapv1'); 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'), false, 'padbootstrap* should not match padbootstrap/files');
assertEquals(isPathAllowed('padbootstrap*/*', 'padbootstrap/files'), true, 'padbootstrap*/* should 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)'); assertEquals(isPathAllowed('padbootstrap*/files', 'padbootstrapv1/files'), true, 'padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)');
// Test wildcard edge cases // Test wildcard edge cases
assertEquals(isPathAllowed('*/*/*/*/*/*', 'a/b'), true, 'Many wildcards can match few segments'); 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'); 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 // Test patterns with partial segment matches
assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap-123'), true, 'Wildcards in isPathAllowed should be segment-based, not character-based'); 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('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('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!'); console.log('All tests passed!');
} }
@ -64,4 +139,4 @@ try {
runTests(); runTests();
} catch (error) { } catch (error) {
console.error('Test failed:', error); console.error('Test failed:', error);
} }

View file

@ -30,7 +30,6 @@ import {
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
verifyIsLoggedInUser, verifyIsLoggedInUser,
verifyApiKeyAccess, verifyApiKeyAccess,
verifyValidLicense
} from "@server/middlewares"; } from "@server/middlewares";
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
@ -531,28 +530,24 @@ authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
authenticated.put( authenticated.put(
"/idp/:idpId/org/:orgId", "/idp/:idpId/org/:orgId",
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
idp.createIdpOrgPolicy idp.createIdpOrgPolicy
); );
authenticated.post( authenticated.post(
"/idp/:idpId/org/:orgId", "/idp/:idpId/org/:orgId",
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
idp.updateIdpOrgPolicy idp.updateIdpOrgPolicy
); );
authenticated.delete( authenticated.delete(
"/idp/:idpId/org/:orgId", "/idp/:idpId/org/:orgId",
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
idp.deleteIdpOrgPolicy idp.deleteIdpOrgPolicy
); );
authenticated.get( authenticated.get(
"/idp/:idpId/org", "/idp/:idpId/org",
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
idp.listIdpOrgPolicies idp.listIdpOrgPolicies
); );
@ -586,49 +581,42 @@ authenticated.post(
authenticated.get( authenticated.get(
`/api-key/:apiKeyId`, `/api-key/:apiKeyId`,
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
apiKeys.getApiKey apiKeys.getApiKey
); );
authenticated.put( authenticated.put(
`/api-key`, `/api-key`,
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
apiKeys.createRootApiKey apiKeys.createRootApiKey
); );
authenticated.delete( authenticated.delete(
`/api-key/:apiKeyId`, `/api-key/:apiKeyId`,
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
apiKeys.deleteApiKey apiKeys.deleteApiKey
); );
authenticated.get( authenticated.get(
`/api-keys`, `/api-keys`,
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
apiKeys.listRootApiKeys apiKeys.listRootApiKeys
); );
authenticated.get( authenticated.get(
`/api-key/:apiKeyId/actions`, `/api-key/:apiKeyId/actions`,
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
apiKeys.listApiKeyActions apiKeys.listApiKeyActions
); );
authenticated.post( authenticated.post(
`/api-key/:apiKeyId/actions`, `/api-key/:apiKeyId/actions`,
verifyValidLicense,
verifyUserIsServerAdmin, verifyUserIsServerAdmin,
apiKeys.setApiKeyActions apiKeys.setApiKeyActions
); );
authenticated.get( authenticated.get(
`/org/:orgId/api-keys`, `/org/:orgId/api-keys`,
verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listApiKeys), verifyUserHasAction(ActionsEnum.listApiKeys),
apiKeys.listOrgApiKeys apiKeys.listOrgApiKeys
@ -636,7 +624,6 @@ authenticated.get(
authenticated.post( authenticated.post(
`/org/:orgId/api-key/:apiKeyId/actions`, `/org/:orgId/api-key/:apiKeyId/actions`,
verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyApiKeyAccess, verifyApiKeyAccess,
verifyUserHasAction(ActionsEnum.setApiKeyActions), verifyUserHasAction(ActionsEnum.setApiKeyActions),
@ -645,7 +632,6 @@ authenticated.post(
authenticated.get( authenticated.get(
`/org/:orgId/api-key/:apiKeyId/actions`, `/org/:orgId/api-key/:apiKeyId/actions`,
verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyApiKeyAccess, verifyApiKeyAccess,
verifyUserHasAction(ActionsEnum.listApiKeyActions), verifyUserHasAction(ActionsEnum.listApiKeyActions),
@ -654,7 +640,6 @@ authenticated.get(
authenticated.put( authenticated.put(
`/org/:orgId/api-key`, `/org/:orgId/api-key`,
verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createApiKey), verifyUserHasAction(ActionsEnum.createApiKey),
apiKeys.createOrgApiKey apiKeys.createOrgApiKey
@ -662,7 +647,6 @@ authenticated.put(
authenticated.delete( authenticated.delete(
`/org/:orgId/api-key/:apiKeyId`, `/org/:orgId/api-key/:apiKeyId`,
verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyApiKeyAccess, verifyApiKeyAccess,
verifyUserHasAction(ActionsEnum.deleteApiKey), verifyUserHasAction(ActionsEnum.deleteApiKey),
@ -671,7 +655,6 @@ authenticated.delete(
authenticated.get( authenticated.get(
`/org/:orgId/api-key/:apiKeyId`, `/org/:orgId/api-key/:apiKeyId`,
verifyValidLicense,
verifyOrgAccess, verifyOrgAccess,
verifyApiKeyAccess, verifyApiKeyAccess,
verifyUserHasAction(ActionsEnum.getApiKey), verifyUserHasAction(ActionsEnum.getApiKey),

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -81,10 +81,6 @@ export async function createOidcIdp(
autoProvision autoProvision
} = parsedBody.data; } = parsedBody.data;
if (!(await license.isUnlocked())) {
autoProvision = false;
}
const key = config.getRawConfig().server.secret; const key = config.getRawConfig().server.secret;
const encryptedSecret = encrypt(clientSecret, key); const encryptedSecret = encrypt(clientSecret, key);

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -1,233 +0,0 @@
// 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);
}

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";

View file

@ -100,10 +100,6 @@ export async function updateOidcIdp(
defaultOrgMapping defaultOrgMapping
} = parsedBody.data; } = parsedBody.data;
if (!(await license.isUnlocked())) {
autoProvision = false;
}
// Check if IDP exists and is of type OIDC // Check if IDP exists and is of type OIDC
const [existingIdp] = await db const [existingIdp] = await db
.select() .select()

View file

@ -6,7 +6,15 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { idp, idpOidcConfig, users } from "@server/db/schemas"; import {
idp,
idpOidcConfig,
idpOrg,
orgs,
roles,
userOrgs,
users
} from "@server/db/schemas";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import * as arctic from "arctic"; import * as arctic from "arctic";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
@ -15,12 +23,12 @@ import jsonwebtoken from "jsonwebtoken";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { import {
createSession, createSession,
generateId,
generateSessionToken, generateSessionToken,
serializeSessionCookie serializeSessionCookie
} from "@server/auth/sessions/app"; } from "@server/auth/sessions/app";
import { decrypt } from "@server/lib/crypto"; import { decrypt } from "@server/lib/crypto";
import { oidcAutoProvision } from "./oidcAutoProvision"; import { UserType } from "@server/types/UserTypes";
import license from "@server/license/license";
const ensureTrailingSlash = (url: string): string => { const ensureTrailingSlash = (url: string): string => {
return url; return url;
@ -160,7 +168,9 @@ export async function validateOidcCallback(
); );
const idToken = tokens.idToken(); const idToken = tokens.idToken();
logger.debug("ID token", { idToken });
const claims = arctic.decodeIdToken(idToken); const claims = arctic.decodeIdToken(idToken);
logger.debug("ID token claims", { claims });
const userIdentifier = jmespath.search( const userIdentifier = jmespath.search(
claims, claims,
@ -210,25 +220,203 @@ export async function validateOidcCallback(
); );
if (existingIdp.idp.autoProvision) { if (existingIdp.idp.autoProvision) {
if (!(await license.isUnlocked())) { const allOrgs = await db.select().from(orgs);
return next(
createHttpError( const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
HttpCode.FORBIDDEN, const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
"Auto-provisioning is not available"
) 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 (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
});
}
} }
await oidcAutoProvision({
idp: existingIdp.idp, logger.debug("User org info", { userOrgInfo });
userIdentifier,
email, let existingUserId = existingUser?.userId;
name,
claims, // sync the user with the orgs and roles
existingUser, await db.transaction(async (trx) => {
req, let userId = existingUser?.userId;
res
// 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()
}))
);
}
}); });
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<ValidateOidcUrlCallbackResponse>(res, { return response<ValidateOidcUrlCallbackResponse>(res, {
data: { data: {
redirectUrl: postAuthRedirectUrl redirectUrl: postAuthRedirectUrl
@ -276,3 +464,13 @@ export async function validateOidcCallback(
); );
} }
} }
function hydrateOrgMapping(
orgMapping: string | null,
orgId: string
): string | undefined {
if (!orgMapping) {
return undefined;
}
return orgMapping.split("{{orgId}}").join(orgId);
}

View file

@ -1,8 +1,3 @@
// 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 site from "./site";
import * as org from "./org"; import * as org from "./org";
import * as resource from "./resource"; import * as resource from "./resource";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View file

@ -1,8 +1,3 @@
// 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 "./getLicenseStatus";
export * from "./activateLicense"; export * from "./activateLicense";
export * from "./listLicenseKeys"; export * from "./listLicenseKeys";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View file

@ -1,8 +1,3 @@
// 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 { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View file

@ -318,8 +318,8 @@ async function updateHttpResource(
domainId: updatePayload.domainId, domainId: updatePayload.domainId,
enabled: updatePayload.enabled, enabled: updatePayload.enabled,
stickySession: updatePayload.stickySession, stickySession: updatePayload.stickySession,
tlsServerName: updatePayload.tlsServerName || null, tlsServerName: updatePayload.tlsServerName,
setHostHeader: updatePayload.setHostHeader || null, setHostHeader: updatePayload.setHostHeader,
fullDomain: updatePayload.fullDomain fullDomain: updatePayload.fullDomain
}) })
.where(eq(resources.resourceId, resource.resourceId)) .where(eq(resources.resourceId, resource.resourceId))

View file

@ -42,7 +42,7 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email({ message: "Please enter a valid email" }), username: z.string(),
roleId: z.string().min(1, { message: "Please select a role" }) roleId: z.string().min(1, { message: "Please select a role" })
}); });
@ -59,7 +59,7 @@ export default function AccessControlsPage() {
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
email: user.email!, username: user.username!,
roleId: user.roleId?.toString() roleId: user.roleId?.toString()
} }
}); });

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { DataTable } from "@app/components/ui/data-table"; import { DataTable } from "@app/components/ui/data-table";

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";

View file

@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";

View file

@ -1,8 +1,3 @@
// 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"; import { redirect } from "next/navigation";
export default async function ApiKeysPage(props: { export default async function ApiKeysPage(props: {

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; import PermissionsSelectBox from "@app/components/PermissionsSelectBox";

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { import {

View file

@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";

View file

@ -1,8 +1,6 @@
"use client"; "use client";
import { import { ColumnDef } from "@tanstack/react-table";
ColumnDef,
} from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table"; import { DataTable } from "@app/components/ui/data-table";
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
@ -25,6 +23,10 @@ export function ResourcesDataTable<TData, TValue>({
searchColumn="name" searchColumn="name"
onAdd={createResource} onAdd={createResource}
addButtonText="Add Resource" addButtonText="Add Resource"
defaultSort={{
id: "name",
desc: false
}}
/> />
); );
} }

View file

@ -320,8 +320,10 @@ export default function ReverseProxyTargets(props: {
AxiosResponse<CreateTargetResponse> AxiosResponse<CreateTargetResponse>
>(`/resource/${params.resourceId}/target`, data); >(`/resource/${params.resourceId}/target`, data);
target.targetId = res.data.data.targetId; target.targetId = res.data.data.targetId;
target.new = false;
} else if (target.updated) { } else if (target.updated) {
await api.post(`/target/${target.targetId}`, data); await api.post(`/target/${target.targetId}`, data);
target.updated = false;
} }
} }
@ -796,6 +798,12 @@ export default function ReverseProxyTargets(props: {
type="submit" type="submit"
variant="outlinePrimary" variant="outlinePrimary"
className="mt-6" className="mt-6"
disabled={
!(
addTargetForm.getValues("ip") &&
addTargetForm.getValues("port")
)
}
> >
Add Target Add Target
</Button> </Button>

View file

@ -64,7 +64,6 @@ import {
InfoSections, InfoSections,
InfoSectionTitle InfoSectionTitle
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import { Separator } from "@app/components/ui/separator";
import { InfoPopup } from "@app/components/ui/info-popup"; import { InfoPopup } from "@app/components/ui/info-popup";
import { import {
isValidCIDR, isValidCIDR,

View file

@ -1,8 +1,6 @@
"use client"; "use client";
import { import { ColumnDef } from "@tanstack/react-table";
ColumnDef,
} from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table"; import { DataTable } from "@app/components/ui/data-table";
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
@ -25,6 +23,10 @@ export function SitesDataTable<TData, TValue>({
searchColumn="name" searchColumn="name"
onAdd={createSite} onAdd={createSite}
addButtonText="Add Site" addButtonText="Add Site"
defaultSort={{
id: "name",
desc: false
}}
/> />
); );
} }

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { import {

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";

View file

@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";

View file

@ -1,8 +1,3 @@
// 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"; import { redirect } from "next/navigation";
export default async function ApiKeysPage(props: { export default async function ApiKeysPage(props: {

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; import PermissionsSelectBox from "@app/components/PermissionsSelectBox";

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { import {

View file

@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";

View file

@ -232,7 +232,6 @@ export default function GeneralPage() {
defaultChecked={form.getValues( defaultChecked={form.getValues(
"autoProvision" "autoProvision"
)} )}
disabled={!isUnlocked()}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
form.setValue( form.setValue(
"autoProvision", "autoProvision",
@ -240,14 +239,6 @@ export default function GeneralPage() {
); );
}} }}
/> />
{!isUnlocked() && (
<Badge
variant="outlinePrimary"
className="ml-2"
>
Professional
</Badge>
)}
</div> </div>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
When enabled, users will be When enabled, users will be

View file

@ -43,8 +43,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
}, },
{ {
title: "Organization Policies", title: "Organization Policies",
href: `/admin/idp/${params.idpId}/policies`, href: `/admin/idp/${params.idpId}/policies`
showProfessional: true
} }
]; ];

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -377,9 +372,7 @@ export default function PoliciesPage() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
JMESPath to extract role The result of this
information from the ID
token. The result of this
expression must return the expression must return the
role name as defined in the role name as defined in the
organization as a string. organization as a string.
@ -401,13 +394,10 @@ export default function PoliciesPage() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
JMESPath to extract This expression must return
organization information thr org ID or true for the
from the ID token. This user to be allowed to access
expression must return thr the organization.
org ID or true for the user
to be allowed to access the
organization.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -578,8 +568,6 @@ export default function PoliciesPage() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
JMESPath to extract role
information from the ID token.
The result of this expression The result of this expression
must return the role name as must return the role name as
defined in the organization as a defined in the organization as a
@ -603,8 +591,6 @@ export default function PoliciesPage() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
JMESPath to extract organization
information from the ID token.
This expression must return the This expression must return the
org ID or true for the user to org ID or true for the user to
be allowed to access the be allowed to access the

View file

@ -192,7 +192,6 @@ export default function Page() {
defaultChecked={form.getValues( defaultChecked={form.getValues(
"autoProvision" "autoProvision"
)} )}
disabled={!isUnlocked()}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
form.setValue( form.setValue(
"autoProvision", "autoProvision",
@ -200,14 +199,6 @@ export default function Page() {
); );
}} }}
/> />
{!isUnlocked() && (
<Badge
variant="outlinePrimary"
className="ml-2"
>
Professional
</Badge>
)}
</div> </div>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
When enabled, users will be When enabled, users will be
@ -421,7 +412,7 @@ export default function Page() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the user The path to the user
identifier in the ID identifier in the ID
token token
</FormDescription> </FormDescription>
@ -442,7 +433,7 @@ export default function Page() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the The path to the
user's email in the ID user's email in the ID
token token
</FormDescription> </FormDescription>
@ -463,7 +454,7 @@ export default function Page() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the The path to the
user's name in the ID user's name in the ID
token token
</FormDescription> </FormDescription>

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";

View file

@ -1,8 +1,3 @@
// 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 { useState } from "react";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { MinusCircle, PlusCircle } from "lucide-react"; import { MinusCircle, PlusCircle } from "lucide-react";
@ -107,35 +102,6 @@ export function SitePriceCalculator({
</div> </div>
<div className="border-t pt-4"> <div className="border-t pt-4">
{mode === "license" && (
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
License fee:
</span>
<span className="font-medium">
${licenseFlatRate.toFixed(2)}
</span>
</div>
)}
<div className="flex justify-between items-center mt-2">
<span className="text-sm font-medium">
Price per site:
</span>
<span className="font-medium">
${pricePerSite.toFixed(2)}
</span>
</div>
<div className="flex justify-between items-center mt-2">
<span className="text-sm font-medium">
Number of sites:
</span>
<span className="font-medium">{siteCount}</span>
</div>
<div className="flex justify-between items-center mt-4 text-lg font-bold">
<span>Total:</span>
<span>${totalCost.toFixed(2)} / mo</span>
</div>
<p className="text-muted-foreground text-sm mt-2 text-center"> <p className="text-muted-foreground text-sm mt-2 text-center">
For the most up-to-date pricing and discounts, For the most up-to-date pricing and discounts,
please visit the{" "} please visit the{" "}
@ -157,7 +123,7 @@ export function SitePriceCalculator({
<Button variant="outline">Cancel</Button> <Button variant="outline">Cancel</Button>
</CredenzaClose> </CredenzaClose>
<Button onClick={continueToPayment}> <Button onClick={continueToPayment}>
Continue to Payment See Purchase Portal
</Button> </Button>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
@ -49,7 +44,7 @@ import {
} from "@app/components/Settings"; } from "@app/components/Settings";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Badge } from "@app/components/ui/badge"; import { Badge } from "@app/components/ui/badge";
import { Check, ShieldCheck, ShieldOff } from "lucide-react"; import { Check, Heart, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import { Progress } from "@app/components/ui/progress"; import { Progress } from "@app/components/ui/progress";
import { MinusCircle, PlusCircle } from "lucide-react"; import { MinusCircle, PlusCircle } from "lucide-react";
@ -57,6 +52,8 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { SitePriceCalculator } from "./components/SitePriceCalculator"; import { SitePriceCalculator } from "./components/SitePriceCalculator";
import Link from "next/link"; import Link from "next/link";
import { Checkbox } from "@app/components/ui/checkbox"; 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({ const formSchema = z.object({
licenseKey: z licenseKey: z
@ -95,6 +92,7 @@ export default function LicensePage() {
const [isActivatingLicense, setIsActivatingLicense] = useState(false); const [isActivatingLicense, setIsActivatingLicense] = useState(false);
const [isDeletingLicense, setIsDeletingLicense] = useState(false); const [isDeletingLicense, setIsDeletingLicense] = useState(false);
const [isRecheckingLicense, setIsRecheckingLicense] = useState(false); const [isRecheckingLicense, setIsRecheckingLicense] = useState(false);
const { supporterStatus } = useSupporterStatusContext();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -370,6 +368,18 @@ export default function LicensePage() {
description="View and manage license keys in the system" description="View and manage license keys in the system"
/> />
<Alert variant="neutral" className="mb-6">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About Licensing
</AlertTitle>
<AlertDescription>
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.
</AlertDescription>
</Alert>
<SettingsContainer> <SettingsContainer>
<SettingsSectionGrid cols={2}> <SettingsSectionGrid cols={2}>
<SettingsSection> <SettingsSection>
@ -387,18 +397,25 @@ export default function LicensePage() {
<Check /> <Check />
{licenseStatus?.tier === {licenseStatus?.tier ===
"PROFESSIONAL" "PROFESSIONAL"
? "Professional License" ? "Commercial License"
: licenseStatus?.tier === : licenseStatus?.tier ===
"ENTERPRISE" "ENTERPRISE"
? "Enterprise License" ? "Commercial License"
: "Licensed"} : "Licensed"}
</div> </div>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-2xl"> {supporterStatus?.visible ? (
Not Licensed <div className="text-2xl">
</div> Community Edition
</div>
) : (
<div className="text-2xl flex items-center gap-2 text-pink-500">
<Heart />
Community Edition
</div>
)}
</div> </div>
)} )}
</div> </div>

View file

@ -1,6 +1,11 @@
import ProfileIcon from "@app/components/ProfileIcon"; 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 { verifySession } from "@app/lib/auth/verifySession";
import UserProvider from "@app/providers/UserProvider"; 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 { Metadata } from "next";
import { cache } from "react"; import { cache } from "react";
@ -17,21 +22,68 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
const getUser = cache(verifySession); const getUser = cache(verifySession);
const user = await getUser(); const user = await getUser();
const licenseStatusRes = await cache(
async () =>
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
)
)();
const licenseStatus = licenseStatusRes.data.data;
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
{user && ( {user && (
<UserProvider user={user}> <UserProvider user={user}>
<div className="p-3"> <div className="p-3 ml-auto">
<ProfileIcon /> <ProfileIcon />
</div> </div>
</UserProvider> </UserProvider>
)} )}
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<div className="w-full max-w-md p-3"> <div className="w-full max-w-md p-3">{children}</div>
{children}
</div>
</div> </div>
{!(
licenseStatus.isHostLicensed && licenseStatus.isLicenseValid
) && (
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600">
<div className="flex items-center space-x-2 whitespace-nowrap">
<span>Pangolin</span>
</div>
<Separator orientation="vertical" />
<a
href="https://fossorial.io/"
target="_blank"
rel="noopener noreferrer"
aria-label="Built by Fossorial"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>Fossorial</span>
<ExternalLink className="w-3 h-3" />
</a>
<Separator orientation="vertical" />
<a
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>Community Edition</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-3 h-3"
>
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
</svg>
</a>
</div>
</footer>
)}
</div> </div>
); );
} }

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";

View file

@ -12,6 +12,7 @@ import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
import LicenseStatusProvider from "@app/providers/LicenseStatusProvider"; import LicenseStatusProvider from "@app/providers/LicenseStatusProvider";
import { GetLicenseStatusResponse } from "@server/routers/license"; import { GetLicenseStatusResponse } from "@server/routers/license";
import LicenseViolation from "./components/LicenseViolation"; import LicenseViolation from "./components/LicenseViolation";
import { cache } from "react";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Dashboard - Pangolin`, title: `Dashboard - Pangolin`,
@ -40,10 +41,12 @@ export default async function RootLayout({
supporterData.visible = res.data.data.visible; supporterData.visible = res.data.data.visible;
supporterData.tier = res.data.data.tier; supporterData.tier = res.data.data.tier;
const licenseStatusRes = const licenseStatusRes = await cache(
await priv.get<AxiosResponse<GetLicenseStatusResponse>>( async () =>
"/license/status" await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
); "/license/status"
)
)();
const licenseStatus = licenseStatusRes.data.data; const licenseStatus = licenseStatusRes.data.data;
return ( return (

View file

@ -68,8 +68,7 @@ export const orgNavItems: SidebarNavItem[] = [
{ {
title: "API Keys", title: "API Keys",
href: "/{orgId}/settings/api-keys", href: "/{orgId}/settings/api-keys",
icon: <KeyRound className="h-4 w-4" />, icon: <KeyRound className="h-4 w-4" />
showProfessional: true
}, },
{ {
title: "Settings", title: "Settings",
@ -87,8 +86,7 @@ export const adminNavItems: SidebarNavItem[] = [
{ {
title: "API Keys", title: "API Keys",
href: "/admin/api-keys", href: "/admin/api-keys",
icon: <KeyRound className="h-4 w-4" />, icon: <KeyRound className="h-4 w-4" />
showProfessional: true
}, },
{ {
title: "Identity Providers", title: "Identity Providers",

View file

@ -0,0 +1,3 @@
"use client";
export function AuthFooter() {}

View file

@ -22,6 +22,7 @@ import { Breadcrumbs } from "@app/components/Breadcrumbs";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -58,6 +59,7 @@ export function Layout({
const pathname = usePathname(); const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin"); const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext(); const { user } = useUserContext();
const { isUnlocked } = useLicenseStatusContext();
return ( return (
<div className="flex flex-col h-screen overflow-hidden"> <div className="flex flex-col h-screen overflow-hidden">
@ -207,7 +209,9 @@ export function Layout({
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center justify-center gap-1" className="flex items-center justify-center gap-1"
> >
Open Source {!isUnlocked()
? "Community Edition"
: "Commercial Edition"}
<ExternalLink size={12} /> <ExternalLink size={12} />
</Link> </Link>
</div> </div>

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { CheckboxWithLabel } from "@app/components/ui/checkbox"; import { CheckboxWithLabel } from "@app/components/ui/checkbox";
@ -26,7 +21,6 @@ function getActionsCategories(root: boolean) {
"Update Organization": "updateOrg", "Update Organization": "updateOrg",
"Get Organization User": "getOrgUser", "Get Organization User": "getOrgUser",
"List Organization Domains": "listOrgDomains", "List Organization Domains": "listOrgDomains",
"Check Org ID": "checkOrgId",
}, },
Site: { Site: {

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";

View file

@ -55,7 +55,7 @@ export function SettingsSectionFooter({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return <div className="flex justify-end space-x-2 mt-auto pt-8">{children}</div>; return <div className="flex justify-end space-x-2 mt-auto pt-6">{children}</div>;
} }
export function SettingsSectionGrid({ export function SettingsSectionGrid({

View file

@ -193,7 +193,7 @@ export default function SupporterStatus() {
contribution allows us to commit more time to contribution allows us to commit more time to
maintain and add new features to the application for maintain and add new features to the application for
everyone. We will never use this to paywall everyone. We will never use this to paywall
features. This is separate from the Professional features. This is separate from any Commercial
Edition. Edition.
</p> </p>

View file

@ -31,7 +31,7 @@ import {
CardTitle CardTitle
} from "@app/components/ui/card"; } from "@app/components/ui/card";
interface DataTableProps<TData, TValue> { type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
data: TData[]; data: TData[];
title?: string; title?: string;
@ -39,7 +39,11 @@ interface DataTableProps<TData, TValue> {
onAdd?: () => void; onAdd?: () => void;
searchPlaceholder?: string; searchPlaceholder?: string;
searchColumn?: string; searchColumn?: string;
} defaultSort?: {
id: string;
desc: boolean;
};
};
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
columns, columns,
@ -48,9 +52,12 @@ export function DataTable<TData, TValue>({
addButtonText, addButtonText,
onAdd, onAdd,
searchPlaceholder = "Search...", searchPlaceholder = "Search...",
searchColumn = "name" searchColumn = "name",
defaultSort
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>(
defaultSort ? [defaultSort] : []
);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState<any>([]); const [globalFilter, setGlobalFilter] = useState<any>([]);

View file

@ -1,8 +1,3 @@
// 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 { GetApiKeyResponse } from "@server/routers/apiKeys";
import { createContext } from "react"; import { createContext } from "react";

View file

@ -1,8 +1,3 @@
// 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 { LicenseStatus } from "@server/license/license";
import { createContext } from "react"; import { createContext } from "react";

View file

@ -1,8 +1,3 @@
// 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 ApiKeyContext from "@app/contexts/apiKeyContext";
import { useContext } from "react"; import { useContext } from "react";

View file

@ -1,8 +1,3 @@
// 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 LicenseStatusContext from "@app/contexts/licenseStatusContext";
import { useContext } from "react"; import { useContext } from "react";

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import ApiKeyContext from "@app/contexts/apiKeyContext"; import ApiKeyContext from "@app/contexts/apiKeyContext";

View file

@ -1,8 +1,3 @@
// 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"; "use client";
import LicenseStatusContext from "@app/contexts/licenseStatusContext"; import LicenseStatusContext from "@app/contexts/licenseStatusContext";