mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-18 10:56:38 +00:00
Compare commits
6 Commits
1.0.0-beta
...
1.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235e91294e | ||
|
|
a36691e5ab | ||
|
|
b1d111a089 | ||
|
|
9e8086908d | ||
|
|
cf6e48be9a | ||
|
|
1df1b55e24 |
@@ -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
3
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
0
config/.gitkeep
Normal 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
0
config/db/.gitkeep
Normal file
0
config/logs/.gitkeep
Normal file
0
config/logs/.gitkeep
Normal 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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
BIN
public/logo/pangolin_orange_192x192.png
Normal file
BIN
public/logo/pangolin_orange_192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/logo/pangolin_orange_512x512.png
Normal file
BIN
public/logo/pangolin_orange_512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
public/logo/pangolin_orange_96x96.png
Normal file
BIN
public/logo/pangolin_orange_96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -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"]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(".");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
59
server/setup/scripts/1.0.0-beta2.ts
Normal file
59
server/setup/scripts/1.0.0-beta2.ts
Normal 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(".");
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user