feat: combined launcher + auth-proxy with path-based routing
This commit is contained in:
parent
c54c01d609
commit
49ac348694
6 changed files with 335 additions and 377 deletions
|
|
@ -1,49 +1,12 @@
|
||||||
{
|
{
|
||||||
"ProxyType": 1,
|
"ProxyType": 1,
|
||||||
"RootOrMatchingDomain": "app.nextwks.eu",
|
"RootOrMatchingDomain": "app.nextwks.eu",
|
||||||
"MatchingDomainAlias": [],
|
"ActiveOrigins": [{
|
||||||
"ActiveOrigins": [
|
"OriginIpOrDomain": "127.0.0.1:9000",
|
||||||
{
|
"RequireTLS": false,
|
||||||
"OriginIpOrDomain": "127.0.0.1:9000",
|
"Weight": 1,
|
||||||
"RequireTLS": false,
|
"MaxConn": 0
|
||||||
"SkipCertValidations": false,
|
}],
|
||||||
"SkipWebSocketOriginCheck": false,
|
|
||||||
"Weight": 1,
|
|
||||||
"MaxConn": 0,
|
|
||||||
"RespTimeout": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"InactiveOrigins": [],
|
|
||||||
"UseStickySession": false,
|
|
||||||
"UseActiveLoadBalance": false,
|
|
||||||
"Disabled": false,
|
"Disabled": false,
|
||||||
"BypassGlobalTLS": false,
|
"AuthenticationProvider": {"AuthMethod": 0}
|
||||||
"VirtualDirectories": [],
|
|
||||||
"HeaderRewriteRules": {
|
|
||||||
"UserDefinedHeaders": null,
|
|
||||||
"RequestHostOverwrite": "",
|
|
||||||
"HSTSMaxAge": 0,
|
|
||||||
"EnablePermissionPolicyHeader": false,
|
|
||||||
"PermissionPolicy": null,
|
|
||||||
"DisableHopByHopHeaderRemoval": false
|
|
||||||
},
|
|
||||||
"EnableWebsocketCustomHeaders": false,
|
|
||||||
"AuthenticationProvider": {
|
|
||||||
"AuthMethod": 4,
|
|
||||||
"BasicAuthCredentials": null,
|
|
||||||
"BasicAuthExceptionRules": null,
|
|
||||||
"BasicAuthGroupIDs": [],
|
|
||||||
"ForwardAuthURL": "",
|
|
||||||
"ForwardAuthResponseHeaders": [],
|
|
||||||
"ForwardAuthResponseClientHeaders": [],
|
|
||||||
"ForwardAuthRequestHeaders": [],
|
|
||||||
"ForwardAuthRequestExcludedCookies": []
|
|
||||||
},
|
|
||||||
"RequireRateLimit": false,
|
|
||||||
"RateLimit": 0,
|
|
||||||
"DisableUptimeMonitor": false,
|
|
||||||
"AccessFilterUUID": "",
|
|
||||||
"DefaultSiteOption": 0,
|
|
||||||
"DefaultSiteValue": "",
|
|
||||||
"Tags": []
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"ProxyType": 1,
|
|
||||||
"RootOrMatchingDomain": "dns.nextwks.eu",
|
|
||||||
"ActiveOrigins": [
|
|
||||||
{
|
|
||||||
"OriginIpOrDomain": "127.0.0.1:8000",
|
|
||||||
"RequireTLS": false,
|
|
||||||
"Weight": 1,
|
|
||||||
"MaxConn": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Disabled": false,
|
|
||||||
"AuthenticationProvider": {
|
|
||||||
"AuthMethod": 0
|
|
||||||
},
|
|
||||||
"BypassGlobalTLS": false
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
{
|
|
||||||
"ProxyType": 1,
|
|
||||||
"RootOrMatchingDomain": "*",
|
|
||||||
"MatchingDomainAlias": [],
|
|
||||||
"ActiveOrigins": [],
|
|
||||||
"InactiveOrigins": [],
|
|
||||||
"UseStickySession": false,
|
|
||||||
"UseActiveLoadBalance": false,
|
|
||||||
"Disabled": false,
|
|
||||||
"BypassGlobalTLS": false,
|
|
||||||
"VirtualDirectories": [],
|
|
||||||
"HeaderRewriteRules": {
|
|
||||||
"UserDefinedHeaders": null,
|
|
||||||
"RequestHostOverwrite": "",
|
|
||||||
"HSTSMaxAge": 0,
|
|
||||||
"EnablePermissionPolicyHeader": false,
|
|
||||||
"PermissionPolicy": null,
|
|
||||||
"DisableHopByHopHeaderRemoval": false
|
|
||||||
},
|
|
||||||
"EnableWebsocketCustomHeaders": false,
|
|
||||||
"AuthenticationProvider": {
|
|
||||||
"AuthMethod": 0,
|
|
||||||
"BasicAuthCredentials": null,
|
|
||||||
"BasicAuthExceptionRules": null,
|
|
||||||
"BasicAuthGroupIDs": [],
|
|
||||||
"ForwardAuthURL": "",
|
|
||||||
"ForwardAuthResponseHeaders": [],
|
|
||||||
"ForwardAuthResponseClientHeaders": [],
|
|
||||||
"ForwardAuthRequestHeaders": [],
|
|
||||||
"ForwardAuthRequestExcludedCookies": []
|
|
||||||
},
|
|
||||||
"RequireRateLimit": false,
|
|
||||||
"RateLimit": 0,
|
|
||||||
"DisableUptimeMonitor": false,
|
|
||||||
"AccessFilterUUID": "",
|
|
||||||
"DefaultSiteOption": 0,
|
|
||||||
"DefaultSiteValue": "",
|
|
||||||
"Tags": []
|
|
||||||
}
|
|
||||||
205
deploy.sh
205
deploy.sh
|
|
@ -33,35 +33,35 @@ fi
|
||||||
|
|
||||||
# --- Common: pull + build ---
|
# --- Common: pull + build ---
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
echo "[1/8] Pulling latest code..."
|
echo "[1/6] Pulling latest code..."
|
||||||
git pull
|
git pull
|
||||||
|
|
||||||
echo "[2/8] Building binary..."
|
echo "[2/6] Building binary..."
|
||||||
export PATH=$PATH:/usr/local/go/bin
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
go build -o "$BINARY_NAME" .
|
go build -o "$BINARY_NAME" .
|
||||||
|
|
||||||
# --- Greenfield path ---
|
# --- Greenfield path ---
|
||||||
if [ "$GREENFIELD" = true ]; then
|
if [ "$GREENFIELD" = true ]; then
|
||||||
echo "[3/8] Removing old deployment..."
|
echo "[3/6] Removing old deployment..."
|
||||||
rm -rf "$TARGET_DIR"
|
rm -rf "$TARGET_DIR"
|
||||||
|
|
||||||
echo "[4/8] Creating target directories..."
|
echo "[4/6] Creating target directories..."
|
||||||
mkdir -p "$TARGET_DIR/config/nextworkspace"
|
mkdir -p "$TARGET_DIR/config/nextworkspace"
|
||||||
mkdir -p "$TARGET_DIR/config/zoraxy/conf/proxy"
|
mkdir -p "$TARGET_DIR/config/zoraxy/conf/proxy"
|
||||||
mkdir -p "$TARGET_DIR/data/zoraxy"
|
mkdir -p "$TARGET_DIR/data/zoraxy"
|
||||||
mkdir -p "$TARGET_DIR/compose"
|
mkdir -p "$TARGET_DIR/compose"
|
||||||
mkdir -p "$TARGET_DIR/logs"
|
mkdir -p "$TARGET_DIR/logs"
|
||||||
|
|
||||||
echo "[5/8] Copying binary..."
|
echo "[5/6] Copying binary..."
|
||||||
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
||||||
|
|
||||||
echo "[6/8] Deploying Zoraxy..."
|
echo "[6/6] Deploying Zoraxy..."
|
||||||
cp compose/zoraxy.yaml "$TARGET_DIR/compose/zoraxy.yaml"
|
cp compose/zoraxy.yaml "$TARGET_DIR/compose/zoraxy.yaml"
|
||||||
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" up -d 2>&1 || echo "[WARN] Zoraxy deploy had issues (see above)"
|
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" up -d 2>&1 || echo "[WARN] Zoraxy deploy had issues (see above)"
|
||||||
|
|
||||||
# Generate Zoraxy proxy configs with actual domain
|
# Generate Zoraxy proxy config (single app.{domain} → binary, auth handled by binary)
|
||||||
echo "[*] Generating Zoraxy proxy configs..."
|
echo "[*] Generating Zoraxy proxy config..."
|
||||||
cat > "$TARGET_DIR/config/zoraxy/conf/proxy/app.$DOMAIN.config" <<ZORAXY_APP
|
cat > "$TARGET_DIR/config/zoraxy/conf/proxy/app.$DOMAIN.config" <<ZORAXY_CFG
|
||||||
{
|
{
|
||||||
"ProxyType": 1,
|
"ProxyType": 1,
|
||||||
"RootOrMatchingDomain": "app.$DOMAIN",
|
"RootOrMatchingDomain": "app.$DOMAIN",
|
||||||
|
|
@ -72,153 +72,74 @@ if [ "$GREENFIELD" = true ]; then
|
||||||
"MaxConn": 0
|
"MaxConn": 0
|
||||||
}],
|
}],
|
||||||
"Disabled": false,
|
"Disabled": false,
|
||||||
"AuthenticationProvider": {"AuthMethod": 4}
|
|
||||||
}
|
|
||||||
ZORAXY_APP
|
|
||||||
|
|
||||||
cat > "$TARGET_DIR/config/zoraxy/conf/proxy/dns.$DOMAIN.config" <<ZORAXY_DNS
|
|
||||||
{
|
|
||||||
"ProxyType": 1,
|
|
||||||
"RootOrMatchingDomain": "dns.$DOMAIN",
|
|
||||||
"ActiveOrigins": [{
|
|
||||||
"OriginIpOrDomain": "127.0.0.1:8000",
|
|
||||||
"RequireTLS": false,
|
|
||||||
"Weight": 1,
|
|
||||||
"MaxConn": 0
|
|
||||||
}],
|
|
||||||
"Disabled": false,
|
|
||||||
"AuthenticationProvider": {"AuthMethod": 0}
|
"AuthenticationProvider": {"AuthMethod": 0}
|
||||||
}
|
}
|
||||||
ZORAXY_DNS
|
ZORAXY_CFG
|
||||||
|
|
||||||
# Copy launcher config files
|
# Copy launcher config files
|
||||||
cp -r config/nextworkspace/* "$TARGET_DIR/config/nextworkspace/"
|
cp -r config/nextworkspace/* "$TARGET_DIR/config/nextworkspace/"
|
||||||
|
|
||||||
# Create Zoraxy admin account
|
# Generate launcher apps.yaml with path-based URLs
|
||||||
echo "[*] Creating Zoraxy admin account..."
|
echo "[*] Generating apps.yaml..."
|
||||||
ZORAXY_ADMIN_URL="http://127.0.0.1:8000/api/auth/register"
|
|
||||||
|
|
||||||
# Wait for Zoraxy to be ready
|
|
||||||
for i in $(seq 1 15); do
|
|
||||||
if curl -sf "http://127.0.0.1:8000/api/auth/userCount" > /dev/null 2>&1; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo " Waiting for Zoraxy... ($i/15)"
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
# Fetch CSRF token AND session cookie from Zoraxy login page (follow redirect)
|
|
||||||
COOKIE_JAR="/tmp/zoraxy_cookies.txt"
|
|
||||||
rm -f "$COOKIE_JAR"
|
|
||||||
LOGIN_PAGE=$(curl -sL -c "$COOKIE_JAR" http://127.0.0.1:8000/)
|
|
||||||
CSRF_TOKEN=$(echo "$LOGIN_PAGE" | grep -oP '(?<=<meta name="zoraxy.csrf.Token" content=")[^"]+' || echo "")
|
|
||||||
if [ -z "$CSRF_TOKEN" ]; then
|
|
||||||
echo "[WARN] Could not fetch CSRF token, trying without it..."
|
|
||||||
ADMIN_CREATE=$(curl -s -b "$COOKIE_JAR" -X POST "$ZORAXY_ADMIN_URL" \
|
|
||||||
-d "username=${ADMIN_USERNAME:-admin}" \
|
|
||||||
-d "password=${ADMIN_PASSWORD:-admin}" 2>&1)
|
|
||||||
else
|
|
||||||
echo "[OK] CSRF token acquired"
|
|
||||||
ADMIN_CREATE=$(curl -s -b "$COOKIE_JAR" -X POST "$ZORAXY_ADMIN_URL" \
|
|
||||||
-H "X-CSRF-Token: $CSRF_TOKEN" \
|
|
||||||
-d "username=${ADMIN_USERNAME:-master}" \
|
|
||||||
-d "password=${ADMIN_PASSWORD:-9Aku7MfklZU9ldnZ}" 2>&1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$ADMIN_CREATE" | grep -qi '"success"\|"ok"\|"User registered\|"registered'; then
|
|
||||||
echo "[OK] Zoraxy admin account created"
|
|
||||||
elif echo "$ADMIN_CREATE" | grep -qi '"error'; then
|
|
||||||
echo "[WARN] Admin account create issue: $ADMIN_CREATE"
|
|
||||||
else
|
|
||||||
echo "[INFO] Admin API response: $ADMIN_CREATE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Login to Zoraxy API for authenticated operations ---
|
|
||||||
echo "[*] Logging in to Zoraxy API..."
|
|
||||||
# Re-fetch CSRF token with fresh cookies for login
|
|
||||||
LOGIN_PAGE=$(curl -sL -c "$COOKIE_JAR" http://127.0.0.1:8000/)
|
|
||||||
CSRF_TOKEN=$(echo "$LOGIN_PAGE" | grep -oP '(?<=<meta name="zoraxy.csrf.Token" content=")[^"]+' || echo "")
|
|
||||||
LOGIN_RESPONSE=$(curl -s -c "$COOKIE_JAR" -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/auth/login" \
|
|
||||||
-H "X-CSRF-Token: ${CSRF_TOKEN}" \
|
|
||||||
-d "username=${ADMIN_USERNAME:-master}" \
|
|
||||||
-d "password=${ADMIN_PASSWORD:-9Aku7MfklZU9ldnZ}")
|
|
||||||
|
|
||||||
if echo "$LOGIN_RESPONSE" | grep -qi '"success"\|"ok"'; then
|
|
||||||
echo "[OK] Logged in as $ADMIN_USERNAME"
|
|
||||||
else
|
|
||||||
echo "[WARN] Login response: $LOGIN_RESPONSE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Configure Let's Encrypt ---
|
|
||||||
echo "[*] Setting Let's Encrypt email..."
|
|
||||||
curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/acme/autoRenew/email" \
|
|
||||||
-d "set=${TLS_EMAIL}" > /dev/null
|
|
||||||
|
|
||||||
echo "[*] Obtaining certificates for app.${DOMAIN}..."
|
|
||||||
OBTAIN_RESULT=$(curl -s -b "$COOKIE_JAR" -X GET "http://127.0.0.1:8000/api/acme/obtainCert" \
|
|
||||||
-G \
|
|
||||||
-d "domains=app.${DOMAIN}" \
|
|
||||||
-d "filename=app.${DOMAIN}" \
|
|
||||||
-d "email=${TLS_EMAIL}" \
|
|
||||||
-d "ca=Let's Encrypt" \
|
|
||||||
-d "dns=false" 2>&1)
|
|
||||||
|
|
||||||
if echo "$OBTAIN_RESULT" | grep -qi '"success"\|"ok"\|"installed'; then
|
|
||||||
echo "[OK] Let's Encrypt certificate obtained for app.${DOMAIN}"
|
|
||||||
else
|
|
||||||
echo "[INFO] Certificate result: $OBTAIN_RESULT"
|
|
||||||
echo "[INFO] This may fail in dev if DNS doesn't point here — auto-renew will retry."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[*] Enabling auto-renew..."
|
|
||||||
curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/acme/autoRenew/enable" \
|
|
||||||
-d "enable=true" > /dev/null
|
|
||||||
|
|
||||||
# --- Configure ZorxAuth SSO ---
|
|
||||||
echo "[*] Configuring ZorxAuth SSO..."
|
|
||||||
curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/sso/zorxauth/provider" \
|
|
||||||
-d "enable=true" \
|
|
||||||
-d "name=NextWorkspace" \
|
|
||||||
-d "admin=${ADMIN_USERNAME}" > /dev/null
|
|
||||||
|
|
||||||
curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/sso/zorxauth/gateway" \
|
|
||||||
-d "redirect=https://app.${DOMAIN}/" > /dev/null
|
|
||||||
|
|
||||||
echo "[OK] ZorxAuth SSO configured"
|
|
||||||
|
|
||||||
# --- Fix SSO redirect URL in BoltDB (API may not persist it correctly) ---
|
|
||||||
echo "[*] Ensuring SSO redirect URL is set in BoltDB..."
|
|
||||||
go build -o /tmp/fix-zoraxy-sso ./tools/fix-zoraxy-sso/
|
|
||||||
sudo /tmp/fix-zoraxy-sso /opt/nextworkspace/config/zoraxy/sys.db 2>&1 || echo "[WARN] SSO BoltDB fix failed (non-fatal)"
|
|
||||||
rm -f /tmp/fix-zoraxy-sso
|
|
||||||
|
|
||||||
# --- Generate launcher apps.yaml with actual domain ---
|
|
||||||
cat > "$TARGET_DIR/config/nextworkspace/apps.yaml" <<EOF
|
cat > "$TARGET_DIR/config/nextworkspace/apps.yaml" <<EOF
|
||||||
apps:
|
apps:
|
||||||
- name: "NextWorkspace"
|
- name: "OpenCloud"
|
||||||
subtitle: "Your Workspace"
|
subtitle: "File Storage"
|
||||||
url: "https://app.${DOMAIN}/"
|
path: "/cloud"
|
||||||
icon: "home"
|
upstream: "http://127.0.0.1:9100"
|
||||||
- name: "Admin"
|
icon: "cloud"
|
||||||
subtitle: "Zoraxy Settings"
|
- name: "Euro Office"
|
||||||
url: "https://dns.${DOMAIN}/"
|
subtitle: "Collaborative Suite"
|
||||||
icon: "settings"
|
path: "/office"
|
||||||
|
upstream: "http://127.0.0.1:9200"
|
||||||
|
icon: "office"
|
||||||
|
- name: "ERPNext"
|
||||||
|
subtitle: "Enterprise ERP"
|
||||||
|
path: "/erp"
|
||||||
|
upstream: "http://127.0.0.1:9300"
|
||||||
|
icon: "erp"
|
||||||
|
- name: "Matrix Chat"
|
||||||
|
subtitle: "Team Communication"
|
||||||
|
path: "/chat"
|
||||||
|
upstream: "http://127.0.0.1:9400"
|
||||||
|
icon: "chat"
|
||||||
|
- name: "Jitsi"
|
||||||
|
subtitle: "Video Conferencing"
|
||||||
|
path: "/meet"
|
||||||
|
upstream: "http://127.0.0.1:9500"
|
||||||
|
icon: "video"
|
||||||
|
- name: "Webmail"
|
||||||
|
subtitle: "Email Client"
|
||||||
|
path: "/mail"
|
||||||
|
upstream: "http://127.0.0.1:9600"
|
||||||
|
icon: "mail"
|
||||||
|
- name: "AI Chat"
|
||||||
|
subtitle: "Open WebUI"
|
||||||
|
path: "/ai"
|
||||||
|
upstream: "http://127.0.0.1:9700"
|
||||||
|
icon: "bot"
|
||||||
|
- name: "Portainer"
|
||||||
|
subtitle: "Container Management"
|
||||||
|
path: "/admin"
|
||||||
|
upstream: "http://127.0.0.1:9800"
|
||||||
|
icon: "admin"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# --- Restart Zoraxy to apply config changes ---
|
# Restart Zoraxy to pick up config
|
||||||
echo "[*] Restarting Zoraxy to apply configs..."
|
echo "[*] Restarting Zoraxy to apply config..."
|
||||||
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" restart 2>&1 || true
|
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" restart 2>&1 || true
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# Write systemd service and start launcher
|
# Write systemd service with EnvironmentFile for .env vars
|
||||||
echo "[7/8] Writing systemd service and starting launcher..."
|
echo "[*] Writing systemd service..."
|
||||||
cat > /etc/systemd/system/$SERVICE_NAME.service <<UNIT
|
cat > /etc/systemd/system/$SERVICE_NAME.service <<UNIT
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=NextWorkspace Launcher
|
Description=NextWorkspace Launcher + Auth Proxy
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Environment=CONFIG_DIR=$TARGET_DIR/config/nextworkspace
|
Environment=CONFIG_DIR=$TARGET_DIR/config/nextworkspace
|
||||||
|
EnvironmentFile=$TARGET_DIR/.env
|
||||||
ExecStart=$TARGET_DIR/$BINARY_NAME
|
ExecStart=$TARGET_DIR/$BINARY_NAME
|
||||||
WorkingDirectory=$TARGET_DIR
|
WorkingDirectory=$TARGET_DIR
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|
@ -234,13 +155,13 @@ UNIT
|
||||||
|
|
||||||
# --- Smart update path ---
|
# --- Smart update path ---
|
||||||
else
|
else
|
||||||
echo "[3/8] Stopping launcher..."
|
echo "[3/6] Stopping launcher..."
|
||||||
systemctl stop $SERVICE_NAME 2>/dev/null || true
|
systemctl stop $SERVICE_NAME 2>/dev/null || true
|
||||||
|
|
||||||
echo "[4/8] Swapping binary..."
|
echo "[4/6] Swapping binary..."
|
||||||
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
||||||
|
|
||||||
echo "[5/8] Refreshing configs..."
|
echo "[5/6] Refreshing configs..."
|
||||||
if [ -f config/nextworkspace/apps.yaml ]; then
|
if [ -f config/nextworkspace/apps.yaml ]; then
|
||||||
cp config/nextworkspace/apps.yaml "$TARGET_DIR/config/nextworkspace/apps.yaml"
|
cp config/nextworkspace/apps.yaml "$TARGET_DIR/config/nextworkspace/apps.yaml"
|
||||||
fi
|
fi
|
||||||
|
|
@ -248,10 +169,8 @@ else
|
||||||
cp config/zoraxy/conf/proxy/* "$TARGET_DIR/config/zoraxy/conf/proxy/" 2>/dev/null || true
|
cp config/zoraxy/conf/proxy/* "$TARGET_DIR/config/zoraxy/conf/proxy/" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[6/8] Restarting Zoraxy..."
|
echo "[6/6] Restarting Zoraxy and launcher..."
|
||||||
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" restart 2>&1 || true
|
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" restart 2>&1 || true
|
||||||
|
|
||||||
echo "[7/8] Starting launcher..."
|
|
||||||
systemctl restart $SERVICE_NAME
|
systemctl restart $SERVICE_NAME
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
332
main.go
332
main.go
|
|
@ -1,12 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
@ -31,7 +38,8 @@ type Config struct {
|
||||||
type AppEntry struct {
|
type AppEntry struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Subtitle string `yaml:"subtitle"`
|
Subtitle string `yaml:"subtitle"`
|
||||||
URL string `yaml:"url"`
|
Path string `yaml:"path"`
|
||||||
|
Upstream string `yaml:"upstream"`
|
||||||
Icon string `yaml:"icon"`
|
Icon string `yaml:"icon"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +47,46 @@ type AppsFile struct {
|
||||||
Apps []AppEntry `yaml:"apps"`
|
Apps []AppEntry `yaml:"apps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Session key (derived from master password) ---
|
||||||
|
|
||||||
|
var sessionKey []byte
|
||||||
|
|
||||||
|
func initSessionKey(password string) {
|
||||||
|
hash := sha256.Sum256([]byte(password))
|
||||||
|
sessionKey = hash[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSessionToken(username string) string {
|
||||||
|
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||||
|
data := fmt.Sprintf("%s:%d", username, expires)
|
||||||
|
mac := hmac.New(sha256.New, sessionKey)
|
||||||
|
mac.Write([]byte(data))
|
||||||
|
sig := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
return hex.EncodeToString([]byte(data)) + "." + sig
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSessionToken(token string) (string, bool) {
|
||||||
|
parts := strings.SplitN(token, ".", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
data, err := hex.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
mac := hmac.New(sha256.New, sessionKey)
|
||||||
|
mac.Write(data)
|
||||||
|
expectedSig := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
if !hmac.Equal([]byte(parts[1]), []byte(expectedSig)) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
pieces := strings.SplitN(string(data), ":", 2)
|
||||||
|
if len(pieces) != 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return pieces[0], true
|
||||||
|
}
|
||||||
|
|
||||||
// --- Config loading ---
|
// --- Config loading ---
|
||||||
|
|
||||||
func loadConfig(configDir string) (*Config, error) {
|
func loadConfig(configDir string) (*Config, error) {
|
||||||
|
|
@ -70,6 +118,25 @@ func loadApps(configDir string) ([]AppEntry, error) {
|
||||||
return appsFile.Apps, nil
|
return appsFile.Apps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Auth middleware ---
|
||||||
|
|
||||||
|
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("session")
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username, valid := validateSessionToken(cookie.Value)
|
||||||
|
if !valid {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Header.Set("X-Forwarded-User", username)
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Handlers ---
|
// --- Handlers ---
|
||||||
|
|
||||||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -77,59 +144,157 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, "OK")
|
fmt.Fprint(w, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loginFormHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tmpl := template.Must(template.New("login").Parse(loginHTML))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
tmpl.Execute(w, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := r.FormValue("username")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
|
||||||
|
adminUser := os.Getenv("ADMIN_USERNAME")
|
||||||
|
adminPass := os.Getenv("ADMIN_PASSWORD")
|
||||||
|
|
||||||
|
if username == "" || password == "" || username != adminUser || password != adminPass {
|
||||||
|
tmpl := template.Must(template.New("login").Parse(loginHTML))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
tmpl.Execute(w, map[string]string{"Error": "Invalid username or password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := createSessionToken(username)
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: token,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: 86400,
|
||||||
|
})
|
||||||
|
http.Redirect(w, r, "/home/", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
http.Redirect(w, r, "/login", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc {
|
func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc {
|
||||||
tmpl := template.Must(template.New("launcher").Parse(launcherHTML))
|
tmpl := template.Must(template.New("launcher").Parse(launcherHTML))
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := r.Header.Get("X-Forwarded-User")
|
user := r.Header.Get("X-Forwarded-User")
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
AppName string
|
AppName string
|
||||||
Description string
|
Description string
|
||||||
User string
|
User string
|
||||||
LoggedIn bool
|
|
||||||
Apps []AppEntry
|
Apps []AppEntry
|
||||||
}{
|
}{
|
||||||
AppName: cfg.App.Name,
|
AppName: cfg.App.Name,
|
||||||
Description: cfg.App.Description,
|
Description: cfg.App.Description,
|
||||||
User: user,
|
User: user,
|
||||||
LoggedIn: user != "",
|
|
||||||
Apps: apps,
|
Apps: apps,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
tmpl.Execute(w, data)
|
tmpl.Execute(w, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Main ---
|
func proxyToUpstream(upstream string) http.HandlerFunc {
|
||||||
|
target, err := url.Parse(upstream)
|
||||||
func main() {
|
|
||||||
configDir := os.Getenv("CONFIG_DIR")
|
|
||||||
if configDir == "" {
|
|
||||||
configDir = "/opt/nextworkspace/config/nextworkspace"
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := loadConfig(configDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
log.Fatalf("Invalid upstream URL %q: %v", upstream, err)
|
||||||
}
|
}
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||||
apps, err := loadApps(configDir)
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
r.Header.Set("X-Forwarded-User", r.Header.Get("X-Forwarded-User"))
|
||||||
log.Fatalf("Failed to load apps: %v", err)
|
proxy.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/health", healthHandler)
|
|
||||||
mux.HandleFunc("/", launcherHandler(cfg, apps))
|
|
||||||
|
|
||||||
log.Printf("NextWorkspace launcher listening on %s", addr)
|
|
||||||
log.Fatal(http.ListenAndServe(addr, mux))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Templates ---
|
||||||
|
|
||||||
|
const loginHTML = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login — NextWorkspace</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #1a1a2e;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
width: 360px;
|
||||||
|
box-shadow: 0 2px 16px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 { font-size: 1.5rem; margin-bottom: 0.25rem; text-align: center; }
|
||||||
|
p.sub { color: #718096; text-align: center; margin-bottom: 1.5rem; font-size: 0.9rem; }
|
||||||
|
.error { background: #fed7d7; color: #c53030; padding: 0.75rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.85rem; }
|
||||||
|
label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.35rem; color: #4a5568; }
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input:focus { border-color: #1a1a2e; }
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.7rem;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover { background: #2d3748; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-box">
|
||||||
|
<h1>NextWorkspace</h1>
|
||||||
|
<p class="sub">Sign in to your workspace</p>
|
||||||
|
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
||||||
|
<form method="POST" action="/login/auth">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required autofocus>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
<button type="submit">Sign In</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
const launcherHTML = `<!DOCTYPE html>
|
const launcherHTML = `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -149,6 +314,7 @@ const launcherHTML = `<!DOCTYPE html>
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
header h1 { font-size: 2rem; margin-bottom: 0.25rem; }
|
header h1 { font-size: 2rem; margin-bottom: 0.25rem; }
|
||||||
header p { color: #a0aec0; font-size: 1rem; }
|
header p { color: #a0aec0; font-size: 1rem; }
|
||||||
|
|
@ -197,21 +363,6 @@ const launcherHTML = `<!DOCTYPE html>
|
||||||
}
|
}
|
||||||
.card h3 { font-size: 1.1rem; margin-bottom: 0.25rem; }
|
.card h3 { font-size: 1.1rem; margin-bottom: 0.25rem; }
|
||||||
.card p { color: #718096; font-size: 0.85rem; }
|
.card p { color: #718096; font-size: 0.85rem; }
|
||||||
.login-prompt {
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem 2rem;
|
|
||||||
}
|
|
||||||
.login-prompt h2 { margin-bottom: 1rem; }
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
background: #1a1a2e;
|
|
||||||
color: #fff;
|
|
||||||
padding: 0.75rem 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.btn:hover { background: #2d3748; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -219,29 +370,78 @@ const launcherHTML = `<!DOCTYPE html>
|
||||||
<h1>{{.AppName}}</h1>
|
<h1>{{.AppName}}</h1>
|
||||||
<p>{{.Description}}</p>
|
<p>{{.Description}}</p>
|
||||||
</header>
|
</header>
|
||||||
|
<div class="user-banner">
|
||||||
{{if .LoggedIn}}
|
Welcome, {{.User}} · <a href="/home/logout">Logout</a>
|
||||||
<div class="user-banner">
|
</div>
|
||||||
Welcome, {{.User}} · <a href="/logout">Logout</a>
|
<div class="grid">
|
||||||
</div>
|
{{range .Apps}}
|
||||||
{{else}}
|
{{if .Upstream}}
|
||||||
<div class="login-prompt">
|
<a class="card" href="{{.Path}}/" target="_blank" rel="noopener">
|
||||||
<h2>Welcome to {{.AppName}}</h2>
|
{{else}}
|
||||||
<p>Sign in to access your workspace applications.</p>
|
<div class="card" style="opacity:0.5; cursor:default;">
|
||||||
<a class="btn" href="/login">Login</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .LoggedIn}}
|
|
||||||
<div class="grid">
|
|
||||||
{{range .Apps}}
|
|
||||||
<a class="card" href="{{.URL}}" target="_blank" rel="noopener">
|
|
||||||
<div class="icon">{{.Icon}}</div>
|
|
||||||
<h3>{{.Name}}</h3>
|
|
||||||
<p>{{.Subtitle}}</p>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
<div class="icon">{{.Icon}}</div>
|
||||||
{{end}}
|
<h3>{{.Name}}</h3>
|
||||||
|
<p>{{.Subtitle}}</p>
|
||||||
|
{{if .Upstream}}</a>{{else}}</div>{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
|
// --- Main ---
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configDir := os.Getenv("CONFIG_DIR")
|
||||||
|
if configDir == "" {
|
||||||
|
configDir = "/opt/nextworkspace/config/nextworkspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
adminPassword := os.Getenv("ADMIN_PASSWORD")
|
||||||
|
if adminPassword == "" {
|
||||||
|
log.Fatal("ADMIN_PASSWORD environment variable is required")
|
||||||
|
}
|
||||||
|
initSessionKey(adminPassword)
|
||||||
|
|
||||||
|
cfg, err := loadConfig(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, err := loadApps(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load apps: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Unprotected paths
|
||||||
|
mux.HandleFunc("/health", healthHandler)
|
||||||
|
mux.HandleFunc("/login", loginFormHandler)
|
||||||
|
mux.HandleFunc("/login/auth", loginAuthHandler)
|
||||||
|
|
||||||
|
// Protected: launcher
|
||||||
|
mux.Handle("/home/", authMiddleware(launcherHandler(cfg, apps)))
|
||||||
|
mux.Handle("/home", authMiddleware(launcherHandler(cfg, apps)))
|
||||||
|
mux.HandleFunc("/home/logout", logoutHandler)
|
||||||
|
|
||||||
|
// Protected: upstream app proxies
|
||||||
|
for _, app := range apps {
|
||||||
|
if app.Path != "" && app.Upstream != "" {
|
||||||
|
appPath := app.Path
|
||||||
|
proxyHandler := authMiddleware(proxyToUpstream(app.Upstream))
|
||||||
|
mux.Handle(appPath+"/", proxyHandler)
|
||||||
|
mux.Handle(appPath, proxyHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: redirect to launcher
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/home/", http.StatusFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("NextWorkspace listening on %s", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, mux))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"go.etcd.io/bbolt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
fmt.Fprintf(os.Stderr, "Usage: fix-zoraxy-sso <db-path>\n")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPath := os.Args[1]
|
|
||||||
bucket := "zorxauth"
|
|
||||||
key := "options"
|
|
||||||
|
|
||||||
db, err := bbolt.Open(dbPath, 0600, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to open DB %s: %v", dbPath, err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
err = db.Update(func(tx *bbolt.Tx) error {
|
|
||||||
b := tx.Bucket([]byte(bucket))
|
|
||||||
if b == nil {
|
|
||||||
return fmt.Errorf("bucket %q not found", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
val := b.Get([]byte(key))
|
|
||||||
if val == nil {
|
|
||||||
return fmt.Errorf("key %q not found in bucket %q", key, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
var opts map[string]interface{}
|
|
||||||
if err := json.Unmarshal(val, &opts); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the critical SSO fields
|
|
||||||
opts["sso_redirect_url"] = fmt.Sprintf("https://app.%s/", getDomain())
|
|
||||||
opts["enable_auth_gateway"] = true
|
|
||||||
|
|
||||||
updated, err := json.Marshal(opts)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.Put([]byte(key), updated)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Update failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("[OK] ZorxAuth SSO config updated in BoltDB")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDomain() string {
|
|
||||||
if d := os.Getenv("DOMAIN"); d != "" {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
return "nextwks.eu"
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue