Compare commits

...

6 Commits

Author SHA1 Message Date
Milo Schwartz
235e91294e 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 <owen@txv.io>
2025-01-07 22:41:35 -05:00
Milo Schwartz
a36691e5ab docs and logos (#7)
* add example config dir, logos, and update CONTRIBUTING.md

* update dockerignore
2025-01-06 22:43:17 -05:00
Owen Schwartz
b1d111a089 Merge pull request #2 from eltociear/patch-1
docs: update README.md
2025-01-06 11:38:56 -05:00
Owen Schwartz
9e8086908d Fix installer on arm 2025-01-06 09:58:00 -05:00
Ikko Eltociear Ashimine
cf6e48be9a docs: update README.md
Automaticlaly -> Automatically
2025-01-06 14:19:01 +09:00
Owen Schwartz
1df1b55e24 Fix docker install on debain 2025-01-05 23:23:43 -05:00
33 changed files with 254 additions and 92 deletions

View File

@@ -23,7 +23,6 @@ next-env.d.ts
.machinelogs*.json .machinelogs*.json
*-audit.json *-audit.json
package-lock.json package-lock.json
config/
install/ install/
bruno/ bruno/
LICENSE LICENSE

3
.gitignore vendored
View File

@@ -25,7 +25,8 @@ next-env.d.ts
migrations migrations
package-lock.json package-lock.json
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
config/ config/config.yml
dist dist
.dist .dist
installer installer
*.tar

View File

@@ -1,6 +1,12 @@
## Contributing ## Contributing
Contributions are welcome! Please see the following page in our documentation with future plans and feature ideas if you are looking for a place to start. Contributions are welcome!
Please see the contribution and local development guide on the docs page before getting started:
https://docs.fossorial.io/development
For ideas about what features to work on and our future plans, please see the roadmap:
https://docs.fossorial.io/roadmap https://docs.fossorial.io/roadmap
@@ -15,4 +21,4 @@ By creating this pull request, I grant the project maintainers an unlimited,
perpetual license to use, modify, and redistribute these contributions under any terms they perpetual license to use, modify, and redistribute these contributions under any terms they
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
represent that I have the right to grant this license for all contributed content. represent that I have the right to grant this license for all contributed content.
``` ```

View File

@@ -26,7 +26,7 @@ COPY --from=builder /app/.next ./.next
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/init ./dist/init COPY --from=builder /app/init ./dist/init
COPY config.example.yml ./dist/config.example.yml COPY config/config.example.yml ./dist/config.example.yml
COPY server/db/names.json ./dist/names.json COPY server/db/names.json ./dist/names.json
COPY public ./public COPY public ./public

View File

@@ -1,18 +1,20 @@
build-all:
all: build push @if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-all tag=<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: build-arm:
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest . docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
build-x86: build-x86:
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
build: build:
docker build -t fosrl/pangolin:latest . docker build -t fosrl/pangolin:latest .
push:
docker push fosrl/pangolin:latest
test: test:
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest

View File

@@ -96,7 +96,7 @@ Pangolin has a straightforward and simple dashboard UI:
3. **Connect Private Sites**: 3. **Connect Private Sites**:
- Install Newt or use another WireGuard client on private sites. - Install Newt or use another WireGuard client on private sites.
- Automaticlaly establish a connection from these sites to the central server. - Automatically establish a connection from these sites to the central server.
4. **Configure Users & Roles** 4. **Configure Users & Roles**
- Define organizations and invite users. - Define organizations and invite users.
- Implement user- or role-based permissions to control resource access. - Implement user- or role-based permissions to control resource access.
@@ -123,4 +123,7 @@ Pangolin is dual licensed under the AGPLv3 and the Fossorial Commercial license.
## Contributions ## 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.

0
config/.gitkeep Normal file
View File

View File

@@ -1,13 +1,14 @@
app: app:
base_url: https://proxy.example.com dashboard_url: http://localhost
log_level: info base_domain: localhost
log_level: debug
save_logs: false save_logs: false
server: server:
external_port: 3000 external_port: 3000
internal_port: 3001 internal_port: 3001
next_port: 3002 next_port: 3002
internal_hostname: pangolin internal_hostname: localhost
secure_cookies: false secure_cookies: false
session_cookie_name: p_session session_cookie_name: p_session
resource_session_cookie_name: p_resource_session resource_session_cookie_name: p_resource_session
@@ -16,34 +17,23 @@ traefik:
cert_resolver: letsencrypt cert_resolver: letsencrypt
http_entrypoint: web http_entrypoint: web
https_entrypoint: websecure https_entrypoint: websecure
prefer_wildcard_cert: true
gerbil: gerbil:
start_port: 51820 start_port: 51820
base_endpoint: proxy.example.com base_endpoint: localhost
use_subdomain: false
block_size: 16 block_size: 16
subnet_group: 10.0.0.0/8 subnet_group: 10.0.0.0/8
use_subdomain: true
rate_limits: rate_limits:
global: global:
window_minutes: 1 window_minutes: 1
max_requests: 100 max_requests: 100
email:
smtp_host: host.hoster.net
smtp_port: 587
smtp_user: no-reply@example.com
smtp_pass: aaaaaaaaaaaaaaaaaa
no_reply: no-reply@example.com
users: users:
server_admin: server_admin:
email: admin@example.com email: admin@example.com
password: Password123! password: Password123!
flags: flags:
require_email_verification: true require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: true

0
config/db/.gitkeep Normal file
View File

0
config/logs/.gitkeep Normal file
View File

View File

@@ -2,12 +2,9 @@ version: "3.7"
services: services:
pangolin: pangolin:
image: fosrl/pangolin:1.0.0-beta.1 image: fosrl/pangolin:latest
container_name: pangolin container_name: pangolin
restart: unless-stopped restart: unless-stopped
ports:
- 3001:3001
- 3000:3000
volumes: volumes:
- ./config:/app/config - ./config:/app/config
healthcheck: healthcheck:
@@ -17,7 +14,7 @@ services:
retries: 5 retries: 5
gerbil: gerbil:
image: fosrl/gerbil:1.0.0-beta.1 image: fosrl/gerbil:latest
container_name: gerbil container_name: gerbil
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:

View File

@@ -1,5 +1,6 @@
app: app:
base_url: https://{{.Domain}} dashboard_url: https://{{.Domain}}
base_domain: {{.Domain}}
log_level: info log_level: info
save_logs: false save_logs: false

View File

@@ -1,11 +1,8 @@
services: services:
pangolin: pangolin:
image: fosrl/pangolin:1.0.0-beta.1 image: fosrl/pangolin:latest
container_name: pangolin container_name: pangolin
restart: unless-stopped restart: unless-stopped
ports:
- 3001:3001
- 3000:3000
volumes: volumes:
- ./config:/app/config - ./config:/app/config
healthcheck: healthcheck:
@@ -15,7 +12,7 @@ services:
retries: 5 retries: 5
gerbil: gerbil:
image: fosrl/gerbil:1.0.0-beta.1 image: fosrl/gerbil:latest
container_name: gerbil container_name: gerbil
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:

View File

@@ -289,39 +289,66 @@ func installDocker() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to detect Linux distribution: %v", err) return fmt.Errorf("failed to detect Linux distribution: %v", err)
} }
osRelease := string(output) osRelease := string(output)
var installCmd *exec.Cmd
// Detect system architecture
archCmd := exec.Command("uname", "-m")
archOutput, err := archCmd.Output()
if err != nil {
return fmt.Errorf("failed to detect system architecture: %v", err)
}
arch := strings.TrimSpace(string(archOutput))
// Map architecture to Docker's architecture naming
var dockerArch string
switch arch {
case "x86_64":
dockerArch = "amd64"
case "aarch64":
dockerArch = "arm64"
default:
return fmt.Errorf("unsupported architecture: %s", arch)
}
var installCmd *exec.Cmd
switch { switch {
case strings.Contains(osRelease, "ID=ubuntu") || strings.Contains(osRelease, "ID=debian"): case strings.Contains(osRelease, "ID=ubuntu"):
installCmd = exec.Command("bash", "-c", ` installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
apt-get update && apt-get update &&
apt-get install -y apt-transport-https ca-certificates curl software-properties-common && apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
apt-get update && apt-get update &&
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
`) `, dockerArch))
case strings.Contains(osRelease, "ID=debian"):
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
apt-get update &&
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
apt-get update &&
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
`, dockerArch))
case strings.Contains(osRelease, "ID=fedora"): case strings.Contains(osRelease, "ID=fedora"):
installCmd = exec.Command("bash", "-c", ` installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
dnf -y install dnf-plugins-core && dnf -y install dnf-plugins-core &&
dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo &&
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
`) `))
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"): case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
installCmd = exec.Command("bash", "-c", ` installCmd = exec.Command("bash", "-c", `
zypper install -y docker docker-compose && zypper install -y docker docker-compose &&
systemctl enable docker systemctl enable docker
`) `)
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"): case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
installCmd = exec.Command("bash", "-c", ` installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
dnf remove -y runc && dnf remove -y runc &&
dnf -y install yum-utils && dnf -y install yum-utils &&
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo && dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin && dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
systemctl enable docker systemctl enable docker
`) `))
case strings.Contains(osRelease, "ID=amzn"): case strings.Contains(osRelease, "ID=amzn"):
installCmd = exec.Command("bash", "-c", ` installCmd = exec.Command("bash", "-c", `
yum update -y && yum update -y &&
@@ -332,7 +359,6 @@ func installDocker() error {
default: default:
return fmt.Errorf("unsupported Linux distribution") return fmt.Errorf("unsupported Linux distribution")
} }
installCmd.Stdout = os.Stdout installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr installCmd.Stderr = os.Stderr
return installCmd.Run() return installCmd.Run()

View File

@@ -1,6 +1,6 @@
{ {
"name": "@fosrl/pangolin", "name": "@fosrl/pangolin",
"version": "1.0.0-beta.1", "version": "1.0.0-beta.2",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI", "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -31,7 +31,7 @@ export function createApiServer() {
); );
} else { } else {
const corsOptions = { const corsOptions = {
origin: config.getRawConfig().app.base_url, origin: config.getRawConfig().app.dashboard_url,
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "X-CSRF-Token"] allowedHeaders: ["Content-Type", "X-CSRF-Token"]
}; };

View File

@@ -17,7 +17,7 @@ export async function sendEmailVerificationCode(
VerifyEmail({ VerifyEmail({
username: email, username: email,
verificationCode: code, verificationCode: code,
verifyLink: `${config.getRawConfig().app.base_url}/auth/verify-email` verifyLink: `${config.getRawConfig().app.dashboard_url}/auth/verify-email`
}), }),
{ {
to: email, to: email,

View File

@@ -3,18 +3,25 @@ import yaml from "js-yaml";
import path from "path"; import path from "path";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; 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 { loadAppVersion } from "@server/lib/loadAppVersion";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
const hostnameSchema = z
.string()
.regex(
/^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.[a-zA-Z]{2,})*$/,
"Invalid hostname. Must be a valid hostname like 'localhost' or 'test.example.com'."
);
const environmentSchema = z.object({ const environmentSchema = z.object({
app: z.object({ app: z.object({
base_url: z dashboard_url: z
.string() .string()
.url() .url()
.transform((url) => url.toLowerCase()), .transform((url) => url.toLowerCase()),
base_domain: hostnameSchema,
log_level: z.enum(["debug", "info", "warn", "error"]), log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.boolean() save_logs: z.boolean()
}), }),
@@ -58,7 +65,7 @@ const environmentSchema = z.object({
smtp_port: portSchema, smtp_port: portSchema,
smtp_user: z.string(), smtp_user: z.string(),
smtp_pass: z.string(), smtp_pass: z.string(),
no_reply: z.string().email(), no_reply: z.string().email()
}) })
.optional(), .optional(),
users: z.object({ 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; let environment: any;
if (fs.existsSync(configFilePath1)) { if (fs.existsSync(configFilePath1)) {
environment = loadConfig(configFilePath1); environment = loadConfig(configFilePath1);
@@ -190,15 +194,7 @@ export class Config {
} }
public getBaseDomain(): string { public getBaseDomain(): string {
const newUrl = new URL(this.rawConfig.app.base_url); return this.rawConfig.app.base_domain;
const hostname = newUrl.hostname;
const parts = hostname.split(".");
if (parts.length <= 2) {
return parts.join(".");
}
return parts.slice(1).join(".");
} }
} }

View File

@@ -6,3 +6,6 @@ export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);
export const APP_PATH = path.join("config"); 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");

View File

@@ -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( await sendEmail(
ResetPasswordCode({ ResetPasswordCode({

View File

@@ -101,7 +101,7 @@ export async function verifyResourceSession(
return allowed(res); 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) { if (!sessions) {
return notAllowed(res); return notAllowed(res);

View File

@@ -82,7 +82,6 @@ export async function createOrg(
let org: Org | null = null; let org: Org | null = null;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// create a url from config.getRawConfig().app.base_url and get the hostname
const domain = config.getBaseDomain(); const domain = config.getBaseDomain();
const newOrg = await trx const newOrg = await trx

View File

@@ -12,6 +12,34 @@ import { isIpInCidr } from "@server/lib/ip";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { addTargets } from "../newt/targets"; 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 const createTargetParamsSchema = z
.object({ .object({
resourceId: z resourceId: z
@@ -23,7 +51,7 @@ const createTargetParamsSchema = z
const createTargetSchema = z const createTargetSchema = z
.object({ .object({
ip: z.string().ip().or(z.literal('localhost')), ip: domainSchema,
method: z.string().min(1).max(10), method: z.string().min(1).max(10),
port: z.number().int().min(1).max(65535), port: z.number().int().min(1).max(65535),
protocol: z.string().optional(), protocol: z.string().optional(),

View File

@@ -11,6 +11,34 @@ import { fromError } from "zod-validation-error";
import { addPeer } from "../gerbil/peers"; import { addPeer } from "../gerbil/peers";
import { addTargets } from "../newt/targets"; 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 const updateTargetParamsSchema = z
.object({ .object({
targetId: z.string().transform(Number).pipe(z.number().int().positive()) targetId: z.string().transform(Number).pipe(z.number().int().positive())
@@ -19,7 +47,7 @@ const updateTargetParamsSchema = z
const updateTargetBodySchema = z const updateTargetBodySchema = z
.object({ .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(), method: z.string().min(1).max(10).optional(),
port: z.number().int().min(1).max(65535).optional(), port: z.number().int().min(1).max(65535).optional(),
enabled: z.boolean().optional() enabled: z.boolean().optional()

View File

@@ -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) { if (doEmail) {
await sendEmail( await sendEmail(

View File

@@ -5,7 +5,6 @@ import { eq, ne } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
export async function copyInConfig() { export async function copyInConfig() {
// create a url from config.getRawConfig().app.base_url and get the hostname
const domain = config.getBaseDomain(); const domain = config.getBaseDomain();
const endpoint = config.getRawConfig().gerbil.base_endpoint; const endpoint = config.getRawConfig().gerbil.base_endpoint;

View File

@@ -7,13 +7,15 @@ import { desc } from "drizzle-orm";
import { __DIRNAME } from "@server/lib/consts"; import { __DIRNAME } from "@server/lib/consts";
import { loadAppVersion } from "@server/lib/loadAppVersion"; import { loadAppVersion } from "@server/lib/loadAppVersion";
import m1 from "./scripts/1.0.0-beta1"; import m1 from "./scripts/1.0.0-beta1";
import m2 from "./scripts/1.0.0-beta2";
// 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.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 // Add new migrations here as they are created
] as const; ] as const;

View File

@@ -1,7 +1,5 @@
import logger from "@server/logger";
export default async function migration() { 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 // SQL operations would go here in ts format
console.log("Done..."); console.log("Done.");
} }

View File

@@ -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(".");
}

View File

@@ -63,8 +63,36 @@ import {
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; 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({ const addTargetSchema = z.object({
ip: z.union([z.string().ip(), z.literal("localhost")]), ip: domainSchema,
method: z.string(), method: z.string(),
port: z.coerce.number().int().positive() port: z.coerce.number().int().positive()
// protocol: z.string(), // protocol: z.string(),
@@ -179,7 +207,7 @@ export default function ReverseProxyTargets(props: {
// make sure that the target IP is within the site subnet // make sure that the target IP is within the site subnet
const targetIp = data.ip; const targetIp = data.ip;
const subnet = site.subnet; const subnet = site.subnet;
if (targetIp === "localhost" || !isIPInSubnet(targetIp, subnet)) { if (!isIPInSubnet(targetIp, subnet)) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Invalid target IP", title: "Invalid target IP",