diff --git a/config/config.example.yml b/config/config.example.yml
index 827a2c49..69a0e06e 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -12,6 +12,7 @@ server:
secure_cookies: false
session_cookie_name: p_session
resource_session_cookie_name: p_resource_session
+ resource_access_token_param: p_token
traefik:
cert_resolver: letsencrypt
diff --git a/install/fs/config.yml b/install/fs/config.yml
index 21a8c0ff..985b8b62 100644
--- a/install/fs/config.yml
+++ b/install/fs/config.yml
@@ -9,9 +9,10 @@ server:
internal_port: 3001
next_port: 3002
internal_hostname: pangolin
- secure_cookies: false
+ secure_cookies: true
session_cookie_name: p_session
resource_session_cookie_name: p_resource_session
+ resource_access_token_param: p_token
traefik:
cert_resolver: letsencrypt
diff --git a/install/fs/docker-compose.yml b/install/fs/docker-compose.yml
index 47fd82f8..ab6528d0 100644
--- a/install/fs/docker-compose.yml
+++ b/install/fs/docker-compose.yml
@@ -1,6 +1,6 @@
services:
pangolin:
- image: fosrl/pangolin:latest
+ image: fosrl/pangolin:{{.PangolinVersion}}
container_name: pangolin
restart: unless-stopped
volumes:
@@ -11,8 +11,9 @@ services:
timeout: "3s"
retries: 5
+{{if .InstallGerbil}}
gerbil:
- image: fosrl/gerbil:latest
+ image: fosrl/gerbil:{{.GerbilVersion}}
container_name: gerbil
restart: unless-stopped
depends_on:
@@ -32,12 +33,20 @@ services:
- 51820:51820/udp
- 443:443 # Port for traefik because of the network_mode
- 80:80 # Port for traefik because of the network_mode
+{{end}}
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
+{{if .InstallGerbil}}
network_mode: service:gerbil # Ports appear on the gerbil service
+{{end}}
+{{if not .InstallGerbil}}
+ ports:
+ - 443:443
+ - 80:80
+{{end}}
depends_on:
pangolin:
condition: service_healthy
diff --git a/install/fs/traefik/traefik_config.yml b/install/fs/traefik/traefik_config.yml
index c83cc8c4..de104a2f 100644
--- a/install/fs/traefik/traefik_config.yml
+++ b/install/fs/traefik/traefik_config.yml
@@ -13,7 +13,7 @@ experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
- version: "v1.0.0-beta.1"
+ version: "v1.0.0-beta.2"
log:
level: "INFO"
diff --git a/install/go.mod b/install/go.mod
index 3de61fa9..85cf49e4 100644
--- a/install/go.mod
+++ b/install/go.mod
@@ -1,3 +1,8 @@
module installer
-go 1.23.0
\ No newline at end of file
+go 1.23.0
+
+require (
+ golang.org/x/sys v0.29.0 // indirect
+ golang.org/x/term v0.28.0 // indirect
+)
diff --git a/install/go.sum b/install/go.sum
index e69de29b..f05f63b4 100644
--- a/install/go.sum
+++ b/install/go.sum
@@ -0,0 +1,4 @@
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
diff --git a/install/main.go b/install/main.go
index 480ff934..ae598033 100644
--- a/install/main.go
+++ b/install/main.go
@@ -10,27 +10,38 @@ import (
"path/filepath"
"runtime"
"strings"
+ "syscall"
"text/template"
"unicode"
+
+ "golang.org/x/term"
)
+func loadVersions(config *Config) {
+ config.PangolinVersion = "1.0.0-beta.5"
+ config.GerbilVersion = "1.0.0-beta.1"
+}
+
//go:embed fs/*
var configFiles embed.FS
type Config struct {
- BaseDomain string `yaml:"baseDomain"`
- DashboardDomain string `yaml:"dashboardUrl"`
- LetsEncryptEmail string `yaml:"letsEncryptEmail"`
- AdminUserEmail string `yaml:"adminUserEmail"`
- AdminUserPassword string `yaml:"adminUserPassword"`
- DisableSignupWithoutInvite bool `yaml:"disableSignupWithoutInvite"`
- DisableUserCreateOrg bool `yaml:"disableUserCreateOrg"`
- EnableEmail bool `yaml:"enableEmail"`
- EmailSMTPHost string `yaml:"emailSMTPHost"`
- EmailSMTPPort int `yaml:"emailSMTPPort"`
- EmailSMTPUser string `yaml:"emailSMTPUser"`
- EmailSMTPPass string `yaml:"emailSMTPPass"`
- EmailNoReply string `yaml:"emailNoReply"`
+ PangolinVersion string
+ GerbilVersion string
+ BaseDomain string
+ DashboardDomain string
+ LetsEncryptEmail string
+ AdminUserEmail string
+ AdminUserPassword string
+ DisableSignupWithoutInvite bool
+ DisableUserCreateOrg bool
+ EnableEmail bool
+ EmailSMTPHost string
+ EmailSMTPPort int
+ EmailSMTPUser string
+ EmailSMTPPass string
+ EmailNoReply string
+ InstallGerbil bool
}
func main() {
@@ -45,13 +56,16 @@ func main() {
// check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil {
config := collectUserInput(reader)
+
+ loadVersions(&config)
+
if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1)
}
if !isDockerInstalled() && runtime.GOOS == "linux" {
- if shouldInstallDocker() {
+ if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
installDocker()
}
}
@@ -82,6 +96,24 @@ func readString(reader *bufio.Reader, prompt string, defaultValue string) string
return input
}
+func readPassword(prompt string) string {
+ fmt.Print(prompt + ": ")
+
+ // Read password without echo
+ password, err := term.ReadPassword(int(syscall.Stdin))
+ fmt.Println() // Add a newline since ReadPassword doesn't add one
+
+ if err != nil {
+ return ""
+ }
+
+ input := strings.TrimSpace(string(password))
+ if input == "" {
+ return readPassword(prompt)
+ }
+ return input
+}
+
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
defaultStr := "no"
if defaultValue {
@@ -109,21 +141,29 @@ func collectUserInput(reader *bufio.Reader) Config {
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
+ config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunned connections", true)
// Admin user configuration
fmt.Println("\n=== Admin User Configuration ===")
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
for {
- config.AdminUserPassword = readString(reader, "Enter admin user password", "")
- if valid, message := validatePassword(config.AdminUserPassword); valid {
- break
+ pass1 := readPassword("Create admin user password")
+ pass2 := readPassword("Confirm admin user password")
+
+ if pass1 != pass2 {
+ fmt.Println("Passwords do not match")
} else {
- fmt.Println("Invalid password:", message)
- fmt.Println("Password requirements:")
- fmt.Println("- At least one uppercase English letter")
- fmt.Println("- At least one lowercase English letter")
- fmt.Println("- At least one digit")
- fmt.Println("- At least one special character")
+ config.AdminUserPassword = pass1
+ if valid, message := validatePassword(config.AdminUserPassword); valid {
+ break
+ } else {
+ fmt.Println("Invalid password:", message)
+ fmt.Println("Password requirements:")
+ fmt.Println("- At least one uppercase English letter")
+ fmt.Println("- At least one lowercase English letter")
+ fmt.Println("- At least one digit")
+ fmt.Println("- At least one special character")
+ }
}
}
@@ -302,13 +342,6 @@ func createConfigFiles(config Config) error {
return nil
}
-func shouldInstallDocker() bool {
- reader := bufio.NewReader(os.Stdin)
- fmt.Print("Would you like to install Docker? (yes/no): ")
- response, _ := reader.ReadString('\n')
- return strings.ToLower(strings.TrimSpace(response)) == "yes"
-}
-
func installDocker() error {
// Detect Linux distribution
cmd := exec.Command("cat", "/etc/os-release")
diff --git a/package.json b/package.json
index 14e87d68..5b1b25b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@fosrl/pangolin",
- "version": "1.0.0-beta.4",
+ "version": "1.0.0-beta.5",
"private": true,
"type": "module",
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
@@ -26,6 +26,7 @@
"@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.2",
"@radix-ui/react-checkbox": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.4",
"@radix-ui/react-icons": "1.3.2",
diff --git a/server/auth/canUserAccessResource.ts b/server/auth/canUserAccessResource.ts
new file mode 100644
index 00000000..bdafaa0d
--- /dev/null
+++ b/server/auth/canUserAccessResource.ts
@@ -0,0 +1,45 @@
+import db from "@server/db";
+import { and, eq } from "drizzle-orm";
+import { roleResources, userResources } from "@server/db/schema";
+
+export async function canUserAccessResource({
+ userId,
+ resourceId,
+ roleId
+}: {
+ userId: string;
+ resourceId: number;
+ roleId: number;
+}): Promise+ This link does not + require visiting in a + browser to complete the + redirect. It contains + the access token + directly in the URL, + which can be useful for + sharing with clients + that do not support + redirects. +
+