fix: stale docs, CORS header gap, hardcoded credentials (items 9-11)
This commit is contained in:
31
README.md
31
README.md
@@ -148,15 +148,28 @@ fusion-deliverables/Vacation/
|
|||||||
|
|
||||||
7. Watch them appear on **Command Deck** and **Fleet Roster**
|
7. Watch them appear on **Command Deck** and **Fleet Roster**
|
||||||
|
|
||||||
### Default login
|
### First-run login
|
||||||
|
|
||||||
| Field | Value |
|
On the very first launch the server generates a random admin password and prints it once to the console:
|
||||||
|-------|--------|
|
|
||||||
| Username | `drjones` |
|
|
||||||
| Password | `czapiewski` |
|
|
||||||
|
|
||||||
Change or add users under **Calibrate → Users** (writes `data/users.json`).
|
```
|
||||||
API routes under `/api/v1/*` require Basic auth; static dashboard and `/ws/dashboard` do not.
|
=== First Run ===
|
||||||
|
Dashboard login: admin / <random-password>
|
||||||
|
Save this — it is not shown again. Change it in Calibrate → Users.
|
||||||
|
=================
|
||||||
|
```
|
||||||
|
|
||||||
|
Subsequent runs load credentials from `data/users.json`. Change or add users under **Calibrate → Users**.
|
||||||
|
|
||||||
|
**API auth summary**
|
||||||
|
|
||||||
|
| Surface | Auth mechanism |
|
||||||
|
|---------|----------------|
|
||||||
|
| `/api/v1/*` REST | HTTP Basic Auth (`Authorization: Basic <base64>`) |
|
||||||
|
| `/ws/dashboard` | `?token=<base64-user:pass>` query parameter |
|
||||||
|
| `/ws/agent` | Fleet-secret `auth` JSON frame on connect |
|
||||||
|
| `/api/v1/agent/*` | `X-Fleet-Secret: <secret>` header (agents only) |
|
||||||
|
| Static SPA + `/api/v1/health` | Open (no auth) |
|
||||||
|
|
||||||
### Output locations
|
### Output locations
|
||||||
|
|
||||||
@@ -223,7 +236,7 @@ crypto miner/
|
|||||||
| GET | `/api/v1/agents` | Fleet list |
|
| GET | `/api/v1/agents` | Fleet list |
|
||||||
| POST | `/api/v1/agents/{id}/command` | Remote action (pause, powershell, …) |
|
| POST | `/api/v1/agents/{id}/command` | Remote action (pause, powershell, …) |
|
||||||
| WS | `/ws/agent` | Worker connection |
|
| WS | `/ws/agent` | Worker connection |
|
||||||
| WS | `/ws/dashboard` | Live dashboard feed |
|
| WS | `/ws/dashboard?token=<base64>` | Live dashboard feed (token = base64 of `user:pass`) |
|
||||||
|
|
||||||
Full route list: `server/internal/api/router.go`
|
Full route list: `server/internal/api/router.go`
|
||||||
|
|
||||||
@@ -279,7 +292,7 @@ Vite proxies `/api` and `/ws` to `localhost:8989`. Run `miner-server.exe` separa
|
|||||||
| Symptom | Likely cause | Fix |
|
| Symptom | Likely cause | Fix |
|
||||||
|---------|----------------|-----|
|
|---------|----------------|-----|
|
||||||
| **Black screen**, empty page | Stale service worker or React/R3F version mismatch | Hard refresh (Ctrl+Shift+R); clear site data for `localhost:8989`; ensure `npm install` + `npm run build` in `server/web`; copy `dist` → `webroot`; restart server |
|
| **Black screen**, empty page | Stale service worker or React/R3F version mismatch | Hard refresh (Ctrl+Shift+R); clear site data for `localhost:8989`; ensure `npm install` + `npm run build` in `server/web`; copy `dist` → `webroot`; restart server |
|
||||||
| Login loop / 401 | Wrong password or `users.json` | Use Calibrate → Users or default `drjones` / `czapiewski` |
|
| Login loop / 401 | Wrong password or missing `users.json` | Check the server console for the first-run password; reset by deleting `data/users.json` and restarting |
|
||||||
| Dashboard builds but server shows placeholder HTML | Missing `server/webroot/index.html` | Run `run.bat` or copy `server/web/dist/*` → `server/webroot/` |
|
| Dashboard builds but server shows placeholder HTML | Missing `server/webroot/index.html` | Run `run.bat` or copy `server/web/dist/*` → `server/webroot/` |
|
||||||
| Forge upload fails | File > 2 GiB | Use paired mode + compress, or embedded for smaller sources |
|
| Forge upload fails | File > 2 GiB | Use paired mode + compress, or embedded for smaller sources |
|
||||||
| Workers never appear | Wrong server URL / firewall | Use LAN IP in Forge; open 8989 on control PC |
|
| Workers never appear | Wrong server URL / firewall | Use LAN IP in Forge; open 8989 on control PC |
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -18,7 +22,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
authUsers = map[string]string{"drjones": "czapiewski"} // default until users.json loads
|
// authUsers is populated from data/users.json on startup. On the very first
|
||||||
|
// run (no users.json) a random password is generated, saved, and printed to
|
||||||
|
// the console — no hard-coded credentials anywhere in the binary.
|
||||||
|
authUsers = map[string]string{}
|
||||||
usersFilePath string
|
usersFilePath string
|
||||||
usersMu sync.RWMutex
|
usersMu sync.RWMutex
|
||||||
|
|
||||||
@@ -50,13 +57,50 @@ func loadUsers(dataDir string) {
|
|||||||
usersFilePath = filepath.Join(dataDir, "users.json")
|
usersFilePath = filepath.Join(dataDir, "users.json")
|
||||||
usersMu.Lock()
|
usersMu.Lock()
|
||||||
defer usersMu.Unlock()
|
defer usersMu.Unlock()
|
||||||
|
|
||||||
data, err := os.ReadFile(usersFilePath)
|
data, err := os.ReadFile(usersFilePath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var loaded map[string]string
|
var loaded map[string]string
|
||||||
if json.Unmarshal(data, &loaded) == nil && len(loaded) > 0 {
|
if json.Unmarshal(data, &loaded) == nil && len(loaded) > 0 {
|
||||||
authUsers = loaded
|
authUsers = loaded
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First run — no users.json (or empty). Generate a random admin password,
|
||||||
|
// save it, and print it clearly so the operator can log in immediately.
|
||||||
|
pw := generateRandomPassword()
|
||||||
|
authUsers = map[string]string{"admin": pw}
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err == nil {
|
||||||
|
d, _ := json.MarshalIndent(authUsers, "", " ")
|
||||||
|
if writeErr := os.WriteFile(usersFilePath, d, 0600); writeErr != nil {
|
||||||
|
log.Printf("[Auth] WARNING: could not save users.json: %v", writeErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
banner := fmt.Sprintf(`
|
||||||
|
╔══════════════════════════════════════════════════╗
|
||||||
|
║ AetherForge — First Run ║
|
||||||
|
║ ║
|
||||||
|
║ Dashboard login ║
|
||||||
|
║ Username : admin ║
|
||||||
|
║ Password : %-34s║
|
||||||
|
║ ║
|
||||||
|
║ Save this — it will not be shown again. ║
|
||||||
|
║ Change it later in Calibrate → Users. ║
|
||||||
|
╚══════════════════════════════════════════════════╝`, pw)
|
||||||
|
log.Print(banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomPassword returns a 20-character hex string suitable for use
|
||||||
|
// as an initial admin password.
|
||||||
|
func generateRandomPassword() string {
|
||||||
|
b := make([]byte, 10)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
// Fallback to a fixed marker so the operator knows something went wrong.
|
||||||
|
return "CHANGE-ME-NOW-12345"
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveUser(username, password string) error {
|
func saveUser(username, password string) error {
|
||||||
@@ -141,7 +185,10 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Use(cors.Handler(cors.Options{
|
r.Use(cors.Handler(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
// X-Fleet-Secret is required by agent-facing endpoints; include it so
|
||||||
|
// browser-based callers (dev tools, custom dashboards) are not blocked
|
||||||
|
// by CORS preflight when sending that header.
|
||||||
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Fleet-Secret"},
|
||||||
AllowCredentials: false,
|
AllowCredentials: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user