mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-28 05:44:01 +02:00
Move docker podman question and add hybird question
Allow empty config Continue to adjust config for hybrid
This commit is contained in:
parent
2907f22200
commit
907dab7d05
9 changed files with 207 additions and 116 deletions
231
install/main.go
231
install/main.go
|
@ -52,6 +52,7 @@ type Config struct {
|
||||||
TraefikBouncerKey string
|
TraefikBouncerKey string
|
||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
Secret string
|
Secret string
|
||||||
|
HybridMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
|
@ -70,9 +71,6 @@ func main() {
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("Please make sure you have the following prerequisites:")
|
fmt.Println("Please make sure you have the following prerequisites:")
|
||||||
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
||||||
fmt.Println("- Point your domain to the VPS IP with A records.")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("https://docs.digpangolin.com/self-host/dns-and-networking")
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("Lets get started!")
|
fmt.Println("Lets get started!")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
|
@ -89,71 +87,8 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
|
||||||
|
|
||||||
chosenContainer := Docker
|
|
||||||
if strings.EqualFold(inputContainer, "docker") {
|
|
||||||
chosenContainer = Docker
|
|
||||||
} else if strings.EqualFold(inputContainer, "podman") {
|
|
||||||
chosenContainer = Podman
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if chosenContainer == Podman {
|
|
||||||
if !isPodmanInstalled() {
|
|
||||||
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
|
||||||
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
|
||||||
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
|
||||||
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
|
||||||
if approved {
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
fmt.Println("You need to run the installer as root for such a configuration.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Podman containers are not able to listen on privileged ports. The official recommendation is to
|
|
||||||
// container low-range ports as unprivileged ports.
|
|
||||||
// Linux only.
|
|
||||||
|
|
||||||
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
|
||||||
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Unprivileged ports have been configured.")
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if chosenContainer == Docker {
|
|
||||||
// check if docker is not installed and the user is root
|
|
||||||
if !isDockerInstalled() {
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the user is in the docker group (linux only)
|
|
||||||
if !isUserInDockerGroup() {
|
|
||||||
fmt.Println("You are not in the docker group.")
|
|
||||||
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This shouldn't happen unless there's a third container runtime.
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
config.InstallationContainerType = chosenContainer
|
|
||||||
|
|
||||||
// check if there is already a config file
|
// check if there is already a config file
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
|
@ -170,7 +105,9 @@ func main() {
|
||||||
|
|
||||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" && chosenContainer == Docker {
|
config.InstallationContainerType = podmanOrDocker(reader)
|
||||||
|
|
||||||
|
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
installDocker()
|
||||||
// try to start docker service but ignore errors
|
// try to start docker service but ignore errors
|
||||||
|
@ -199,15 +136,15 @@ func main() {
|
||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if (isDockerInstalled() && chosenContainer == Docker) ||
|
if (isDockerInstalled() && config.InstallationContainerType == Docker) ||
|
||||||
(isPodmanInstalled() && chosenContainer == Podman) {
|
(isPodmanInstalled() && config.InstallationContainerType == Podman) {
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
if err := pullContainers(chosenContainer); err != nil {
|
if err := pullContainers(config.InstallationContainerType); err != nil {
|
||||||
fmt.Println("Error: ", err)
|
fmt.Println("Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := startContainers(chosenContainer); err != nil {
|
if err := startContainers(config.InstallationContainerType); err != nil {
|
||||||
fmt.Println("Error: ", err)
|
fmt.Println("Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -288,22 +225,89 @@ func main() {
|
||||||
|
|
||||||
// Check if containers were started during this installation
|
// Check if containers were started during this installation
|
||||||
containersStarted := false
|
containersStarted := false
|
||||||
if (isDockerInstalled() && chosenContainer == Docker) ||
|
if (isDockerInstalled() && config.InstallationContainerType == Docker) ||
|
||||||
(isPodmanInstalled() && chosenContainer == Podman) {
|
(isPodmanInstalled() && config.InstallationContainerType == Podman) {
|
||||||
// Try to fetch and display the token if containers are running
|
// Try to fetch and display the token if containers are running
|
||||||
containersStarted = true
|
containersStarted = true
|
||||||
printSetupToken(chosenContainer, config.DashboardDomain)
|
printSetupToken(config.InstallationContainerType, config.DashboardDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If containers weren't started or token wasn't found, show instructions
|
// If containers weren't started or token wasn't found, show instructions
|
||||||
if !containersStarted {
|
if !containersStarted {
|
||||||
showSetupTokenInstructions(chosenContainer, config.DashboardDomain)
|
showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Installation complete!")
|
fmt.Println("Installation complete!")
|
||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||||
|
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||||
|
|
||||||
|
chosenContainer := Docker
|
||||||
|
if strings.EqualFold(inputContainer, "docker") {
|
||||||
|
chosenContainer = Docker
|
||||||
|
} else if strings.EqualFold(inputContainer, "podman") {
|
||||||
|
chosenContainer = Podman
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosenContainer == Podman {
|
||||||
|
if !isPodmanInstalled() {
|
||||||
|
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
||||||
|
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
||||||
|
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
||||||
|
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
||||||
|
if approved {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Podman containers are not able to listen on privileged ports. The official recommendation is to
|
||||||
|
// container low-range ports as unprivileged ports.
|
||||||
|
// Linux only.
|
||||||
|
|
||||||
|
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
||||||
|
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Unprivileged ports have been configured.")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if chosenContainer == Docker {
|
||||||
|
// check if docker is not installed and the user is root
|
||||||
|
if !isDockerInstalled() {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user is in the docker group (linux only)
|
||||||
|
if !isUserInDockerGroup() {
|
||||||
|
fmt.Println("You are not in the docker group.")
|
||||||
|
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This shouldn't happen unless there's a third container runtime.
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chosenContainer
|
||||||
|
}
|
||||||
|
|
||||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||||
if defaultValue != "" {
|
if defaultValue != "" {
|
||||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
||||||
|
@ -318,6 +322,12 @@ func readString(reader *bufio.Reader, prompt string, defaultValue string) string
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
||||||
|
fmt.Print(prompt + ": ")
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
return strings.TrimSpace(input)
|
||||||
|
}
|
||||||
|
|
||||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
func readPassword(prompt string, reader *bufio.Reader) string {
|
||||||
if term.IsTerminal(int(syscall.Stdin)) {
|
if term.IsTerminal(int(syscall.Stdin)) {
|
||||||
fmt.Print(prompt + ": ")
|
fmt.Print(prompt + ": ")
|
||||||
|
@ -347,6 +357,11 @@ func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
||||||
return strings.ToLower(input) == "yes"
|
return strings.ToLower(input) == "yes"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
||||||
|
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
||||||
|
return strings.ToLower(input) == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
||||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
||||||
if input == "" {
|
if input == "" {
|
||||||
|
@ -362,42 +377,50 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.HybridMode = readBoolNoDefault(reader, "Do you want to use hybrid mode?")
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
if !config.HybridMode {
|
||||||
defaultDashboardDomain := ""
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
if config.BaseDomain != "" {
|
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
// Set default dashboard domain after base domain is collected
|
||||||
|
defaultDashboardDomain := ""
|
||||||
|
if config.BaseDomain != "" {
|
||||||
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
|
}
|
||||||
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
}
|
}
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||||
|
|
||||||
// Email configuration
|
if !config.HybridMode {
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
// Email configuration
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
|
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
if config.EnableEmail {
|
if config.EnableEmail {
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if config.BaseDomain == "" {
|
// Validate required fields
|
||||||
fmt.Println("Error: Domain name is required")
|
if config.BaseDomain == "" {
|
||||||
os.Exit(1)
|
fmt.Println("Error: Domain name is required")
|
||||||
}
|
os.Exit(1)
|
||||||
if config.DashboardDomain == "" {
|
}
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
if config.DashboardDomain == "" {
|
||||||
os.Exit(1)
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
}
|
os.Exit(1)
|
||||||
if config.LetsEncryptEmail == "" {
|
}
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
if config.LetsEncryptEmail == "" {
|
||||||
os.Exit(1)
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
|
@ -24,8 +24,8 @@ export const SESSION_COOKIE_EXPIRES =
|
||||||
60 *
|
60 *
|
||||||
60 *
|
60 *
|
||||||
config.getRawConfig().server.dashboard_session_length_hours;
|
config.getRawConfig().server.dashboard_session_length_hours;
|
||||||
export const COOKIE_DOMAIN =
|
export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url ?
|
||||||
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
"." + new URL(config.getRawConfig().app.dashboard_url!).hostname : undefined;
|
||||||
|
|
||||||
export function generateSessionToken(): string {
|
export function generateSessionToken(): string {
|
||||||
const bytes = new Uint8Array(20);
|
const bytes = new Uint8Array(20);
|
||||||
|
|
|
@ -6,6 +6,11 @@ import logger from "@server/logger";
|
||||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
function createEmailClient() {
|
function createEmailClient() {
|
||||||
|
if (config.isHybridMode()) {
|
||||||
|
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const emailConfig = config.getRawConfig().email;
|
const emailConfig = config.getRawConfig().email;
|
||||||
if (!emailConfig) {
|
if (!emailConfig) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
|
@ -96,7 +96,11 @@ export class Config {
|
||||||
if (!this.rawConfig) {
|
if (!this.rawConfig) {
|
||||||
throw new Error("Config not loaded. Call load() first.");
|
throw new Error("Config not loaded. Call load() first.");
|
||||||
}
|
}
|
||||||
license.setServerSecret(this.rawConfig.server.secret);
|
if (this.rawConfig.hybrid) {
|
||||||
|
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
license.setServerSecret(this.rawConfig.server.secret!);
|
||||||
|
|
||||||
await this.checkKeyStatus();
|
await this.checkKeyStatus();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,21 +16,28 @@ export const configSchema = z
|
||||||
dashboard_url: z
|
dashboard_url: z
|
||||||
.string()
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.optional()
|
|
||||||
.pipe(z.string().url())
|
.pipe(z.string().url())
|
||||||
.transform((url) => url.toLowerCase()),
|
.transform((url) => url.toLowerCase())
|
||||||
|
.optional(),
|
||||||
log_level: z
|
log_level: z
|
||||||
.enum(["debug", "info", "warn", "error"])
|
.enum(["debug", "info", "warn", "error"])
|
||||||
.optional()
|
.optional()
|
||||||
.default("info"),
|
.default("info"),
|
||||||
save_logs: z.boolean().optional().default(false),
|
save_logs: z.boolean().optional().default(false),
|
||||||
log_failed_attempts: z.boolean().optional().default(false),
|
log_failed_attempts: z.boolean().optional().default(false),
|
||||||
telmetry: z
|
telemetry: z
|
||||||
.object({
|
.object({
|
||||||
anonymous_usage: z.boolean().optional().default(true)
|
anonymous_usage: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.default({})
|
||||||
|
}).optional().default({
|
||||||
|
log_level: "info",
|
||||||
|
save_logs: false,
|
||||||
|
log_failed_attempts: false,
|
||||||
|
telemetry: {
|
||||||
|
anonymous_usage: true
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
hybrid: z
|
hybrid: z
|
||||||
.object({
|
.object({
|
||||||
|
@ -122,9 +129,25 @@ export const configSchema = z
|
||||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
trust_proxy: z.number().int().gte(0).optional().default(1),
|
||||||
secret: z
|
secret: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
|
||||||
.transform(getEnvOrYaml("SERVER_SECRET"))
|
.transform(getEnvOrYaml("SERVER_SECRET"))
|
||||||
.pipe(z.string().min(8))
|
.pipe(z.string().min(8))
|
||||||
|
.optional()
|
||||||
|
}).optional().default({
|
||||||
|
integration_port: 3003,
|
||||||
|
external_port: 3000,
|
||||||
|
internal_port: 3001,
|
||||||
|
next_port: 3002,
|
||||||
|
internal_hostname: "pangolin",
|
||||||
|
session_cookie_name: "p_session_token",
|
||||||
|
resource_access_token_param: "p_token",
|
||||||
|
resource_access_token_headers: {
|
||||||
|
id: "P-Access-Token-Id",
|
||||||
|
token: "P-Access-Token"
|
||||||
|
},
|
||||||
|
resource_session_request_param: "resource_session_request_param",
|
||||||
|
dashboard_session_length_hours: 720,
|
||||||
|
resource_session_length_hours: 720,
|
||||||
|
trust_proxy: 1
|
||||||
}),
|
}),
|
||||||
postgres: z
|
postgres: z
|
||||||
.object({
|
.object({
|
||||||
|
@ -282,6 +305,10 @@ export const configSchema = z
|
||||||
if (data.flags?.disable_config_managed_domains) {
|
if (data.flags?.disable_config_managed_domains) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// If hybrid is defined, domains are not required
|
||||||
|
if (data.hybrid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -290,6 +317,32 @@ export const configSchema = z
|
||||||
{
|
{
|
||||||
message: "At least one domain must be defined"
|
message: "At least one domain must be defined"
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
// If hybrid is defined, server secret is not required
|
||||||
|
if (data.hybrid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If hybrid is not defined, server secret must be defined
|
||||||
|
return data.server?.secret !== undefined && data.server.secret.length > 0;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Server secret must be defined"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
// If hybrid is defined, dashboard_url is not required
|
||||||
|
if (data.hybrid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If hybrid is not defined, dashboard_url must be defined
|
||||||
|
return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Dashboard URL must be defined"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export function readConfigFile() {
|
export function readConfigFile() {
|
||||||
|
|
|
@ -16,7 +16,7 @@ class TelemetryClient {
|
||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const enabled = config.getRawConfig().app.telmetry.anonymous_usage;
|
const enabled = config.getRawConfig().app.telemetry.anonymous_usage;
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
const dev = process.env.ENVIRONMENT !== "prod";
|
const dev = process.env.ENVIRONMENT !== "prod";
|
||||||
|
|
||||||
|
|
|
@ -36,16 +36,16 @@ import { verifyTotpCode } from "@server/auth/totp";
|
||||||
|
|
||||||
// The RP ID is the domain name of your application
|
// The RP ID is the domain name of your application
|
||||||
const rpID = (() => {
|
const rpID = (() => {
|
||||||
const url = new URL(config.getRawConfig().app.dashboard_url);
|
const url = config.getRawConfig().app.dashboard_url ? new URL(config.getRawConfig().app.dashboard_url!) : undefined;
|
||||||
// For localhost, we must use 'localhost' without port
|
// For localhost, we must use 'localhost' without port
|
||||||
if (url.hostname === 'localhost') {
|
if (url?.hostname === 'localhost' || !url) {
|
||||||
return 'localhost';
|
return 'localhost';
|
||||||
}
|
}
|
||||||
return url.hostname;
|
return url.hostname;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const rpName = "Pangolin";
|
const rpName = "Pangolin";
|
||||||
const origin = config.getRawConfig().app.dashboard_url;
|
const origin = config.getRawConfig().app.dashboard_url || "localhost";
|
||||||
|
|
||||||
// Database-based challenge storage (replaces in-memory storage)
|
// Database-based challenge storage (replaces in-memory storage)
|
||||||
// Challenges are stored in the webauthnChallenge table with automatic expiration
|
// Challenges are stored in the webauthnChallenge table with automatic expiration
|
||||||
|
|
|
@ -8,7 +8,7 @@ export async function copyInConfig() {
|
||||||
const endpoint = config.getRawConfig().gerbil.base_endpoint;
|
const endpoint = config.getRawConfig().gerbil.base_endpoint;
|
||||||
const listenPort = config.getRawConfig().gerbil.start_port;
|
const listenPort = config.getRawConfig().gerbil.start_port;
|
||||||
|
|
||||||
if (!config.getRawConfig().flags?.disable_config_managed_domains) {
|
if (!config.getRawConfig().flags?.disable_config_managed_domains && config.getRawConfig().domains) {
|
||||||
await copyInDomains();
|
await copyInDomains();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { eq } from "drizzle-orm";
|
||||||
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
const random: RandomReader = {
|
const random: RandomReader = {
|
||||||
read(bytes: Uint8Array): void {
|
read(bytes: Uint8Array): void {
|
||||||
|
@ -22,6 +23,11 @@ function generateId(length: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureSetupToken() {
|
export async function ensureSetupToken() {
|
||||||
|
if (config.isHybridMode()) {
|
||||||
|
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if a server admin already exists
|
// Check if a server admin already exists
|
||||||
const [existingAdmin] = await db
|
const [existingAdmin] = await db
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue