Merge branch 'dev' into clients-pops

This commit is contained in:
miloschwartz 2025-06-19 16:34:06 -04:00
commit acf25e8ad7
No known key found for this signature in database
20 changed files with 14219 additions and 14178 deletions

105
package-lock.json generated
View file

@ -14041,111 +14041,6 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.24.4" "zod": "^3.24.4"
} }
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz",
"integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz",
"integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz",
"integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz",
"integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz",
"integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz",
"integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz",
"integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
} }
} }
} }

View file

@ -20,8 +20,9 @@ const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() { export function createApiServer() {
const apiServer = express(); const apiServer = express();
if (config.getRawConfig().server.trust_proxy) { const trustProxy = config.getRawConfig().server.trust_proxy;
apiServer.set("trust proxy", 1); if (trustProxy) {
apiServer.set("trust proxy", trustProxy);
} }
const corsConfig = config.getRawConfig().server.cors; const corsConfig = config.getRawConfig().server.cors;

View file

@ -35,8 +35,9 @@ class RedisManager {
password: redisConfig.password, password: redisConfig.password,
db: redisConfig.db, db: redisConfig.db,
tls: { tls: {
rejectUnauthorized: false rejectUnauthorized:
}, redisConfig.tls?.reject_unauthorized || false
}
}; };
return opts; return opts;
} }

View file

@ -106,7 +106,7 @@ export const configSchema = z
credentials: z.boolean().optional() credentials: z.boolean().optional()
}) })
.optional(), .optional(),
trust_proxy: z.boolean().optional().default(true), trust_proxy: z.number().int().gte(0).optional().default(1),
secret: z secret: z
.string() .string()
.optional() .optional()
@ -133,7 +133,7 @@ export const configSchema = z
db: z.number().int().nonnegative().optional().default(0), db: z.number().int().nonnegative().optional().default(0),
tls: z tls: z
.object({ .object({
rejectUnauthorized: z.boolean().optional().default(true) reject_unauthorized: z.boolean().optional().default(true)
}) })
.optional() .optional()
}) })

View file

@ -23,8 +23,8 @@ export const loginBodySchema = z
.object({ .object({
email: z email: z
.string() .string()
.email() .toLowerCase()
.transform((v) => v.toLowerCase()), .email(),
password: z.string(), password: z.string(),
code: z.string().optional() code: z.string().optional()
}) })

View file

@ -20,8 +20,8 @@ export const requestPasswordResetBody = z
.object({ .object({
email: z email: z
.string() .string()
.email() .toLowerCase()
.transform((v) => v.toLowerCase()) .email(),
}) })
.strict(); .strict();

View file

@ -21,8 +21,8 @@ export const resetPasswordBody = z
.object({ .object({
email: z email: z
.string() .string()
.email() .toLowerCase()
.transform((v) => v.toLowerCase()), .email(),
token: z.string(), // reset secret code token: z.string(), // reset secret code
newPassword: passwordSchema, newPassword: passwordSchema,
code: z.string().optional() // 2fa code code: z.string().optional() // 2fa code

View file

@ -26,8 +26,8 @@ import { UserType } from "@server/types/UserTypes";
export const signupBodySchema = z.object({ export const signupBodySchema = z.object({
email: z email: z
.string() .string()
.email() .toLowerCase()
.transform((v) => v.toLowerCase()), .email(),
password: passwordSchema, password: passwordSchema,
inviteToken: z.string().optional(), inviteToken: z.string().optional(),
inviteId: z.string().optional() inviteId: z.string().optional()

View file

@ -172,10 +172,10 @@ export async function validateOidcCallback(
const claims = arctic.decodeIdToken(idToken); const claims = arctic.decodeIdToken(idToken);
logger.debug("ID token claims", { claims }); logger.debug("ID token claims", { claims });
const userIdentifier = jmespath.search( let userIdentifier = jmespath.search(
claims, claims,
existingIdp.idpOidcConfig.identifierPath existingIdp.idpOidcConfig.identifierPath
); ) as string | null;
if (!userIdentifier) { if (!userIdentifier) {
return next( return next(
@ -186,6 +186,8 @@ export async function validateOidcCallback(
); );
} }
userIdentifier = userIdentifier.toLowerCase();
logger.debug("User identifier", { userIdentifier }); logger.debug("User identifier", { userIdentifier });
let email = null; let email = null;
@ -209,6 +211,10 @@ export async function validateOidcCallback(
logger.debug("User email", { email }); logger.debug("User email", { email });
logger.debug("User name", { name }); logger.debug("User name", { name });
if (email) {
email = email.toLowerCase();
}
const [existingUser] = await db const [existingUser] = await db
.select() .select()
.from(users) .from(users)

View file

@ -22,8 +22,8 @@ const authWithWhitelistBodySchema = z
.object({ .object({
email: z email: z
.string() .string()
.email() .toLowerCase()
.transform((v) => v.toLowerCase()), .email(),
otp: z.string().optional() otp: z.string().optional()
}) })
.strict(); .strict();

View file

@ -39,7 +39,7 @@ const createHttpResourceSchema = z
isBaseDomain: z.boolean().optional(), isBaseDomain: z.boolean().optional(),
siteId: z.number(), siteId: z.number(),
http: z.boolean(), http: z.boolean(),
protocol: z.string(), protocol: z.enum(["tcp", "udp"]),
domainId: z.string() domainId: z.string()
}) })
.strict() .strict()
@ -71,7 +71,7 @@ const createRawResourceSchema = z
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
siteId: z.number(), siteId: z.number(),
http: z.boolean(), http: z.boolean(),
protocol: z.string(), protocol: z.enum(["tcp", "udp"]),
proxyPort: z.number().int().min(1).max(65535) proxyPort: z.number().int().min(1).max(65535)
}) })
.strict() .strict()
@ -85,7 +85,7 @@ const createRawResourceSchema = z
return true; return true;
}, },
{ {
message: "Proxy port cannot be set" message: "Raw resources are not allowed"
} }
); );
@ -400,7 +400,7 @@ async function createRawResource(
resourceId: newResource[0].resourceId resourceId: newResource[0].resourceId
}); });
if (req.userOrgRoleId != adminRole[0].roleId) { if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the resource // make sure the user can access the resource
await trx.insert(userResources).values({ await trx.insert(userResources).values({
userId: req.user?.userId!, userId: req.user?.userId!,

View file

@ -21,6 +21,7 @@ const bodySchema = z
.object({ .object({
email: z email: z
.string() .string()
.toLowerCase()
.optional() .optional()
.refine((data) => { .refine((data) => {
if (data) { if (data) {
@ -28,7 +29,7 @@ const bodySchema = z
} }
return true; return true;
}), }),
username: z.string().nonempty(), username: z.string().nonempty().toLowerCase(),
name: z.string().optional(), name: z.string().optional(),
type: z.enum(["internal", "oidc"]).optional(), type: z.enum(["internal", "oidc"]).optional(),
idpId: z.number().optional(), idpId: z.number().optional(),

View file

@ -30,8 +30,8 @@ const inviteUserBodySchema = z
.object({ .object({
email: z email: z
.string() .string()
.email() .toLowerCase()
.transform((v) => v.toLowerCase()), .email(),
roleId: z.number(), roleId: z.number(),
validHours: z.number().gt(0).lte(168), validHours: z.number().gt(0).lte(168),
sendEmail: z.boolean().optional(), sendEmail: z.boolean().optional(),

View file

@ -2,14 +2,16 @@ import { migrate } from "drizzle-orm/node-postgres/migrator";
import { db } from "../db/pg"; import { db } from "../db/pg";
import semver from "semver"; import semver from "semver";
import { versionMigrations } from "../db/pg"; import { versionMigrations } from "../db/pg";
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts"; import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
import path from "path"; import path from "path";
import m1 from "./scriptsSqlite/1.6.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA // EXCEPT FOR THE DATABASE AND THE SCHEMA
// Define the migration list with versions and their corresponding functions // Define the migration list with versions and their corresponding functions
const migrations = [ const migrations = [
{ version: "1.6.0", run: m1 }
// Add new migrations here as they are created // Add new migrations here as they are created
] as { ] as {
version: string; version: string;

View file

@ -21,6 +21,7 @@ import m17 from "./scriptsSqlite/1.1.0";
import m18 from "./scriptsSqlite/1.2.0"; import m18 from "./scriptsSqlite/1.2.0";
import m19 from "./scriptsSqlite/1.3.0"; import m19 from "./scriptsSqlite/1.3.0";
import m20 from "./scriptsSqlite/1.5.0"; import m20 from "./scriptsSqlite/1.5.0";
import m21 from "./scriptsSqlite/1.6.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA // EXCEPT FOR THE DATABASE AND THE SCHEMA
@ -42,6 +43,7 @@ const migrations = [
{ version: "1.2.0", run: m18 }, { version: "1.2.0", run: m18 },
{ version: "1.3.0", run: m19 }, { version: "1.3.0", run: m19 },
{ version: "1.5.0", run: m20 }, { version: "1.5.0", run: m20 },
{ version: "1.6.0", run: m21 }
// Add new migrations here as they are created // Add new migrations here as they are created
] as const; ] as const;

View file

@ -0,0 +1,57 @@
import { db } from "@server/db/pg/driver";
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { sql } from "drizzle-orm";
import fs from "fs";
import yaml from "js-yaml";
const version = "1.6.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
try {
db.execute(sql`UPDATE 'user' SET email = LOWER(email);`);
db.execute(sql`UPDATE 'user' SET username = LOWER(username);`);
console.log(`Migrated database schema`);
} catch (e) {
console.log("Unable to make all usernames and emails lowercase");
console.log(e);
}
try {
// Determine which config file exists
const filePaths = [configFilePath1, configFilePath2];
let filePath = "";
for (const path of filePaths) {
if (fs.existsSync(path)) {
filePath = path;
break;
}
}
if (!filePath) {
throw new Error(
`No config file found (expected config.yml or config.yaml).`
);
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
rawConfig = yaml.load(fileContents);
if (rawConfig.server?.trust_proxy) {
rawConfig.server.trust_proxy = 1;
}
// Write the updated YAML back to the file
const updatedYaml = yaml.dump(rawConfig);
fs.writeFileSync(filePath, updatedYaml, "utf8");
console.log(`Set trust_proxy to 1 in config file`);
} catch (e) {
console.log(`Unable to migrate config file. Error: ${e}`);
}
console.log(`${version} migration complete`);
}

View file

@ -0,0 +1,66 @@
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
import Database from "better-sqlite3";
import fs from "fs";
import yaml from "js-yaml";
import path from "path";
const version = "1.6.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
try {
db.pragma("foreign_keys = OFF");
db.transaction(() => {
db.exec(`
UPDATE 'user' SET email = LOWER(email);
UPDATE 'user' SET username = LOWER(username);
`);
})(); // <-- executes the transaction immediately
db.pragma("foreign_keys = ON");
console.log(`Migrated database schema`);
} catch (e) {
console.log("Unable to make all usernames and emails lowercase");
console.log(e);
}
try {
// Determine which config file exists
const filePaths = [configFilePath1, configFilePath2];
let filePath = "";
for (const path of filePaths) {
if (fs.existsSync(path)) {
filePath = path;
break;
}
}
if (!filePath) {
throw new Error(
`No config file found (expected config.yml or config.yaml).`
);
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
rawConfig = yaml.load(fileContents);
if (rawConfig.server?.trust_proxy) {
rawConfig.server.trust_proxy = 1;
}
// Write the updated YAML back to the file
const updatedYaml = yaml.dump(rawConfig);
fs.writeFileSync(filePath, updatedYaml, "utf8");
console.log(`Set trust_proxy to 1 in config file`);
} catch (e) {
console.log(`Unable to migrate config file. Please do it manually. Error: ${e}`);
}
console.log(`${version} migration complete`);
}

View file

@ -42,6 +42,7 @@ export default function StepperForm() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false); const [isChecked, setIsChecked] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [orgCreated, setOrgCreated] = useState(false);
const orgSchema = z.object({ const orgSchema = z.object({
orgName: z.string().min(1, { message: t('orgNameRequired') }), orgName: z.string().min(1, { message: t('orgNameRequired') }),
@ -83,7 +84,7 @@ export default function StepperForm() {
}; };
const checkOrgIdAvailability = useCallback(async (value: string) => { const checkOrgIdAvailability = useCallback(async (value: string) => {
if (loading) { if (loading || orgCreated) {
return; return;
} }
try { try {
@ -96,7 +97,7 @@ export default function StepperForm() {
} catch (error) { } catch (error) {
setOrgIdTaken(false); setOrgIdTaken(false);
} }
}, []); }, [loading, orgCreated, api]);
const debouncedCheckOrgIdAvailability = useCallback( const debouncedCheckOrgIdAvailability = useCallback(
debounce(checkOrgIdAvailability, 300), debounce(checkOrgIdAvailability, 300),
@ -129,7 +130,7 @@ export default function StepperForm() {
}); });
if (res && res.status === 201) { if (res && res.status === 201) {
// setCurrentStep("site"); setOrgCreated(true);
router.push(`/${values.orgId}/settings/sites/create`); router.push(`/${values.orgId}/settings/sites/create`);
} }
} catch (e) { } catch (e) {

View file

@ -78,10 +78,12 @@ export function Layout({
} }
if (lightOrDark === "light") { if (lightOrDark === "light") {
return "/logo/word_mark_black.png"; // return "/logo/word_mark_black.png";
return "/logo/pangolin_orange.svg";
} }
return "/logo/word_mark_white.png"; // return "/logo/word_mark_white.png";
return "/logo/pangolin_orange.svg";
} }
setPath(getPath()); setPath(getPath());
@ -170,8 +172,8 @@ export function Layout({
<Image <Image
src={path} src={path}
alt="Pangolin Logo" alt="Pangolin Logo"
width={110} width={35}
height={25} height={35}
priority={true} priority={true}
quality={25} quality={25}
/> />

View file

@ -1,4 +1,4 @@
import { cookies } from "next/headers"; import { cookies, headers } from "next/headers";
import { pullEnv } from "../pullEnv"; import { pullEnv } from "../pullEnv";
export async function authCookieHeader() { export async function authCookieHeader() {
@ -7,9 +7,16 @@ export async function authCookieHeader() {
const allCookies = await cookies(); const allCookies = await cookies();
const cookieName = env.server.sessionCookieName; const cookieName = env.server.sessionCookieName;
const sessionId = allCookies.get(cookieName)?.value ?? null; const sessionId = allCookies.get(cookieName)?.value ?? null;
// all other headers
// this is needed to pass through x-forwarded-for, x-forwarded-proto, etc.
const otherHeaders = await headers();
const otherHeadersObject = Object.fromEntries(otherHeaders.entries());
return { return {
headers: { headers: {
Cookie: `${cookieName}=${sessionId}`, Cookie: `${cookieName}=${sessionId}`,
...otherHeadersObject
}, },
}; };
} }