Initial commit: PilotVault multi-service project
Add API Server (Go/PocketBase), Web App (Go BFF + Vue), Fly App (Flutter/DJI MSDK), Adobe Plugin, and Docker/Docker AIO deployment configs. Design assets and build artifacts are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
afc6952eda
172 files changed
+24591
No files matched your search
@@ -0,0 +1,10 @@
|
||||
# Web App (control-panel BFF) configuration
|
||||
# Copy to .env and adjust. The server also reads plain environment variables.
|
||||
|
||||
# Address the Web App BFF listens on
|
||||
ADDR=:8090
|
||||
|
||||
# Default API Server this BFF proxies to (no trailing slash). The browser never
|
||||
# contacts the API Server directly; every request flows through this BFF. A user
|
||||
# may override this per-session at login, which is remembered in a cookie.
|
||||
API_BASE=http://localhost:8080
|
||||
@@ -0,0 +1,29 @@
|
||||
# Builds the Vue frontend (if needed) and runs the Web App BFF.
|
||||
#
|
||||
# ./Run-WebApp.ps1 # build frontend then serve
|
||||
# ./Run-WebApp.ps1 -SkipBuild # serve existing dist only
|
||||
|
||||
param([switch]$SkipBuild)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$serverDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$root = Split-Path -Parent $serverDir # the "Web App" folder
|
||||
$webDir = Join-Path $root "web"
|
||||
|
||||
$goBin = "C:\Program Files\Go\bin"
|
||||
if (Test-Path $goBin) { $env:Path = "$goBin;$env:Path" }
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
Write-Host "Building frontend..." -ForegroundColor Cyan
|
||||
Push-Location $webDir
|
||||
try {
|
||||
if (-not (Test-Path "node_modules")) { npm install }
|
||||
npm run build
|
||||
} finally { Pop-Location }
|
||||
}
|
||||
|
||||
Push-Location $serverDir
|
||||
try {
|
||||
Write-Host "Starting Web App on :8090 (Ctrl+C to stop)..." -ForegroundColor Cyan
|
||||
go run .
|
||||
} finally { Pop-Location }
|
||||
@@ -0,0 +1,453 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
cookieToken = "dji_token"
|
||||
cookieEmail = "dji_email"
|
||||
cookieApi = "dji_api"
|
||||
)
|
||||
|
||||
var client = &http.Client{Timeout: 15 * time.Second}
|
||||
|
||||
// apiBaseFor returns the API Server base for this request: the per-session value
|
||||
// chosen at login (cookie), falling back to the server's configured default.
|
||||
func (a *App) apiBaseFor(r *http.Request) string {
|
||||
if c, err := r.Cookie(cookieApi); err == nil && c.Value != "" {
|
||||
return c.Value
|
||||
}
|
||||
return a.apiBase
|
||||
}
|
||||
|
||||
func normalizeURL(u string) string {
|
||||
u = strings.TrimSpace(u)
|
||||
if u == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||||
u = "http://" + u
|
||||
}
|
||||
return strings.TrimRight(u, "/")
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// requireAuth gates a handler on the presence of a session cookie.
|
||||
func (a *App) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := r.Cookie(cookieToken); err != nil {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "not signed in"})
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /bff/login {"email","password"} → proxies to API Server /api/auth/login.
|
||||
func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
ApiBase string `json:"apiBase"` // optional API Server override (per session)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid json"})
|
||||
return
|
||||
}
|
||||
|
||||
base := a.apiBase
|
||||
if b := normalizeURL(body.ApiBase); b != "" {
|
||||
base = b
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]string{"email": body.Email, "password": body.Password})
|
||||
resp, err := client.Post(base+"/api/auth/login", "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Relay PocketBase's error (e.g. invalid credentials).
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = w.Write(data)
|
||||
return
|
||||
}
|
||||
|
||||
var auth struct {
|
||||
Token string `json:"token"`
|
||||
Record struct {
|
||||
Email string `json:"email"`
|
||||
} `json:"record"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &auth); err != nil || auth.Token == "" {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "unexpected auth response"})
|
||||
return
|
||||
}
|
||||
|
||||
setCookie(w, cookieToken, auth.Token, true)
|
||||
setCookie(w, cookieEmail, auth.Record.Email, false)
|
||||
setCookie(w, cookieApi, base, true) // remember the chosen API Server for this session
|
||||
writeJSON(w, http.StatusOK, map[string]any{"email": auth.Record.Email})
|
||||
}
|
||||
|
||||
// GET /bff/config → the server's default API Server address (to prefill the field).
|
||||
func (a *App) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"apiBase": a.apiBase})
|
||||
}
|
||||
|
||||
// POST /bff/logout
|
||||
func (a *App) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
clearCookie(w, cookieToken)
|
||||
clearCookie(w, cookieEmail)
|
||||
clearCookie(w, cookieApi)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// GET /bff/me → current session info incl. role (or 401). Proxies to the API
|
||||
// Server /api/me so the role is always fresh (reflects admin promotions).
|
||||
func (a *App) handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
token := tokenOf(r)
|
||||
if token == "" {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "not signed in"})
|
||||
return
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/me", nil)
|
||||
req.Header.Set("Authorization", token)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// Fall back to the cookie email so the session survives a brief API blip.
|
||||
email := ""
|
||||
if c, err := r.Cookie(cookieEmail); err == nil {
|
||||
email = c.Value
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"email": email, "role": "user"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
relay(w, resp)
|
||||
}
|
||||
|
||||
// GET /bff/devices → API Server /api/devices
|
||||
func (a *App) handleDevices(w http.ResponseWriter, r *http.Request) {
|
||||
a.proxyGET(w, a.apiBaseFor(r)+"/api/devices")
|
||||
}
|
||||
|
||||
// GET /bff/devices/{id}/track → API Server /api/devices/{id}/track
|
||||
func (a *App) handleTrack(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
a.proxyGET(w, a.apiBaseFor(r)+"/api/devices/"+id+"/track")
|
||||
}
|
||||
|
||||
// POST /bff/devices/{id}/command → API Server /api/devices/{id}/command
|
||||
func (a *App) handleCommand(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
resp, err := client.Post(a.apiBaseFor(r)+"/api/devices/"+id+"/command", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
relay(w, resp)
|
||||
}
|
||||
|
||||
// tokenOf returns the PocketBase auth token stored in the session cookie.
|
||||
func tokenOf(r *http.Request) string {
|
||||
if c, err := r.Cookie(cookieToken); err == nil {
|
||||
return c.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GET /bff/preferences → API Server /api/preferences (Authorization: session token)
|
||||
func (a *App) handleGetPrefs(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/preferences", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
relay(w, resp)
|
||||
}
|
||||
|
||||
// PUT /bff/preferences → API Server /api/preferences (Authorization: session token)
|
||||
func (a *App) handlePutPrefs(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPut, a.apiBaseFor(r)+"/api/preferences", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
relay(w, resp)
|
||||
}
|
||||
|
||||
// GET /bff/integrations/opensky → API Server /api/integrations/opensky
|
||||
func (a *App) handleGetOpenSky(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/integrations/opensky", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PUT /bff/integrations/opensky → API Server /api/integrations/opensky
|
||||
func (a *App) handlePutOpenSky(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPut, a.apiBaseFor(r)+"/api/integrations/opensky", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/integrations/opensky/health → API Server /api/integrations/opensky/health
|
||||
func (a *App) handleOpenSkyHealth(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/integrations/opensky/health", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// GET /bff/integrations/filetransfer → API Server /api/integrations/filetransfer
|
||||
func (a *App) handleGetFileTransfer(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/integrations/filetransfer", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PUT /bff/integrations/filetransfer → API Server /api/integrations/filetransfer
|
||||
func (a *App) handlePutFileTransfer(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPut, a.apiBaseFor(r)+"/api/integrations/filetransfer", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/integrations/filetransfer/health → API Server /api/integrations/filetransfer/health
|
||||
func (a *App) handleFileTransferHealth(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/integrations/filetransfer/health", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// GET /bff/integrations/localstorage → API Server /api/integrations/localstorage
|
||||
func (a *App) handleGetLocalStorage(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/integrations/localstorage", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PUT /bff/integrations/localstorage → API Server /api/integrations/localstorage
|
||||
func (a *App) handlePutLocalStorage(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPut, a.apiBaseFor(r)+"/api/integrations/localstorage", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/integrations/localstorage/health → API Server /api/integrations/localstorage/health
|
||||
func (a *App) handleLocalStorageHealth(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/integrations/localstorage/health", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// GET /bff/integrations/webdav → API Server /api/integrations/webdav
|
||||
func (a *App) handleGetWebDav(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/integrations/webdav", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PUT /bff/integrations/webdav → API Server /api/integrations/webdav
|
||||
func (a *App) handlePutWebDav(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPut, a.apiBaseFor(r)+"/api/integrations/webdav", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/integrations/webdav/health → API Server /api/integrations/webdav/health
|
||||
func (a *App) handleWebDavHealth(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/integrations/webdav/health", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// GET /bff/users → API Server /api/users (admin only, enforced upstream)
|
||||
func (a *App) handleListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/users", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/users → API Server /api/users
|
||||
func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/users", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PATCH /bff/users/{id} → API Server /api/users/{id}
|
||||
func (a *App) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPatch, a.apiBaseFor(r)+"/api/users/"+url.PathEscape(id), bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// DELETE /bff/users/{id} → API Server /api/users/{id}
|
||||
func (a *App) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
req, _ := http.NewRequest(http.MethodDelete, a.apiBaseFor(r)+"/api/users/"+url.PathEscape(id), nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// GET /bff/orgs → API Server /api/orgs (manager only, enforced upstream)
|
||||
func (a *App) handleListOrgs(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/orgs", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/orgs → API Server /api/orgs
|
||||
func (a *App) handleCreateOrg(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/orgs", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PATCH /bff/orgs/{id} → API Server /api/orgs/{id}
|
||||
func (a *App) handleUpdateOrg(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPatch, a.apiBaseFor(r)+"/api/orgs/"+url.PathEscape(id), bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// DELETE /bff/orgs/{id} → API Server /api/orgs/{id}
|
||||
func (a *App) handleDeleteOrg(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
req, _ := http.NewRequest(http.MethodDelete, a.apiBaseFor(r)+"/api/orgs/"+url.PathEscape(id), nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// doRelay executes an outbound request and relays the response verbatim.
|
||||
func (a *App) doRelay(w http.ResponseWriter, req *http.Request) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
relay(w, resp)
|
||||
}
|
||||
|
||||
func (a *App) proxyGET(w http.ResponseWriter, url string) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
relay(w, resp)
|
||||
}
|
||||
|
||||
func relay(w http.ResponseWriter, resp *http.Response) {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
// GET /bff/ws → relays the API Server's /ws/ui websocket to the browser.
|
||||
func (a *App) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := r.Cookie(cookieToken); err != nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
wsBase := strings.Replace(a.apiBaseFor(r), "http", "ws", 1) // http→ws, https→wss
|
||||
upstream, _, err := websocket.DefaultDialer.Dial(wsBase+"/ws/ui", nil)
|
||||
if err != nil {
|
||||
http.Error(w, "upstream websocket unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
downstream, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
upstream.Close()
|
||||
return
|
||||
}
|
||||
go pipe(upstream, downstream)
|
||||
pipe(downstream, upstream)
|
||||
}
|
||||
|
||||
func pipe(src, dst *websocket.Conn) {
|
||||
defer src.Close()
|
||||
defer dst.Close()
|
||||
for {
|
||||
mt, msg, err := src.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := dst.WriteMessage(mt, msg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setCookie(w http.ResponseWriter, name, value string, httpOnly bool) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
HttpOnly: httpOnly,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: 7 * 24 * 3600,
|
||||
})
|
||||
}
|
||||
|
||||
func clearCookie(w http.ResponseWriter, name string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name, Value: "", Path: "/", HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode, MaxAge: -1,
|
||||
})
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))a(l);new MutationObserver(l=>{for(const c of l)if(c.type==="childList")for(const h of c.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&a(h)}).observe(document,{childList:!0,subtree:!0});function o(l){const c={};return l.integrity&&(c.integrity=l.integrity),l.referrerPolicy&&(c.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?c.credentials="include":l.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function a(l){if(l.ep)return;l.ep=!0;const c=o(l);fetch(l.href,c)}})();/**
|
||||
* @vue/shared v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/function va(e){const i=Object.create(null);for(const o of e.split(","))i[o]=1;return o=>o in i}const ie={},_s=[],Zn=()=>{},iu=()=>!1,lr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ur=e=>e.startsWith("onUpdate:"),Ce=Object.assign,ya=(e,i)=>{const o=e.indexOf(i);o>-1&&e.splice(o,1)},Qc=Object.prototype.hasOwnProperty,Xt=(e,i)=>Qc.call(e,i),pt=Array.isArray,vs=e=>po(e)==="[object Map]",Ts=e=>po(e)==="[object Set]",il=e=>po(e)==="[object Date]",Lt=e=>typeof e=="function",pe=e=>typeof e=="string",Ln=e=>typeof e=="symbol",Qt=e=>e!==null&&typeof e=="object",su=e=>(Qt(e)||Lt(e))&&Lt(e.then)&&Lt(e.catch),ou=Object.prototype.toString,po=e=>ou.call(e),td=e=>po(e).slice(8,-1),ru=e=>po(e)==="[object Object]",ba=e=>pe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Xs=va(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),cr=e=>{const i=Object.create(null);return(o=>i[o]||(i[o]=e(o)))},ed=/-\w/g,Pn=cr(e=>e.replace(ed,i=>i.slice(1).toUpperCase())),nd=/\B([A-Z])/g,Li=cr(e=>e.replace(nd,"-$1").toLowerCase()),au=cr(e=>e.charAt(0).toUpperCase()+e.slice(1)),jr=cr(e=>e?`on${au(e)}`:""),Vn=(e,i)=>!Object.is(e,i),Go=(e,...i)=>{for(let o=0;o<e.length;o++)e[o](...i)},lu=(e,i,o,a=!1)=>{Object.defineProperty(e,i,{configurable:!0,enumerable:!1,writable:a,value:o})},dr=e=>{const i=parseFloat(e);return isNaN(i)?e:i},id=e=>{const i=pe(e)?Number(e):NaN;return isNaN(i)?e:i};let sl;const fr=()=>sl||(sl=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ks(e){if(pt(e)){const i={};for(let o=0;o<e.length;o++){const a=e[o],l=pe(a)?ad(a):ks(a);if(l)for(const c in l)i[c]=l[c]}return i}else if(pe(e)||Qt(e))return e}const sd=/;(?![^(]*\))/g,od=/:([^]+)/,rd=/\/\*[^]*?\*\//g;function ad(e){const i={};return e.replace(rd,"").split(sd).forEach(o=>{if(o){const a=o.split(od);a.length>1&&(i[a[0].trim()]=a[1].trim())}}),i}function Ot(e){let i="";if(pe(e))i=e;else if(pt(e))for(let o=0;o<e.length;o++){const a=Ot(e[o]);a&&(i+=a+" ")}else if(Qt(e))for(const o in e)e[o]&&(i+=o+" ");return i.trim()}const ld="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ud=va(ld);function uu(e){return!!e||e===""}function cd(e,i){if(e.length!==i.length)return!1;let o=!0;for(let a=0;o&&a<e.length;a++)o=Si(e[a],i[a]);return o}function Si(e,i){if(e===i)return!0;let o=il(e),a=il(i);if(o||a)return o&&a?e.getTime()===i.getTime():!1;if(o=Ln(e),a=Ln(i),o||a)return e===i;if(o=pt(e),a=pt(i),o||a)return o&&a?cd(e,i):!1;if(o=Qt(e),a=Qt(i),o||a){if(!o||!a)return!1;const l=Object.keys(e).length,c=Object.keys(i).length;if(l!==c)return!1;for(const h in e){const g=e.hasOwnProperty(h),v=i.hasOwnProperty(h);if(g&&!v||!g&&v||!Si(e[h],i[h]))return!1}}return String(e)===String(i)}function xa(e,i){return e.findIndex(o=>Si(o,i))}const cu=e=>!!(e&&e.__v_isRef===!0),M=e=>pe(e)?e:e==null?"":pt(e)||Qt(e)&&(e.toString===ou||!Lt(e.toString))?cu(e)?M(e.value):JSON.stringify(e,du,2):String(e),du=(e,i)=>cu(i)?du(e,i.value):vs(i)?{[`Map(${i.size})`]:[...i.entries()].reduce((o,[a,l],c)=>(o[Wr(a,c)+" =>"]=l,o),{})}:Ts(i)?{[`Set(${i.size})`]:[...i.values()].map(o=>Wr(o))}:Ln(i)?Wr(i):Qt(i)&&!pt(i)&&!ru(i)?String(i):i,Wr=(e,i="")=>{var o;return Ln(e)?`Symbol(${(o=e.description)!=null?o:i})`:e};/**
|
||||
* @vue/reactivity v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/let ze;class dd{constructor(i=!1){this.detached=i,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!i&&ze&&(ze.active?(this.parent=ze,this.index=(ze.scopes||(ze.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let i,o;if(this.scopes)for(i=0,o=this.scopes.length;i<o;i++)this.scopes[i].pause();for(i=0,o=this.effects.length;i<o;i++)this.effects[i].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let i,o;if(this.scopes)for(i=0,o=this.scopes.length;i<o;i++)this.scopes[i].resume();for(i=0,o=this.effects.length;i<o;i++)this.effects[i].resume()}}run(i){if(this._active){const o=ze;try{return ze=this,i()}finally{ze=o}}}on(){++this._on===1&&(this.prevScope=ze,ze=this)}off(){if(this._on>0&&--this._on===0){if(ze===this)ze=this.prevScope;else{let i=ze;for(;i;){if(i.prevScope===this){i.prevScope=this.prevScope;break}i=i.prevScope}}this.prevScope=void 0}}stop(i){if(this._active){this._active=!1;let o,a;for(o=0,a=this.effects.length;o<a;o++)this.effects[o].stop();for(this.effects.length=0,o=0,a=this.cleanups.length;o<a;o++)this.cleanups[o]();if(this.cleanups.length=0,this.scopes){for(o=0,a=this.scopes.length;o<a;o++)this.scopes[o].stop(!0);this.scopes.length=0}if(!this.detached&&this.parent&&!i){const l=this.parent.scopes.pop();l&&l!==this&&(this.parent.scopes[this.index]=l,l.index=this.index)}this.parent=void 0}}}function fd(){return ze}let re;const Kr=new WeakSet;class fu{constructor(i){this.fn=i,this.deps=void 0,this.depsTail=void 0,this.flags=5,this.next=void 0,this.cleanup=void 0,this.scheduler=void 0,ze&&(ze.active?ze.effects.push(this):this.flags&=-2)}pause(){this.flags|=64}resume(){this.flags&64&&(this.flags&=-65,Kr.has(this)&&(Kr.delete(this),this.trigger()))}notify(){this.flags&2&&!(this.flags&32)||this.flags&8||pu(this)}run(){if(!(this.flags&1))return this.fn();this.flags|=2,ol(this),mu(this);const i=re,o=Tn;re=this,Tn=!0;try{return this.fn()}finally{gu(this),re=i,Tn=o,this.flags&=-3}}stop(){if(this.flags&1){for(let i=this.deps;i;i=i.nextDep)Sa(i);this.deps=this.depsTail=void 0,ol(this),this.onStop&&this.onStop(),this.flags&=-2}}trigger(){this.flags&64?Kr.add(this):this.scheduler?this.scheduler():this.runIfDirty()}runIfDirty(){ia(this)&&this.run()}get dirty(){return ia(this)}}let hu=0,Qs,to;function pu(e,i=!1){if(e.flags|=8,i){e.next=to,to=e;return}e.next=Qs,Qs=e}function wa(){hu++}function ka(){if(--hu>0)return;if(to){let i=to;for(to=void 0;i;){const o=i.next;i.next=void 0,i.flags&=-9,i=o}}let e;for(;Qs;){let i=Qs;for(Qs=void 0;i;){const o=i.next;if(i.next=void 0,i.flags&=-9,i.flags&1)try{i.trigger()}catch(a){e||(e=a)}i=o}}if(e)throw e}function mu(e){for(let i=e.deps;i;i=i.nextDep)i.version=-1,i.prevActiveLink=i.dep.activeLink,i.dep.activeLink=i}function gu(e){let i,o=e.depsTail,a=o;for(;a;){const l=a.prevDep;a.version===-1?(a===o&&(o=l),Sa(a),hd(a)):i=a,a.dep.activeLink=a.prevActiveLink,a.prevActiveLink=void 0,a=l}e.deps=i,e.depsTail=o}function ia(e){for(let i=e.deps;i;i=i.nextDep)if(i.dep.version!==i.version||i.dep.computed&&(_u(i.dep.computed)||i.dep.version!==i.version))return!0;return!!e._dirty}function _u(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===oo)||(e.globalVersion=oo,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!ia(e))))return;e.flags|=2;const i=e.dep,o=re,a=Tn;re=e,Tn=!0;try{mu(e);const l=e.fn(e._value);(i.version===0||Vn(l,e._value))&&(e.flags|=128,e._value=l,i.version++)}catch(l){throw i.version++,l}finally{re=o,Tn=a,gu(e),e.flags&=-3}}function Sa(e,i=!1){const{dep:o,prevSub:a,nextSub:l}=e;if(a&&(a.nextSub=l,e.prevSub=void 0),l&&(l.prevSub=a,e.nextSub=void 0),o.subs===e&&(o.subs=a,!a&&o.computed)){o.computed.flags&=-5;for(let c=o.computed.deps;c;c=c.nextDep)Sa(c,!0)}!i&&!--o.sc&&o.map&&o.map.delete(o.key)}function hd(e){const{prevDep:i,nextDep:o}=e;i&&(i.nextDep=o,e.prevDep=void 0),o&&(o.prevDep=i,e.nextDep=void 0)}let Tn=!0;const vu=[];function $n(){vu.push(Tn),Tn=!1}function Hn(){const e=vu.pop();Tn=e===void 0?!0:e}function ol(e){const{cleanup:i}=e;if(e.cleanup=void 0,i){const o=re;re=void 0;try{i()}finally{re=o}}}let oo=0;class pd{constructor(i,o){this.sub=i,this.dep=o,this.version=o.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Pa{constructor(i){this.computed=i,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(i){if(!re||!Tn||re===this.computed)return;let o=this.activeLink;if(o===void 0||o.sub!==re)o=this.activeLink=new pd(re,this),re.deps?(o.prevDep=re.depsTail,re.depsTail.nextDep=o,re.depsTail=o):re.deps=re.depsTail=o,yu(o);else if(o.version===-1&&(o.version=this.version,o.nextDep)){const a=o.nextDep;a.prevDep=o.prevDep,o.prevDep&&(o.prevDep.nextDep=a),o.prevDep=re.depsTail,o.nextDep=void 0,re.depsTail.nextDep=o,re.depsTail=o,re.deps===o&&(re.deps=a)}return o}trigger(i){this.versiLine truncated
|
||||
* @vue/runtime-core v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/function mo(e,i,o,a){try{return a?e(...a):e()}catch(l){pr(l,i,o)}}function mn(e,i,o,a){if(Lt(e)){const l=mo(e,i,o,a);return l&&su(l)&&l.catch(c=>{pr(c,i,o)}),l}if(pt(e)){const l=[];for(let c=0;c<e.length;c++)l.push(mn(e[c],i,o,a));return l}}function pr(e,i,o,a=!0){const l=i?i.vnode:null,{errorHandler:c,throwUnhandledErrorInProduction:h}=i&&i.appContext.config||ie;if(i){let g=i.parent;const v=i.proxy,P=`https://vuejs.org/error-reference/#runtime-${o}`;for(;g;){const k=g.ec;if(k){for(let O=0;O<k.length;O++)if(k[O](e,v,P)===!1)return}g=g.parent}if(c){$n(),mo(c,null,10,[e,v,P]),Hn();return}}Fd(e,o,l,a,h)}function Fd(e,i,o,a=!0,l=!1){if(l)throw e;console.error(e)}const je=[];let Dn=-1;const ys=[];let ki=null,gs=0;const Lu=Promise.resolve();let Xo=null;function Mu(e){const i=Xo||Lu;return e?i.then(this?e.bind(this):e):i}function Vd(e){let i=Dn+1,o=je.length;for(;i<o;){const a=i+o>>>1,l=je[a],c=ao(l);c<e||c===e&&l.flags&2?i=a+1:o=a}return i}function Ca(e){if(!(e.flags&1)){const i=ao(e),o=je[je.length-1];!o||!(e.flags&2)&&i>=ao(o)?je.push(e):je.splice(Vd(i),0,e),e.flags|=1,Cu()}}function Cu(){Xo||(Xo=Lu.then(Eu))}function Zd(e){pt(e)?ys.push(...e):ki&&e.id===-1?ki.splice(gs+1,0,e):e.flags&1||(ys.push(e),e.flags|=1),Cu()}function al(e,i,o=Dn+1){for(;o<je.length;o++){const a=je[o];if(a&&a.flags&2){if(e&&a.id!==e.uid)continue;je.splice(o,1),o--,a.flags&4&&(a.flags&=-2),a(),a.flags&4||(a.flags&=-2)}}}function Ou(e){if(ys.length){const i=[...new Set(ys)].sort((o,a)=>ao(o)-ao(a));if(ys.length=0,ki){ki.push(...i);return}for(ki=i,gs=0;gs<ki.length;gs++){const o=ki[gs];o.flags&4&&(o.flags&=-2),o.flags&8||o(),o.flags&=-2}ki=null,gs=0}}const ao=e=>e.id==null?e.flags&2?-1:1/0:e.id;function Eu(e){try{for(Dn=0;Dn<je.length;Dn++){const i=je[Dn];i&&!(i.flags&8)&&(i.flags&4&&(i.flags&=-2),mo(i,i.i,i.i?15:14),i.flags&4||(i.flags&=-2))}}finally{for(;Dn<je.length;Dn++){const i=je[Dn];i&&(i.flags&=-2)}Dn=-1,je.length=0,Ou(),Xo=null,(je.length||ys.length)&&Eu()}}let De=null,zu=null;function Qo(e){const i=De;return De=e,zu=e&&e.type.__scopeId||null,i}function ot(e,i=De,o){if(!i||e._n)return e;const a=(...l)=>{a._d&&nr(-1);const c=Qo(i);let h;try{h=e(...l)}finally{Qo(c),a._d&&nr(1)}return h};return a._n=!0,a._c=!0,a._d=!0,a}function xt(e,i){if(De===null)return e;const o=br(De),a=e.dirs||(e.dirs=[]);for(let l=0;l<i.length;l++){let[c,h,g,v=ie]=i[l];c&&(Lt(c)&&(c={mounted:c,updated:c}),c.deep&&oi(h),a.push({dir:c,instance:o,value:h,oldValue:void 0,arg:g,modifiers:v}))}return e}function Zi(e,i,o,a){const l=e.dirs,c=i&&i.dirs;for(let h=0;h<l.length;h++){const g=l[h];c&&(g.oldValue=c[h].value);let v=g.dir[a];v&&($n(),mn(v,o,8,[e.el,g,e,i]),Hn())}}function Au(e,i){if(We){let o=We.provides;const a=We.parent&&We.parent.provides;a===o&&(o=We.provides=Object.create(a)),o[e]=i}}function eo(e,i,o=!1){const a=dc();if(a||xs){let l=xs?xs._context.provides:a?a.parent==null||a.ce?a.vnode.appContext&&a.vnode.appContext.provides:a.parent.provides:void 0;if(l&&e in l)return l[e];if(arguments.length>1)return o&&Lt(i)?i.call(a&&a.proxy):i}}const $d=Symbol.for("v-scx"),Hd=()=>eo($d);function Je(e,i,o){return Iu(e,i,o)}function Iu(e,i,o=ie){const{immediate:a,deep:l,flush:c,once:h}=o,g=Ce({},o),v=i&&a||!i&&c!=="post";let P;if(fo){if(c==="sync"){const F=Hd();P=F.__watcherHandles||(F.__watcherHandles=[])}else if(!v){const F=()=>{};return F.stop=Zn,F.resume=Zn,F.pause=Zn,F}}const k=We;g.call=(F,nt,q)=>mn(F,k,nt,q);let O=!1;c==="post"?g.scheduler=F=>{Ye(F,k&&k.suspense)}:c!=="sync"&&(O=!0,g.scheduler=(F,nt)=>{nt?F():Ca(F)}),g.augmentJob=F=>{i&&(F.flags|=4),O&&(F.flags|=2,k&&(F.id=k.uid,F.i=k))};const $=Rd(e,i,g);return fo&&(P?P.push($):v&&$()),$}function Ud(e,i,o){const a=this.proxy,l=pe(e)?e.includes(".")?Nu(a,e):()=>a[e]:e.bind(a,a);let c;Lt(i)?c=i:(c=i.handler,o=i);const h=go(this),g=Iu(l,c.bind(a),o);return h(),g}function Nu(e,i){const o=i.split(".");return()=>{let a=e;for(let l=0;l<o.length&&a;l++)a=a[o[l]];return a}}const jd=Symbol("_vte"),Bu=e=>e.__isTeleport,fn=Symbol("_leaveCb"),Ws=Symbol("_enterCb");function Wd(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Ls(()=>{e.isMounted=!0}),_r(()=>{e.isUnmounting=!0}),e}const cn=[Function,Array],Du={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:cn,onEnter:cn,onAfterEnter:cn,onEnterCancelled:cn,onBeforeLeave:cn,onLeave:cn,onAfterLeave:cn,onLeaveCancelled:cn,onBeforeAppear:cn,onAppear:cn,onAfterAppear:cn,onAppearCancelled:cn},Ru=e=>{const i=e.subTree;return i.component?Ru(i.component):i},Kd={name:"BaseTransition",props:Du,setup(e,{slots:i}){const o=dc(),a=Wd();return()=>{const l=i.default&&Zu(i.default(),!0),c=l&&l.length?Fu(l):o.subTree?V():void 0;if(!c)return;const h=Gt(e),{mode:g}=h;if(a.isLeaving)return Yr(c);const v=ll(c);if(!v)return Yr(c);let P=la(v,h,a,o,O=>P=O);v.type!==Be&&lo(v,P);let k=o.subTree&&ll(o.subTree);if(k&&k.type!==Be&&!Wi(k,v)&&Ru(o).type!==Be){let O=la(k,h,a,o);if(lo(k,O),g==="out-in"&&v.type!==Be)return a.isLeaving=!0,O.afterLeave=()=>{a.isLea
Line truncated
|
||||
* @vue/runtime-dom v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/let pa;const xl=typeof window<"u"&&window.trustedTypes;if(xl)try{pa=xl.createPolicy("vue",{createHTML:e=>e})}catch{}const pc=pa?e=>pa.createHTML(e):e=>e,qf="http://www.w3.org/2000/svg",Gf="http://www.w3.org/1998/Math/MathML",ni=typeof document<"u"?document:null,wl=ni&&ni.createElement("template"),Yf={insert:(e,i,o)=>{i.insertBefore(e,o||null)},remove:e=>{const i=e.parentNode;i&&i.removeChild(e)},createElement:(e,i,o,a)=>{const l=i==="svg"?ni.createElementNS(qf,e):i==="mathml"?ni.createElementNS(Gf,e):o?ni.createElement(e,{is:o}):ni.createElement(e);return e==="select"&&a&&a.multiple!=null&&l.setAttribute("multiple",a.multiple),l},createText:e=>ni.createTextNode(e),createComment:e=>ni.createComment(e),setText:(e,i)=>{e.nodeValue=i},setElementText:(e,i)=>{e.textContent=i},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ni.querySelector(e),setScopeId(e,i){e.setAttribute(i,"")},insertStaticContent(e,i,o,a,l,c){const h=o?o.previousSibling:i.lastChild;if(l&&(l===c||l.nextSibling))for(;i.insertBefore(l.cloneNode(!0),o),!(l===c||!(l=l.nextSibling)););else{wl.innerHTML=pc(a==="svg"?`<svg>${e}</svg>`:a==="mathml"?`<math>${e}</math>`:e);const g=wl.content;if(a==="svg"||a==="mathml"){const v=g.firstChild;for(;v.firstChild;)g.appendChild(v.firstChild);g.removeChild(v)}i.insertBefore(g,o)}return[h?h.nextSibling:i.firstChild,o?o.previousSibling:i.lastChild]}},xi="transition",Ks="animation",ho=Symbol("_vtc"),mc={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Jf=Ce({},Du,mc),Xf=e=>(e.displayName="Transition",e.props=Jf,e),Qf=Xf((e,{slots:i})=>Wf(qd,th(e),i)),Hi=(e,i=[])=>{pt(e)?e.forEach(o=>o(...i)):e&&e(...i)},kl=e=>e?pt(e)?e.some(i=>i.length>1):e.length>1:!1;function th(e){const i={};for(const G in e)G in mc||(i[G]=e[G]);if(e.css===!1)return i;const{name:o="v",type:a,duration:l,enterFromClass:c=`${o}-enter-from`,enterActiveClass:h=`${o}-enter-active`,enterToClass:g=`${o}-enter-to`,appearFromClass:v=c,appearActiveClass:P=h,appearToClass:k=g,leaveFromClass:O=`${o}-leave-from`,leaveActiveClass:$=`${o}-leave-active`,leaveToClass:F=`${o}-leave-to`}=e,nt=eh(l),q=nt&&nt[0],At=nt&&nt[1],{onBeforeEnter:Dt,onEnter:bt,onEnterCancelled:ut,onLeave:X,onLeaveCancelled:ft,onBeforeAppear:jt=Dt,onAppear:de=bt,onAppearCancelled:me=ut}=i,vt=(G,st,It,Wt)=>{G._enterCancelled=Wt,Ui(G,st?k:g),Ui(G,st?P:h),It&&It()},Ft=(G,st)=>{G._isLeaving=!1,Ui(G,O),Ui(G,F),Ui(G,$),st&&st()},Tt=G=>(st,It)=>{const Wt=G?de:bt,kt=()=>vt(st,G,It);Hi(Wt,[st,kt]),Sl(()=>{Ui(st,G?v:c),ei(st,G?k:g),kl(Wt)||Pl(st,a,q,kt)})};return Ce(i,{onBeforeEnter(G){Hi(Dt,[G]),ei(G,c),ei(G,h)},onBeforeAppear(G){Hi(jt,[G]),ei(G,v),ei(G,P)},onEnter:Tt(!1),onAppear:Tt(!0),onLeave(G,st){G._isLeaving=!0;const It=()=>Ft(G,st);ei(G,O),G._enterCancelled?(ei(G,$),Ml(G)):(Ml(G),ei(G,$)),Sl(()=>{G._isLeaving&&(Ui(G,O),ei(G,F),kl(X)||Pl(G,a,At,It))}),Hi(X,[G,It])},onEnterCancelled(G){vt(G,!1,void 0,!0),Hi(ut,[G])},onAppearCancelled(G){vt(G,!0,void 0,!0),Hi(me,[G])},onLeaveCancelled(G){Ft(G),Hi(ft,[G])}})}function eh(e){if(e==null)return null;if(Qt(e))return[ta(e.enter),ta(e.leave)];{const i=ta(e);return[i,i]}}function ta(e){return id(e)}function ei(e,i){i.split(/\s+/).forEach(o=>o&&e.classList.add(o)),(e[ho]||(e[ho]=new Set)).add(i)}function Ui(e,i){i.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const o=e[ho];o&&(o.delete(i),o.size||(e[ho]=void 0))}function Sl(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let nh=0;function Pl(e,i,o,a){const l=e._endId=++nh,c=()=>{l===e._endId&&a()};if(o!=null)return setTimeout(c,o);const{type:h,timeout:g,propCount:v}=ih(e,i);if(!h)return a();const P=h+"end";let k=0;const O=()=>{e.removeEventListener(P,$),c()},$=F=>{F.target===e&&++k>=v&&O()};setTimeout(()=>{k<v&&O()},g+1),e.addEventListener(P,$)}function ih(e,i){const o=window.getComputedStyle(e),a=nt=>(o[nt]||"").split(", "),l=a(`${xi}Delay`),c=a(`${xi}Duration`),h=Tl(l,c),g=a(`${Ks}Delay`),v=a(`${Ks}Duration`),P=Tl(g,v);let k=null,O=0,$=0;i===xi?h>0&&(k=xi,O=h,$=c.length):i===Ks?P>0&&(k=Ks,O=P,$=v.length):(O=Math.max(h,P),k=O>0?h>P?xi:Ks:null,$=k?k===xi?c.length:v.length:0);const F=k===xi&&/\b(?:transform|all)(?:,|$)/.test(a(`${xi}Property`).toString());return{type:k,timeout:O,propCount:$,hasTransform:F}}function Tl(e,i){for(;e.length<i.length;)e=e.concat(e);return Math.max(...i.map((o,a)=>Ll(o)+Ll(e[a])))}function Ll(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Ml(e){return(e?e.ownerDocument:document).body.offsetHeight}function sh(e,i,o){const a=e[ho];a&&(i=(i?[i,...a]:[...a]).join(" ")),i==null?e.removeAttribute("class"):o?e.setAttribute("class",i):e.className=i}const or=Symbol("_vod"),gc=Symbol("_vsh"),oh={name:"show",beforeMount(e,{value:i},{transition:o}){e[or]=e.style.display==="none"?"":e.sty
Line truncated
|
||||
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
|
||||
* (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
|
||||
*/var Lp=Js.exports,Jl;function Mp(){return Jl||(Jl=1,(function(e,i){(function(o,a){a(i)})(Lp,(function(o){var a="1.9.4";function l(t){var n,s,r,u;for(s=1,r=arguments.length;s<r;s++){u=arguments[s];for(n in u)t[n]=u[n]}return t}var c=Object.create||(function(){function t(){}return function(n){return t.prototype=n,new t}})();function h(t,n){var s=Array.prototype.slice;if(t.bind)return t.bind.apply(t,s.call(arguments,1));var r=s.call(arguments,2);return function(){return t.apply(n,r.length?r.concat(s.call(arguments)):arguments)}}var g=0;function v(t){return"_leaflet_id"in t||(t._leaflet_id=++g),t._leaflet_id}function P(t,n,s){var r,u,p,S;return S=function(){r=!1,u&&(p.apply(s,u),u=!1)},p=function(){r?u=arguments:(t.apply(s,arguments),setTimeout(S,n),r=!0)},p}function k(t,n,s){var r=n[1],u=n[0],p=r-u;return t===r&&s?t:((t-u)%p+p)%p+u}function O(){return!1}function $(t,n){if(n===!1)return t;var s=Math.pow(10,n===void 0?6:n);return Math.round(t*s)/s}function F(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function nt(t){return F(t).split(/\s+/)}function q(t,n){Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?c(t.options):{});for(var s in n)t.options[s]=n[s];return t.options}function At(t,n,s){var r=[];for(var u in t)r.push(encodeURIComponent(s?u.toUpperCase():u)+"="+encodeURIComponent(t[u]));return(!n||n.indexOf("?")===-1?"?":"&")+r.join("&")}var Dt=/\{ *([\w_ -]+) *\}/g;function bt(t,n){return t.replace(Dt,function(s,r){var u=n[r];if(u===void 0)throw new Error("No value provided for variable "+s);return typeof u=="function"&&(u=u(n)),u})}var ut=Array.isArray||function(t){return Object.prototype.toString.call(t)==="[object Array]"};function X(t,n){for(var s=0;s<t.length;s++)if(t[s]===n)return s;return-1}var ft="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function jt(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var de=0;function me(t){var n=+new Date,s=Math.max(0,16-(n-de));return de=n+s,window.setTimeout(t,s)}var vt=window.requestAnimationFrame||jt("RequestAnimationFrame")||me,Ft=window.cancelAnimationFrame||jt("CancelAnimationFrame")||jt("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function Tt(t,n,s){if(s&&vt===me)t.call(n);else return vt.call(window,h(t,n))}function G(t){t&&Ft.call(window,t)}var st={__proto__:null,extend:l,create:c,bind:h,get lastId(){return g},stamp:v,throttle:P,wrapNum:k,falseFn:O,formatNum:$,trim:F,splitWords:nt,setOptions:q,getParamString:At,template:bt,isArray:ut,indexOf:X,emptyImageUrl:ft,requestFn:vt,cancelFn:Ft,requestAnimFrame:Tt,cancelAnimFrame:G};function It(){}It.extend=function(t){var n=function(){q(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()},s=n.__super__=this.prototype,r=c(s);r.constructor=n,n.prototype=r;for(var u in this)Object.prototype.hasOwnProperty.call(this,u)&&u!=="prototype"&&u!=="__super__"&&(n[u]=this[u]);return t.statics&&l(n,t.statics),t.includes&&(Wt(t.includes),l.apply(null,[r].concat(t.includes))),l(r,t),delete r.statics,delete r.includes,r.options&&(r.options=s.options?c(s.options):{},l(r.options,t.options)),r._initHooks=[],r.callInitHooks=function(){if(!this._initHooksCalled){s.callInitHooks&&s.callInitHooks.call(this),this._initHooksCalled=!0;for(var p=0,S=r._initHooks.length;p<S;p++)r._initHooks[p].call(this)}},n},It.include=function(t){var n=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=n,this.mergeOptions(t.options)),this},It.mergeOptions=function(t){return l(this.prototype.options,t),this},It.addInitHook=function(t){var n=Array.prototype.slice.call(arguments,1),s=typeof t=="function"?t:function(){this[t].apply(this,n)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(s),this};function Wt(t){if(!(typeof L>"u"||!L||!L.Mixin)){t=ut(t)?t:[t];for(var n=0;n<t.length;n++)t[n]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",new Error().stack)}}var kt={on:function(t,n,s){if(typeof t=="object")for(var r in t)this._on(r,t[r],n);else{t=nt(t);for(var u=0,p=t.length;u<p;u++)this._on(t[u],n,s)}return this},off:function(t,n,s){if(!arguments.length)delete this._events;else if(typeof t=="object")for(var r in t)this._off(r,t[r],n);else{t=nt(t);for(var u=arguments.length===1,p=0,S=t.length;p<S;p++)u?this._off(t[p]):this._off(t[p],n,s)}return this},_on:function(t,n,s,r){if(typeof n!="function"){console.warn("wrong listener type: "+typeof n);return}if(this._listens(t,n,s)===!1){s===this&&(s=void 0);var u={fn:n,ctx:s};r&&(u.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(u)}},_off:function(t,n,s){var r,u,p;if(this._events&&(r=this._events[t],!!r)){if(arguments.length===1){if(this._firingCount)for(u=0,p=r.length;u<p;u++)r[u].fn=O;delete this._events[t];return}if(typeof n!="function"){console.warn("wrong listener typLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
:root,[data-theme=light]{--navy-950: #0B1730;--navy-900: #0F1E3D;--navy-800: #1B2E52;--navy-700: #26406E;--blue-50: #EAF1FE;--blue-100: #D6E3FD;--blue-200: #B4CDFA;--blue-300: #8FB4F6;--blue-400: #5B93F5;--blue-500: #3D7BF0;--blue-600: #2B62CC;--blue-700: #1F4CA0;--slate-0: #FFFFFF;--slate-50: #F6F7F9;--slate-100: #EEF0F3;--slate-150: #E6E9EE;--slate-200: #DCE0E7;--slate-300: #C5CCD7;--slate-400: #97A1B0;--slate-500: #6B7688;--slate-600: #4C5566;--slate-700: #333B4A;--slate-800: #1E2635;--slate-900: #131A28;--slate-950: #0B111C;--steel: #5A6B85;--green-500: #1F8A5B;--green-100: #DCF1E7;--green-600:#177049;--amber-500: #D9852B;--amber-100: #FBEBD5;--amber-600:#B86C1B;--red-500: #D64545;--red-100: #FBE0E0;--red-600: #B83232;--bg-app: var(--slate-100);--bg-subtle: var(--slate-50);--surface: var(--slate-0);--surface-2: var(--slate-50);--surface-inset: var(--slate-100);--border: var(--slate-200);--border-strong: var(--slate-300);--border-subtle: var(--slate-150);--text-primary: var(--navy-900);--text-secondary:var(--steel);--text-tertiary: var(--slate-400);--text-inverse: var(--slate-0);--text-on-accent:#FFFFFF;--accent: var(--blue-500);--accent-hover: var(--blue-600);--accent-active: var(--blue-700);--accent-soft: var(--blue-50);--accent-soft-fg:var(--blue-700);--focus-ring: color-mix(in srgb, var(--blue-500) 45%, transparent);--success: var(--green-500);--success-soft: var(--green-100);--success-fg: var(--green-600);--warning: var(--amber-500);--warning-soft: var(--amber-100);--warning-fg: var(--amber-600);--danger: var(--red-500);--danger-soft: var(--red-100);--danger-fg: var(--red-600);--overlay: color-mix(in srgb, var(--navy-950) 55%, transparent);--font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono: "Space Mono", ui-monospace, "SF Mono", "JetBrains Mono", monospace;--radius-xs: 4px;--radius-sm: 6px;--radius-md: 10px;--radius-lg: 14px;--radius-xl: 20px;--radius-pill: 999px;--shadow-xs: 0 1px 2px rgba(15,30,61,.06);--shadow-sm: 0 1px 2px rgba(15,30,61,.06), 0 1px 3px rgba(15,30,61,.04);--shadow-md: 0 2px 4px rgba(15,30,61,.06), 0 6px 16px rgba(15,30,61,.08);--shadow-lg: 0 8px 24px rgba(15,30,61,.1), 0 2px 6px rgba(15,30,61,.06);--ease-standard: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--dur-fast: .12s;--dur-base: .2s;color-scheme:light}[data-theme=dark]{--bg-app: var(--navy-950);--bg-subtle: var(--slate-950);--surface: #10203F;--surface-2: #142748;--surface-inset: var(--navy-950);--border: color-mix(in srgb, #FFFFFF 10%, transparent);--border-strong: color-mix(in srgb, #FFFFFF 18%, transparent);--border-subtle: color-mix(in srgb, #FFFFFF 6%, transparent);--text-primary: #F4F7FC;--text-secondary:#8FA0BE;--text-tertiary: #5E6E8C;--text-inverse: var(--navy-900);--text-on-accent:#FFFFFF;--accent: var(--blue-400);--accent-hover: var(--blue-300);--accent-active: var(--blue-200);--accent-soft: color-mix(in srgb, var(--blue-500) 18%, transparent);--accent-soft-fg:var(--blue-300);--focus-ring: color-mix(in srgb, var(--blue-400) 55%, transparent);--success:var(--green-500);--success-soft: color-mix(in srgb, var(--green-500) 22%, transparent);--success-fg:#5FD3A0;--warning:var(--amber-500);--warning-soft: color-mix(in srgb, var(--amber-500) 22%, transparent);--warning-fg:#F0B26A;--danger: var(--red-500);--danger-soft: color-mix(in srgb, var(--red-500) 22%, transparent);--danger-fg: #F08A8A;--overlay: color-mix(in srgb, #000000 62%, transparent);--shadow-xs: 0 1px 2px rgba(0,0,0,.35);--shadow-sm: 0 1px 3px rgba(0,0,0,.4);--shadow-md: 0 4px 12px rgba(0,0,0,.45);--shadow-lg: 0 12px 32px rgba(0,0,0,.5);color-scheme:dark}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-Line truncated
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="11" fill="#0F1E3D" />
|
||||
<g stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
||||
<polyline points="10,30 21,17 32,30" stroke="#3D7BF0" />
|
||||
<polyline points="16,33 27,20 38,33" stroke="#F4F7FC" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
// Apply saved theme + font size before first paint (light is the brand default).
|
||||
(function () {
|
||||
var GROUND = { light: '#EEF0F3', dark: '#0B1730' }
|
||||
var FONT = { sm: 15, md: 16, lg: 18 }
|
||||
var html = document.documentElement
|
||||
var mode = 'light', prefs = {}
|
||||
try {
|
||||
mode = localStorage.getItem('pv_theme') || 'light'
|
||||
} catch (e) {}
|
||||
try {
|
||||
prefs = JSON.parse(localStorage.getItem('pv_prefs') || '{}') || {}
|
||||
} catch (e) {}
|
||||
var t = mode
|
||||
if (mode === 'system') {
|
||||
t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
html.setAttribute('data-theme', t)
|
||||
html.style.backgroundColor = GROUND[t] || GROUND.light
|
||||
html.style.fontSize = (FONT[prefs.fontSize] || 16) + 'px'
|
||||
if (prefs.reduceMotion) html.classList.add('reduce-motion')
|
||||
})()
|
||||
</script>
|
||||
<title>PilotVault — Control Panel</title>
|
||||
<script type="module" crossorigin src="./assets/index-BldP9Pra.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DBe0h801.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
module pilotvault/webapp
|
||||
|
||||
go 1.24
|
||||
|
||||
require github.com/gorilla/websocket v1.5.3
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@@ -0,0 +1,140 @@
|
||||
// Command webapp is the PilotVault control-panel BFF. It serves the embedded
|
||||
// Vue single-page app and proxies /bff/* to the API Server, so the browser only
|
||||
// ever talks to this server (same-origin). The API Server is the gateway to
|
||||
// PocketBase and to the live Fly App data; the browser never contacts either
|
||||
// directly.
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed all:dist
|
||||
var distFS embed.FS
|
||||
|
||||
// App is the control-panel BFF. It talks ONLY to the API Server; the API Server
|
||||
// is the gateway to PocketBase and to the live Fly App data. The browser never
|
||||
// contacts PocketBase or the API Server directly.
|
||||
type App struct {
|
||||
apiBase string // e.g. http://localhost:8080
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
|
||||
log.SetPrefix("[web] ")
|
||||
|
||||
loadDotEnv(".env")
|
||||
addr := envOr("ADDR", ":8090")
|
||||
app := &App{apiBase: strings.TrimRight(envOr("API_BASE", "http://localhost:8080"), "/")}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// Auth (proxied to API Server → PocketBase)
|
||||
mux.HandleFunc("POST /bff/login", app.handleLogin)
|
||||
mux.HandleFunc("POST /bff/logout", app.handleLogout)
|
||||
mux.HandleFunc("GET /bff/me", app.handleMe)
|
||||
mux.HandleFunc("GET /bff/config", app.handleConfig)
|
||||
// Data (proxied to the API Server; gated by session cookie)
|
||||
mux.HandleFunc("GET /bff/devices", app.requireAuth(app.handleDevices))
|
||||
mux.HandleFunc("GET /bff/devices/{id}/track", app.requireAuth(app.handleTrack))
|
||||
mux.HandleFunc("POST /bff/devices/{id}/command", app.requireAuth(app.handleCommand))
|
||||
// User preferences (persisted in PocketBase via the API Server; needs the token)
|
||||
mux.HandleFunc("GET /bff/preferences", app.requireAuth(app.handleGetPrefs))
|
||||
mux.HandleFunc("PUT /bff/preferences", app.requireAuth(app.handlePutPrefs))
|
||||
// Plugin integrations (OpenSky) — per-user/per-org settings via the API Server
|
||||
mux.HandleFunc("GET /bff/integrations/opensky", app.requireAuth(app.handleGetOpenSky))
|
||||
mux.HandleFunc("PUT /bff/integrations/opensky", app.requireAuth(app.handlePutOpenSky))
|
||||
mux.HandleFunc("POST /bff/integrations/opensky/health", app.requireAuth(app.handleOpenSkyHealth))
|
||||
// Plugin integrations (File transfer: FTP/SFTP) — per-user/per-org settings
|
||||
mux.HandleFunc("GET /bff/integrations/filetransfer", app.requireAuth(app.handleGetFileTransfer))
|
||||
mux.HandleFunc("PUT /bff/integrations/filetransfer", app.requireAuth(app.handlePutFileTransfer))
|
||||
mux.HandleFunc("POST /bff/integrations/filetransfer/health", app.requireAuth(app.handleFileTransferHealth))
|
||||
// Plugin integrations (Local storage: host filesystem) — per-user/per-org isolated folders
|
||||
mux.HandleFunc("GET /bff/integrations/localstorage", app.requireAuth(app.handleGetLocalStorage))
|
||||
mux.HandleFunc("PUT /bff/integrations/localstorage", app.requireAuth(app.handlePutLocalStorage))
|
||||
mux.HandleFunc("POST /bff/integrations/localstorage/health", app.requireAuth(app.handleLocalStorageHealth))
|
||||
// Plugin integrations (WebDAV) — per-user/per-org settings
|
||||
mux.HandleFunc("GET /bff/integrations/webdav", app.requireAuth(app.handleGetWebDav))
|
||||
mux.HandleFunc("PUT /bff/integrations/webdav", app.requireAuth(app.handlePutWebDav))
|
||||
mux.HandleFunc("POST /bff/integrations/webdav/health", app.requireAuth(app.handleWebDavHealth))
|
||||
// User-management (role + org scoping enforced by the API Server)
|
||||
mux.HandleFunc("GET /bff/users", app.requireAuth(app.handleListUsers))
|
||||
mux.HandleFunc("POST /bff/users", app.requireAuth(app.handleCreateUser))
|
||||
mux.HandleFunc("PATCH /bff/users/{id}", app.requireAuth(app.handleUpdateUser))
|
||||
mux.HandleFunc("DELETE /bff/users/{id}", app.requireAuth(app.handleDeleteUser))
|
||||
// Organizations (create/edit/delete are superadmin-only upstream)
|
||||
mux.HandleFunc("GET /bff/orgs", app.requireAuth(app.handleListOrgs))
|
||||
mux.HandleFunc("POST /bff/orgs", app.requireAuth(app.handleCreateOrg))
|
||||
mux.HandleFunc("PATCH /bff/orgs/{id}", app.requireAuth(app.handleUpdateOrg))
|
||||
mux.HandleFunc("DELETE /bff/orgs/{id}", app.requireAuth(app.handleDeleteOrg))
|
||||
mux.HandleFunc("GET /bff/ws", app.handleWS)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "api": app.apiBase})
|
||||
})
|
||||
|
||||
dist, err := fs.Sub(distFS, "dist")
|
||||
if err != nil {
|
||||
log.Fatalf("embed dist: %v", err)
|
||||
}
|
||||
mux.Handle("/", noCache(http.FileServer(http.FS(dist))))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: logRequests(mux),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
log.Printf("Web App (control panel) on http://localhost%s → API Server %s", addr, app.apiBase)
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
func envOr(k, d string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// loadDotEnv loads KEY=VALUE pairs from a .env file into the process env if they
|
||||
// are not already set. It is intentionally minimal (no quoting rules beyond
|
||||
// trimming surrounding quotes).
|
||||
func loadDotEnv(path string) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
key, val, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
val = strings.Trim(strings.TrimSpace(val), `"'`)
|
||||
if _, exists := os.LookupEnv(key); !exists {
|
||||
_ = os.Setenv(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func noCache(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store, must-revalidate")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func logRequests(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond))
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user