Doing some testing to make sure that Cloudflare works with the app
This commit is contained in:
@@ -15,6 +15,41 @@ const els = {
|
|||||||
discordStart: document.getElementById("discordStart"),
|
discordStart: document.getElementById("discordStart"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function defaultApiBase() {
|
||||||
|
// 1) URL query override: …/index.html?api=http://host:9080
|
||||||
|
try {
|
||||||
|
const qs = new URLSearchParams(window.location.search);
|
||||||
|
const qApi = qs.get("api");
|
||||||
|
if (qApi) return qApi.replace(/\/+$/, "");
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// 2) Meta override in index.html: <meta name="gc-api-base" content="http://host:9080">
|
||||||
|
const m = document.querySelector('meta[name="gc-api-base"]');
|
||||||
|
if (m && m.content) return m.content.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// 3) Heuristic from frontend origin
|
||||||
|
try {
|
||||||
|
const u = new URL(window.location.href);
|
||||||
|
const proto = u.protocol;
|
||||||
|
const host = u.hostname; // no port
|
||||||
|
const portStr = u.port; // "" if default (80/443)
|
||||||
|
const bracketHost = host.includes(":") ? `[${host}]` : host; // IPv6-safe
|
||||||
|
|
||||||
|
const port = portStr ? parseInt(portStr, 10) : null;
|
||||||
|
let apiPort = port;
|
||||||
|
|
||||||
|
// Known frontend→API mappings
|
||||||
|
if (port === 8082) apiPort = 8080;
|
||||||
|
else if (port === 9082) apiPort = 9080;
|
||||||
|
else if (port) apiPort = Math.max(1, port - 2); // generic “minus two” fallback
|
||||||
|
|
||||||
|
return apiPort ? `${proto}//${bracketHost}:${apiPort}` : `${proto}//${bracketHost}`;
|
||||||
|
} catch {
|
||||||
|
return window.location.origin.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const LS_KEY = "gc_client_config_v1";
|
const LS_KEY = "gc_client_config_v1";
|
||||||
const POSTS_KEY = "gc_posts_index_v1";
|
const POSTS_KEY = "gc_posts_index_v1";
|
||||||
|
|
||||||
@@ -33,7 +68,17 @@ function saveConfig(c){ localStorage.setItem(LS_KEY, JSON.stringify(c)); Object.
|
|||||||
function getPosts(){ try { return JSON.parse(localStorage.getItem(POSTS_KEY)) ?? []; } catch { return []; } }
|
function getPosts(){ try { return JSON.parse(localStorage.getItem(POSTS_KEY)) ?? []; } catch { return []; } }
|
||||||
function setPosts(v){ localStorage.setItem(POSTS_KEY, JSON.stringify(v)); renderPosts(); }
|
function setPosts(v){ localStorage.setItem(POSTS_KEY, JSON.stringify(v)); renderPosts(); }
|
||||||
function norm(u){ return (u||"").replace(/\/+$/,""); }
|
function norm(u){ return (u||"").replace(/\/+$/,""); }
|
||||||
function applyConfig(){ els.shardUrl.value = cfg.url ?? location.origin; els.bearer.value = cfg.bearer ?? ""; els.passphrase.value = cfg.passphrase ?? ""; }
|
function applyConfig() {
|
||||||
|
// If no URL saved yet, detect a sensible default and persist it
|
||||||
|
if (!cfg.url) {
|
||||||
|
const detected = defaultApiBase(); // uses ?api=…, <meta gc-api-base>, or port heuristic
|
||||||
|
cfg.url = detected;
|
||||||
|
try { localStorage.setItem(LS_KEY, JSON.stringify(cfg)); } catch {}
|
||||||
|
}
|
||||||
|
els.shardUrl.value = cfg.url;
|
||||||
|
els.bearer.value = cfg.bearer ?? "";
|
||||||
|
els.passphrase.value = cfg.passphrase ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
async function checkHealth() {
|
async function checkHealth() {
|
||||||
if (!cfg.url) return; els.health.textContent = "Checking…";
|
if (!cfg.url) return; els.health.textContent = "Checking…";
|
||||||
@@ -151,7 +196,17 @@ async function delServer(p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function discordStart() {
|
async function discordStart() {
|
||||||
|
// Last-resort auto-fill if user didn’t hit Save
|
||||||
|
if (!cfg.url) {
|
||||||
|
const derived = defaultApiBase();
|
||||||
|
if (derived) {
|
||||||
|
cfg.url = derived;
|
||||||
|
try { localStorage.setItem(LS_KEY, JSON.stringify(cfg)); } catch {}
|
||||||
|
els.shardUrl.value = derived;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!cfg.url) { alert("Set shard URL first."); return; }
|
if (!cfg.url) { alert("Set shard URL first."); return; }
|
||||||
|
|
||||||
const r = await fetch(cfg.url + "/v1/auth/discord/start", { headers: { "X-GC-3P-Assent": "1" }});
|
const r = await fetch(cfg.url + "/v1/auth/discord/start", { headers: { "X-GC-3P-Assent": "1" }});
|
||||||
if (!r.ok) { alert("Discord SSO not available"); return; }
|
if (!r.ok) { alert("Discord SSO not available"); return; }
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
|
@@ -4,6 +4,9 @@
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>GreenCoast — Client</title>
|
<title>GreenCoast — Client</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||||
|
<meta name="gc-api-base" content="https://api.greencoast.fullmooncyberworks.com">
|
||||||
|
<!-- Optional: hard override API base during testing -->
|
||||||
|
<!-- <meta name="gc-api-base" content="http://127.0.0.1:9080"> -->
|
||||||
<link rel="stylesheet" href="./styles.css"/>
|
<link rel="stylesheet" href="./styles.css"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -12,6 +15,10 @@
|
|||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Connect</h2>
|
<h2>Connect</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label>Detected API</label>
|
||||||
|
<input id="detectedApi" readonly />
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Shard URL</label>
|
<label>Shard URL</label>
|
||||||
<input id="shardUrl" placeholder="http://localhost:8080" />
|
<input id="shardUrl" placeholder="http://localhost:8080" />
|
||||||
@@ -65,5 +72,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="./app.js"></script>
|
<script type="module" src="./app.js"></script>
|
||||||
|
<script>
|
||||||
|
// Show what the client detected for the API base, to confirm routing
|
||||||
|
(function(){
|
||||||
|
try {
|
||||||
|
if (typeof defaultApiBase === "function") {
|
||||||
|
const el = document.getElementById("detectedApi");
|
||||||
|
if (el) el.value = defaultApiBase();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
69
configs/shard.test.yaml
Normal file
69
configs/shard.test.yaml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
shard_id: "gc-test-001"
|
||||||
|
|
||||||
|
listen:
|
||||||
|
http: "0.0.0.0:9080" # API for testers
|
||||||
|
https: "" # if you terminate TLS at a proxy, leave empty
|
||||||
|
ws: "0.0.0.0:9081" # reserved
|
||||||
|
|
||||||
|
tls:
|
||||||
|
enable: false # set true only if serving HTTPS directly here
|
||||||
|
cert_file: "/etc/greencoast/tls/cert.pem"
|
||||||
|
key_file: "/etc/greencoast/tls/key.pem"
|
||||||
|
|
||||||
|
federation:
|
||||||
|
mtls_enable: false
|
||||||
|
listen: "0.0.0.0:9443"
|
||||||
|
cert_file: "/etc/greencoast/fed/cert.pem"
|
||||||
|
key_file: "/etc/greencoast/fed/key.pem"
|
||||||
|
client_ca_file: "/etc/greencoast/fed/clients_ca.pem"
|
||||||
|
|
||||||
|
ui:
|
||||||
|
enable: true
|
||||||
|
path: "./client"
|
||||||
|
base_url: "/"
|
||||||
|
frontend_http: "0.0.0.0:9082" # static client for testers
|
||||||
|
|
||||||
|
storage:
|
||||||
|
backend: "fs"
|
||||||
|
path: "/var/lib/greencoast/objects"
|
||||||
|
max_object_kb: 128 # lower if you want to constrain uploads
|
||||||
|
|
||||||
|
security:
|
||||||
|
zero_trust: true
|
||||||
|
require_mtls_for_federation: true
|
||||||
|
accept_client_signed_tokens: true
|
||||||
|
log_level: "warn"
|
||||||
|
|
||||||
|
privacy:
|
||||||
|
retain_ip: "no"
|
||||||
|
retain_user_agent: "no"
|
||||||
|
retain_timestamps: "coarse"
|
||||||
|
|
||||||
|
auth:
|
||||||
|
# IMPORTANT: rotate this per environment (use `openssl rand -hex 32`)
|
||||||
|
signing_secret: "D941C4F91D0046D28CDBC3F425DE0B4EA26BD2A80434E0F160D1B7C813EB43F8"
|
||||||
|
sso:
|
||||||
|
discord:
|
||||||
|
enabled: true
|
||||||
|
client_id: "REPLACE"
|
||||||
|
client_secret: "REPLACE"
|
||||||
|
# must exactly match your Discord app's allowed redirect
|
||||||
|
redirect_uri: "https://greencoast.fullmooncyberworks.com/auth-callback.html"
|
||||||
|
google:
|
||||||
|
enabled: false
|
||||||
|
client_id: ""
|
||||||
|
client_secret: ""
|
||||||
|
redirect_uri: ""
|
||||||
|
facebook:
|
||||||
|
enabled: false
|
||||||
|
client_id: ""
|
||||||
|
client_secret: ""
|
||||||
|
redirect_uri: ""
|
||||||
|
two_factor:
|
||||||
|
webauthn_enabled: false
|
||||||
|
totp_enabled: false
|
||||||
|
|
||||||
|
limits:
|
||||||
|
rate:
|
||||||
|
burst: 20
|
||||||
|
per_minute: 60 # slightly tighter for external testing
|
26
docker-compose.test.yml
Normal file
26
docker-compose.test.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
shard-test:
|
||||||
|
build: .
|
||||||
|
container_name: greencoast-shard-test
|
||||||
|
restart: unless-stopped
|
||||||
|
user: "0:0"
|
||||||
|
# You can keep these published for local debugging; Tunnel doesn't require them.
|
||||||
|
ports:
|
||||||
|
- "9080:9080" # API
|
||||||
|
- "9082:9082" # Frontend
|
||||||
|
environment:
|
||||||
|
- GC_DEV_ALLOW_UNAUTH=false
|
||||||
|
volumes:
|
||||||
|
- ./testdata:/var/lib/greencoast
|
||||||
|
- ./configs/shard.test.yaml:/app/shard.yaml:ro
|
||||||
|
- ./client:/app/client:ro
|
||||||
|
|
||||||
|
cloudflared:
|
||||||
|
image: cloudflare/cloudflared:latest
|
||||||
|
# Use the token you copy from Cloudflare Zero Trust → Tunnels
|
||||||
|
command: tunnel --no-autoupdate run --token ${CF_TUNNEL_TOKEN}
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- shard-test
|
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -9,7 +10,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mount static on the API mux (kept for compatibility; still serves under API port if you want)
|
func init() {
|
||||||
|
// Ensure common types are known (some distros are sparse by default)
|
||||||
|
_ = mime.AddExtensionType(".js", "application/javascript; charset=utf-8")
|
||||||
|
_ = mime.AddExtensionType(".css", "text/css; charset=utf-8")
|
||||||
|
_ = mime.AddExtensionType(".html", "text/html; charset=utf-8")
|
||||||
|
_ = mime.AddExtensionType(".map", "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) MountStatic(dir string, baseURL string) {
|
func (s *Server) MountStatic(dir string, baseURL string) {
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return
|
return
|
||||||
@@ -23,7 +31,6 @@ func (s *Server) MountStatic(dir string, baseURL string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: serve the same static handler on its own port (frontend).
|
|
||||||
func (s *Server) ListenFrontendHTTP(addr, dir, baseURL string) error {
|
func (s *Server) ListenFrontendHTTP(addr, dir, baseURL string) error {
|
||||||
if dir == "" || addr == "" {
|
if dir == "" || addr == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -48,6 +55,7 @@ func (s *Server) staticHandler(dir, baseURL string) http.Handler {
|
|||||||
}
|
}
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
s.secureHeaders(w)
|
s.secureHeaders(w)
|
||||||
|
|
||||||
up := strings.TrimPrefix(r.URL.Path, baseURL)
|
up := strings.TrimPrefix(r.URL.Path, baseURL)
|
||||||
if up == "" || strings.HasSuffix(r.URL.Path, "/") {
|
if up == "" || strings.HasSuffix(r.URL.Path, "/") {
|
||||||
up = "index.html"
|
up = "index.html"
|
||||||
@@ -57,12 +65,19 @@ func (s *Server) staticHandler(dir, baseURL string) http.Handler {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve file if it exists, else SPA-fallback to index.html
|
||||||
if st, err := os.Stat(full); err == nil && !st.IsDir() {
|
if st, err := os.Stat(full); err == nil && !st.IsDir() {
|
||||||
|
// Set Content-Type explicitly based on extension
|
||||||
|
if ctype := mime.TypeByExtension(filepath.Ext(full)); ctype != "" {
|
||||||
|
w.Header().Set("Content-Type", ctype)
|
||||||
|
}
|
||||||
http.ServeFile(w, r, full)
|
http.ServeFile(w, r, full)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fallback := filepath.Join(dir, "index.html")
|
fallback := filepath.Join(dir, "index.html")
|
||||||
if _, err := os.Stat(fallback); err == nil {
|
if _, err := os.Stat(fallback); err == nil {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
http.ServeFile(w, r, fallback)
|
http.ServeFile(w, r, fallback)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user