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": {
"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() {
const apiServer = express();
if (config.getRawConfig().server.trust_proxy) {
apiServer.set("trust proxy", 1);
const trustProxy = config.getRawConfig().server.trust_proxy;
if (trustProxy) {
apiServer.set("trust proxy", trustProxy);
}
const corsConfig = config.getRawConfig().server.cors;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,7 +39,7 @@ const createHttpResourceSchema = z
isBaseDomain: z.boolean().optional(),
siteId: z.number(),
http: z.boolean(),
protocol: z.string(),
protocol: z.enum(["tcp", "udp"]),
domainId: z.string()
})
.strict()
@ -71,7 +71,7 @@ const createRawResourceSchema = z
name: z.string().min(1).max(255),
siteId: z.number(),
http: z.boolean(),
protocol: z.string(),
protocol: z.enum(["tcp", "udp"]),
proxyPort: z.number().int().min(1).max(65535)
})
.strict()
@ -85,7 +85,7 @@ const createRawResourceSchema = z
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
});
if (req.userOrgRoleId != adminRole[0].roleId) {
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the resource
await trx.insert(userResources).values({
userId: req.user?.userId!,

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@ import m17 from "./scriptsSqlite/1.1.0";
import m18 from "./scriptsSqlite/1.2.0";
import m19 from "./scriptsSqlite/1.3.0";
import m20 from "./scriptsSqlite/1.5.0";
import m21 from "./scriptsSqlite/1.6.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@ -42,6 +43,7 @@ const migrations = [
{ version: "1.2.0", run: m18 },
{ version: "1.3.0", run: m19 },
{ version: "1.5.0", run: m20 },
{ version: "1.6.0", run: m21 }
// Add new migrations here as they are created
] 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 [isChecked, setIsChecked] = useState(false);
const [error, setError] = useState<string | null>(null);
const [orgCreated, setOrgCreated] = useState(false);
const orgSchema = z.object({
orgName: z.string().min(1, { message: t('orgNameRequired') }),
@ -83,7 +84,7 @@ export default function StepperForm() {
};
const checkOrgIdAvailability = useCallback(async (value: string) => {
if (loading) {
if (loading || orgCreated) {
return;
}
try {
@ -96,7 +97,7 @@ export default function StepperForm() {
} catch (error) {
setOrgIdTaken(false);
}
}, []);
}, [loading, orgCreated, api]);
const debouncedCheckOrgIdAvailability = useCallback(
debounce(checkOrgIdAvailability, 300),
@ -129,7 +130,7 @@ export default function StepperForm() {
});
if (res && res.status === 201) {
// setCurrentStep("site");
setOrgCreated(true);
router.push(`/${values.orgId}/settings/sites/create`);
}
} catch (e) {

View file

@ -78,10 +78,12 @@ export function Layout({
}
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());
@ -170,8 +172,8 @@ export function Layout({
<Image
src={path}
alt="Pangolin Logo"
width={110}
height={25}
width={35}
height={35}
priority={true}
quality={25}
/>

View file

@ -1,4 +1,4 @@
import { cookies } from "next/headers";
import { cookies, headers } from "next/headers";
import { pullEnv } from "../pullEnv";
export async function authCookieHeader() {
@ -7,9 +7,16 @@ export async function authCookieHeader() {
const allCookies = await cookies();
const cookieName = env.server.sessionCookieName;
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 {
headers: {
Cookie: `${cookieName}=${sessionId}`,
...otherHeadersObject
},
};
}