From 235e91294e82044eb43257e7eb93456f1bc90ba6 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Tue, 7 Jan 2025 22:41:35 -0500 Subject: [PATCH 1/6] remove `base_url` from config (#13) * add example config dir, logos, and update CONTRIBUTING.md * update dockerignore * split base_url into dashboard_url and base_domain * Remove unessicary ports * Allow anything for the ip * Update docker tags * Complex regex for domains/ips * update gitignore --------- Co-authored-by: Owen Schwartz --- .gitignore | 1 + Makefile | 14 +++-- README.md | 5 +- config/config.example.yml | 3 +- docker-compose.example.yml | 7 +-- install/fs/config.yml | 3 +- install/fs/docker-compose.yml | 7 +-- package.json | 2 +- server/apiServer.ts | 2 +- server/auth/sendEmailVerificationCode.ts | 2 +- server/lib/config.ts | 26 ++++---- server/lib/consts.ts | 3 + server/routers/auth/requestPasswordReset.ts | 2 +- server/routers/badger/verifySession.ts | 2 +- server/routers/org/createOrg.ts | 1 - server/routers/target/createTarget.ts | 30 +++++++++- server/routers/target/updateTarget.ts | 30 +++++++++- server/routers/user/inviteUser.ts | 2 +- server/setup/copyInConfig.ts | 1 - server/setup/migrations.ts | 4 +- server/setup/scripts/1.0.0-beta1.ts | 6 +- server/setup/scripts/1.0.0-beta2.ts | 59 +++++++++++++++++++ .../[resourceId]/connectivity/page.tsx | 32 +++++++++- 23 files changed, 193 insertions(+), 51 deletions(-) create mode 100644 server/setup/scripts/1.0.0-beta2.ts diff --git a/.gitignore b/.gitignore index 9c4578e..a4ca8de 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ config/config.yml dist .dist installer +*.tar diff --git a/Makefile b/Makefile index 4b54dd5..de182bf 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,20 @@ - -all: build push +build-all: + @if [ -z "$(tag)" ]; then \ + echo "Error: tag is required. Usage: make build-all tag="; \ + exit 1; \ + fi + docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push . + docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile --push . build-arm: docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest . build-x86: - docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . + docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . build: docker build -t fosrl/pangolin:latest . -push: - docker push fosrl/pangolin:latest - test: docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest diff --git a/README.md b/README.md index 01ae93c..759f083 100644 --- a/README.md +++ b/README.md @@ -123,4 +123,7 @@ Pangolin is dual licensed under the AGPLv3 and the Fossorial Commercial license. ## Contributions -Please see [CONTRIBUTIONS](./CONTRIBUTING.md) in the repository for guidelines and best practices. +Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. + +Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository. +For all feature requests, or other ideas, please use the [Discussions](https://github.com/orgs/fosrl/discussions) section. diff --git a/config/config.example.yml b/config/config.example.yml index 0b5d171..9311514 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,5 +1,6 @@ app: - base_url: http://localhost + dashboard_url: http://localhost + base_domain: localhost log_level: debug save_logs: false diff --git a/docker-compose.example.yml b/docker-compose.example.yml index b736e94..b6184c6 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -2,12 +2,9 @@ version: "3.7" services: pangolin: - image: fosrl/pangolin:1.0.0-beta.1 + image: fosrl/pangolin:latest container_name: pangolin restart: unless-stopped - ports: - - 3001:3001 - - 3000:3000 volumes: - ./config:/app/config healthcheck: @@ -17,7 +14,7 @@ services: retries: 5 gerbil: - image: fosrl/gerbil:1.0.0-beta.1 + image: fosrl/gerbil:latest container_name: gerbil restart: unless-stopped depends_on: diff --git a/install/fs/config.yml b/install/fs/config.yml index 17c8b5e..2ad323f 100644 --- a/install/fs/config.yml +++ b/install/fs/config.yml @@ -1,5 +1,6 @@ app: - base_url: https://{{.Domain}} + dashboard_url: https://{{.Domain}} + base_domain: {{.Domain}} log_level: info save_logs: false diff --git a/install/fs/docker-compose.yml b/install/fs/docker-compose.yml index bf08aaf..47fd82f 100644 --- a/install/fs/docker-compose.yml +++ b/install/fs/docker-compose.yml @@ -1,11 +1,8 @@ services: pangolin: - image: fosrl/pangolin:1.0.0-beta.1 + image: fosrl/pangolin:latest container_name: pangolin restart: unless-stopped - ports: - - 3001:3001 - - 3000:3000 volumes: - ./config:/app/config healthcheck: @@ -15,7 +12,7 @@ services: retries: 5 gerbil: - image: fosrl/gerbil:1.0.0-beta.1 + image: fosrl/gerbil:latest container_name: gerbil restart: unless-stopped depends_on: diff --git a/package.json b/package.json index 9496afe..e05785b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fosrl/pangolin", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "private": true, "type": "module", "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI", diff --git a/server/apiServer.ts b/server/apiServer.ts index 9a1a98d..27796be 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -31,7 +31,7 @@ export function createApiServer() { ); } else { const corsOptions = { - origin: config.getRawConfig().app.base_url, + origin: config.getRawConfig().app.dashboard_url, methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], allowedHeaders: ["Content-Type", "X-CSRF-Token"] }; diff --git a/server/auth/sendEmailVerificationCode.ts b/server/auth/sendEmailVerificationCode.ts index 57523a5..5fe2b28 100644 --- a/server/auth/sendEmailVerificationCode.ts +++ b/server/auth/sendEmailVerificationCode.ts @@ -17,7 +17,7 @@ export async function sendEmailVerificationCode( VerifyEmail({ username: email, verificationCode: code, - verifyLink: `${config.getRawConfig().app.base_url}/auth/verify-email` + verifyLink: `${config.getRawConfig().app.dashboard_url}/auth/verify-email` }), { to: email, diff --git a/server/lib/config.ts b/server/lib/config.ts index 8fdc455..35540ba 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -3,18 +3,25 @@ import yaml from "js-yaml"; import path from "path"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { __DIRNAME, APP_PATH } from "@server/lib/consts"; +import { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; import { loadAppVersion } from "@server/lib/loadAppVersion"; import { passwordSchema } from "@server/auth/passwordSchema"; const portSchema = z.number().positive().gt(0).lte(65535); +const hostnameSchema = z + .string() + .regex( + /^(?!-)[a-zA-Z0-9-]{1,63}(? url.toLowerCase()), + base_domain: hostnameSchema, log_level: z.enum(["debug", "info", "warn", "error"]), save_logs: z.boolean() }), @@ -58,7 +65,7 @@ const environmentSchema = z.object({ smtp_port: portSchema, smtp_user: z.string(), smtp_pass: z.string(), - no_reply: z.string().email(), + no_reply: z.string().email() }) .optional(), users: z.object({ @@ -99,9 +106,6 @@ export class Config { } }; - const configFilePath1 = path.join(APP_PATH, "config.yml"); - const configFilePath2 = path.join(APP_PATH, "config.yaml"); - let environment: any; if (fs.existsSync(configFilePath1)) { environment = loadConfig(configFilePath1); @@ -190,15 +194,7 @@ export class Config { } public getBaseDomain(): string { - const newUrl = new URL(this.rawConfig.app.base_url); - const hostname = newUrl.hostname; - const parts = hostname.split("."); - - if (parts.length <= 2) { - return parts.join("."); - } - - return parts.slice(1).join("."); + return this.rawConfig.app.base_domain; } } diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 156b334..a444f9c 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -6,3 +6,6 @@ export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); export const APP_PATH = path.join("config"); + +export const configFilePath1 = path.join(APP_PATH, "config.yml"); +export const configFilePath2 = path.join(APP_PATH, "config.yaml"); diff --git a/server/routers/auth/requestPasswordReset.ts b/server/routers/auth/requestPasswordReset.ts index e3d1de3..a223e5f 100644 --- a/server/routers/auth/requestPasswordReset.ts +++ b/server/routers/auth/requestPasswordReset.ts @@ -82,7 +82,7 @@ export async function requestPasswordReset( }); }); - const url = `${config.getRawConfig().app.base_url}/auth/reset-password?email=${email}&token=${token}`; + const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`; await sendEmail( ResetPasswordCode({ diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 756fd04..459219c 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -101,7 +101,7 @@ export async function verifyResourceSession( return allowed(res); } - const redirectUrl = `${config.getRawConfig().app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`; + const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`; if (!sessions) { return notAllowed(res); diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index b86dfd1..3c25c0c 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -82,7 +82,6 @@ export async function createOrg( let org: Org | null = null; await db.transaction(async (trx) => { - // create a url from config.getRawConfig().app.base_url and get the hostname const domain = config.getBaseDomain(); const newOrg = await trx diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 742cdf6..e7ae3ac 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -12,6 +12,34 @@ import { isIpInCidr } from "@server/lib/ip"; import { fromError } from "zod-validation-error"; import { addTargets } from "../newt/targets"; +// Regular expressions for validation +const DOMAIN_REGEX = + /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +const IPV4_REGEX = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; +const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; + +// Schema for domain names and IP addresses +const domainSchema = z + .string() + .min(1, "Domain cannot be empty") + .max(255, "Domain name too long") + .refine( + (value) => { + // Check if it's a valid IP address (v4 or v6) + if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { + return true; + } + + // Check if it's a valid domain name + return DOMAIN_REGEX.test(value); + }, + { + message: "Invalid domain name or IP address format", + path: ["domain"] + } + ); + const createTargetParamsSchema = z .object({ resourceId: z @@ -23,7 +51,7 @@ const createTargetParamsSchema = z const createTargetSchema = z .object({ - ip: z.string().ip().or(z.literal('localhost')), + ip: domainSchema, method: z.string().min(1).max(10), port: z.number().int().min(1).max(65535), protocol: z.string().optional(), diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 3e28802..77f127e 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -11,6 +11,34 @@ import { fromError } from "zod-validation-error"; import { addPeer } from "../gerbil/peers"; import { addTargets } from "../newt/targets"; +// Regular expressions for validation +const DOMAIN_REGEX = + /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +const IPV4_REGEX = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; +const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; + +// Schema for domain names and IP addresses +const domainSchema = z + .string() + .min(1, "Domain cannot be empty") + .max(255, "Domain name too long") + .refine( + (value) => { + // Check if it's a valid IP address (v4 or v6) + if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { + return true; + } + + // Check if it's a valid domain name + return DOMAIN_REGEX.test(value); + }, + { + message: "Invalid domain name or IP address format", + path: ["domain"] + } + ); + const updateTargetParamsSchema = z .object({ targetId: z.string().transform(Number).pipe(z.number().int().positive()) @@ -19,7 +47,7 @@ const updateTargetParamsSchema = z const updateTargetBodySchema = z .object({ - ip: z.string().ip().or(z.literal('localhost')).optional(), // for now we cant update the ip; you will have to delete + ip: domainSchema.optional(), method: z.string().min(1).max(10).optional(), port: z.number().int().min(1).max(65535).optional(), enabled: z.boolean().optional() diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 7b77149..3031e39 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -152,7 +152,7 @@ export async function inviteUser( }); }); - const inviteLink = `${config.getRawConfig().app.base_url}/invite?token=${inviteId}-${token}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; if (doEmail) { await sendEmail( diff --git a/server/setup/copyInConfig.ts b/server/setup/copyInConfig.ts index c3ca161..0ff3ba7 100644 --- a/server/setup/copyInConfig.ts +++ b/server/setup/copyInConfig.ts @@ -5,7 +5,6 @@ import { eq, ne } from "drizzle-orm"; import logger from "@server/logger"; export async function copyInConfig() { - // create a url from config.getRawConfig().app.base_url and get the hostname const domain = config.getBaseDomain(); const endpoint = config.getRawConfig().gerbil.base_endpoint; diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts index 5483b2a..7b1ad8c 100644 --- a/server/setup/migrations.ts +++ b/server/setup/migrations.ts @@ -7,13 +7,15 @@ import { desc } from "drizzle-orm"; import { __DIRNAME } from "@server/lib/consts"; import { loadAppVersion } from "@server/lib/loadAppVersion"; import m1 from "./scripts/1.0.0-beta1"; +import m2 from "./scripts/1.0.0-beta2"; // 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.0.0-beta.1", run: m1 } + { version: "1.0.0-beta.1", run: m1 }, + { version: "1.0.0-beta.2", run: m2 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scripts/1.0.0-beta1.ts b/server/setup/scripts/1.0.0-beta1.ts index 1a56483..65d9ad1 100644 --- a/server/setup/scripts/1.0.0-beta1.ts +++ b/server/setup/scripts/1.0.0-beta1.ts @@ -1,7 +1,5 @@ -import logger from "@server/logger"; - export default async function migration() { - console.log("Running setup script 1.0.0-beta.1"); + console.log("Running setup script 1.0.0-beta.1..."); // SQL operations would go here in ts format - console.log("Done..."); + console.log("Done."); } diff --git a/server/setup/scripts/1.0.0-beta2.ts b/server/setup/scripts/1.0.0-beta2.ts new file mode 100644 index 0000000..f8aa9bc --- /dev/null +++ b/server/setup/scripts/1.0.0-beta2.ts @@ -0,0 +1,59 @@ +import { configFilePath1, configFilePath2 } from "@server/lib/consts"; +import fs from "fs"; +import yaml from "js-yaml"; + +export default async function migration() { + console.log("Running setup script 1.0.0-beta.2..."); + + // 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); + + // Validate the structure + if (!rawConfig.app || !rawConfig.app.base_url) { + throw new Error(`Invalid config file: app.base_url is missing.`); + } + + // Move base_url to dashboard_url and calculate base_domain + const baseUrl = rawConfig.app.base_url; + rawConfig.app.dashboard_url = baseUrl; + rawConfig.app.base_domain = getBaseDomain(baseUrl); + + // Remove the old base_url + delete rawConfig.app.base_url; + + // Write the updated YAML back to the file + const updatedYaml = yaml.dump(rawConfig); + fs.writeFileSync(filePath, updatedYaml, "utf8"); + + console.log("Done."); +} + +function getBaseDomain(url: string): string { + const newUrl = new URL(url); + const hostname = newUrl.hostname; + const parts = hostname.split("."); + + if (parts.length <= 2) { + return parts.join("."); + } + + return parts.slice(-2).join("."); +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index 9b85883..a6d8821 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -63,8 +63,36 @@ import { } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; +// Regular expressions for validation +const DOMAIN_REGEX = + /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +const IPV4_REGEX = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; +const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; + +// Schema for domain names and IP addresses +const domainSchema = z + .string() + .min(1, "Domain cannot be empty") + .max(255, "Domain name too long") + .refine( + (value) => { + // Check if it's a valid IP address (v4 or v6) + if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { + return true; + } + + // Check if it's a valid domain name + return DOMAIN_REGEX.test(value); + }, + { + message: "Invalid domain name or IP address format", + path: ["domain"] + } + ); + const addTargetSchema = z.object({ - ip: z.union([z.string().ip(), z.literal("localhost")]), + ip: domainSchema, method: z.string(), port: z.coerce.number().int().positive() // protocol: z.string(), @@ -179,7 +207,7 @@ export default function ReverseProxyTargets(props: { // make sure that the target IP is within the site subnet const targetIp = data.ip; const subnet = site.subnet; - if (targetIp === "localhost" || !isIPInSubnet(targetIp, subnet)) { + if (!isIPInSubnet(targetIp, subnet)) { toast({ variant: "destructive", title: "Invalid target IP", From 4421f470a4ca7617b5f594af7acb7389708b7ca7 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Wed, 8 Jan 2025 21:45:37 -0500 Subject: [PATCH 2/6] add security policy --- SECURITY.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8962e8e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us: + +1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk. +2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include: + +- Description and location of the vulnerability. +- Potential impact of the vulnerability. +- Steps to reproduce the vulnerability. +- Potential solutions to fix the vulnerability. +- Your name/handle and a link for recognition (optional). + +We aim to address the issue as soon as possible. From a556339b7667e7aa43234c4ee884ad8021c3a6b6 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Wed, 8 Jan 2025 23:13:35 -0500 Subject: [PATCH 3/6] allow hyphens in base_domain regex --- server/lib/config.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/server/lib/config.ts b/server/lib/config.ts index 35540ba..6642e7d 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -3,7 +3,11 @@ import yaml from "js-yaml"; import path from "path"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; +import { + __DIRNAME, + configFilePath1, + configFilePath2 +} from "@server/lib/consts"; import { loadAppVersion } from "@server/lib/loadAppVersion"; import { passwordSchema } from "@server/auth/passwordSchema"; @@ -11,9 +15,9 @@ const portSchema = z.number().positive().gt(0).lte(65535); const hostnameSchema = z .string() .regex( - /^(?!-)[a-zA-Z0-9-]{1,63}(? Date: Thu, 9 Jan 2025 23:21:57 -0500 Subject: [PATCH 4/6] verify redirects are safe before redirecting --- server/routers/badger/verifySession.ts | 3 ++- src/app/[orgId]/layout.tsx | 2 +- src/app/[orgId]/settings/general/layout.tsx | 2 +- src/app/[orgId]/settings/layout.tsx | 2 +- src/app/auth/login/DashboardLoginForm.tsx | 8 ++++---- src/app/auth/login/page.tsx | 12 +++++++++--- .../auth/reset-password/ResetPasswordForm.tsx | 7 +++---- src/app/auth/reset-password/page.tsx | 8 +++++++- .../[resourceId]/ResourceAuthPortal.tsx | 6 +----- src/app/auth/resource/[resourceId]/page.tsx | 12 +++++++++++- src/app/auth/signup/SignupForm.tsx | 11 ++++++----- src/app/auth/signup/page.tsx | 12 +++++++++--- src/app/auth/verify-email/VerifyEmailForm.tsx | 7 +++---- src/app/auth/verify-email/page.tsx | 8 +++++++- src/app/layout.tsx | 17 ++++++++++------- src/app/page.tsx | 7 +++++-- src/components/LoginForm.tsx | 2 +- src/lib/cleanRedirect.ts | 18 ++++++++++++++++++ 18 files changed, 99 insertions(+), 45 deletions(-) create mode 100644 src/lib/cleanRedirect.ts diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 459219c..c369aef 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -101,7 +101,8 @@ export async function verifyResourceSession( return allowed(res); } - const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`; + // const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`; + const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(resource.resourceId)}`; if (!sessions) { return notAllowed(res); diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index b8b5c6a..fa41beb 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -25,7 +25,7 @@ export default async function OrgLayout(props: { const user = await getUser(); if (!user) { - redirect(`/?redirect=/${orgId}`); + redirect(`/`); } try { diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 5923118..4b41b8c 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({ const user = await getUser(); if (!user) { - redirect(`/?redirect=/${orgId}/settings/general`); + redirect(`/`); } let orgUser = null; diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index 95a6cc0..b0b561a 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -61,7 +61,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const user = await getUser(); if (!user) { - redirect(`/?redirect=/${params.orgId}/`); + redirect(`/`); } const cookie = await authCookieHeader(); diff --git a/src/app/auth/login/DashboardLoginForm.tsx b/src/app/auth/login/DashboardLoginForm.tsx index 088fc63..715a0fb 100644 --- a/src/app/auth/login/DashboardLoginForm.tsx +++ b/src/app/auth/login/DashboardLoginForm.tsx @@ -13,6 +13,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import Image from "next/image"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; type DashboardLoginFormProps = { redirect?: string; @@ -57,10 +58,9 @@ export default function DashboardLoginForm({ { - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } else if (redirect) { - router.push(redirect); + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/"); } diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 87c2707..118cfcd 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -5,6 +5,7 @@ import { cache } from "react"; import DashboardLoginForm from "./DashboardLoginForm"; import { Mail } from "lucide-react"; import { pullEnv } from "@app/lib/pullEnv"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; export const dynamic = "force-dynamic"; @@ -25,6 +26,11 @@ export default async function Page(props: { redirect("/"); } + let redirectUrl: string | undefined = undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect as string); + } + return ( <> {isInvite && ( @@ -42,16 +48,16 @@ export default async function Page(props: { )} - + {(!signUpDisabled || isInvite) && (

Don't have an account?{" "} diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index a9232d4..ae99781 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -43,6 +43,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; import { passwordSchema } from "@server/auth/passwordSchema"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; const requestSchema = z.object({ email: z.string().email() @@ -186,11 +187,9 @@ export default function ResetPasswordForm({ setSuccessMessage("Password reset successfully! Back to login..."); setTimeout(() => { - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } if (redirect) { - router.push(redirect); + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/login"); } diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx index b5636c4..73654be 100644 --- a/src/app/auth/reset-password/page.tsx +++ b/src/app/auth/reset-password/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { cache } from "react"; import ResetPasswordForm from "./ResetPasswordForm"; import Link from "next/link"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; export const dynamic = "force-dynamic"; @@ -21,6 +22,11 @@ export default async function Page(props: { redirect("/"); } + let redirectUrl: string | undefined = undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect); + } + return ( <> diff --git a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx index a25edf7..c23403a 100644 --- a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx @@ -481,11 +481,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { className={`${numMethods <= 1 ? "mt-0" : ""}`} > await handleSSOAuth() } diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 886801c..4258f68 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -55,7 +55,17 @@ export default async function ResourceAuthPage(props: { ); } - const redirectUrl = searchParams.redirect || authInfo.url; + let redirectUrl = authInfo.url; + // if (searchParams.redirect) { + // try { + // const serverResourceHost = new URL(authInfo.url).host; + // const redirectHost = new URL(searchParams.redirect).host; + // + // if (serverResourceHost === redirectHost) { + // redirectUrl = searchParams.redirect; + // } + // } catch (e) {} + // } const hasAuth = authInfo.password || diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index 9630d90..f839284 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -30,6 +30,7 @@ import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import Image from "next/image"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; type SignupFormProps = { redirect?: string; @@ -92,17 +93,17 @@ export default function SignupForm({ if (res.data?.data?.emailVerificationRequired) { if (redirect) { - router.push(`/auth/verify-email?redirect=${redirect}`); + const safe = cleanRedirect(redirect); + router.push(`/auth/verify-email?redirect=${safe}`); } else { router.push("/auth/verify-email"); } return; } - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } else if (redirect) { - router.push(redirect); + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/"); } diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index f53ff2c..361cc0d 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -1,5 +1,6 @@ import SignupForm from "@app/app/auth/signup/SignupForm"; import { verifySession } from "@app/lib/auth/verifySession"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; import { Mail } from "lucide-react"; import Link from "next/link"; @@ -41,6 +42,11 @@ export default async function Page(props: { } } + let redirectUrl: string | undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect); + } + return ( <> {isInvite && ( @@ -59,7 +65,7 @@ export default async function Page(props: { )} @@ -68,9 +74,9 @@ export default async function Page(props: { Already have an account?{" "} diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx index 7a6bc08..8a0ca89 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -36,6 +36,7 @@ import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/api";; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; const FormSchema = z.object({ email: z.string().email({ message: "Invalid email address" }), @@ -91,11 +92,9 @@ export default function VerifyEmailForm({ "Email successfully verified! Redirecting you..." ); setTimeout(() => { - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } if (redirect) { - router.push(redirect); + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/"); } diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx index 3452df6..033fa75 100644 --- a/src/app/auth/verify-email/page.tsx +++ b/src/app/auth/verify-email/page.tsx @@ -1,5 +1,6 @@ import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm"; import { verifySession } from "@app/lib/auth/verifySession"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; import { redirect } from "next/navigation"; import { cache } from "react"; @@ -27,11 +28,16 @@ export default async function Page(props: { redirect("/"); } + let redirectUrl: string | undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect as string); + } + return ( <> ); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 16ce996..b4abbad 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,8 @@ import { ThemeProvider } from "@app/providers/ThemeProvider"; import EnvProvider from "@app/providers/EnvProvider"; import { Separator } from "@app/components/ui/separator"; import { pullEnv } from "@app/lib/pullEnv"; +import { BookOpenText } from "lucide-react"; +import Image from "next/image"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, @@ -38,10 +40,10 @@ export default async function RootLayout({

{children}
{/* Footer */} -