Files
SimpleArmaAdmin/cmd/gateway/main.go

460 lines
13 KiB
Go

package main
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
"SimpleArmaAdmin/internal/auth"
internal_nats "SimpleArmaAdmin/internal/nats"
"SimpleArmaAdmin/internal/webauthn"
"github.com/gorilla/websocket"
"github.com/nats-io/nats.go"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
type Gateway struct {
natsClient *internal_nats.Client
dashboards map[*websocket.Conn]bool
mu sync.Mutex
}
func (g *Gateway) handleWebSocket(w http.ResponseWriter, r *http.Request) {
role := r.URL.Query().Get("role")
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrade error: %v", err)
return
}
if role == "dashboard" {
g.mu.Lock()
g.dashboards[conn] = true
g.mu.Unlock()
}
defer func() {
if role == "dashboard" {
g.mu.Lock()
delete(g.dashboards, conn)
g.mu.Unlock()
}
conn.Close()
}()
communityID := "comm-123-abc"
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
break
}
if role != "dashboard" {
if messageType == websocket.BinaryMessage {
g.natsClient.PublishLog(context.Background(), communityID, "live", p)
} else {
g.natsClient.Conn.Publish("telemetry."+communityID, p)
}
}
}
}
func (g *Gateway) broadcast(messageType int, data []byte) {
g.mu.Lock()
defer g.mu.Unlock()
for client := range g.dashboards {
err := client.WriteMessage(messageType, data)
if err != nil {
client.Close()
delete(g.dashboards, client)
}
}
}
// ============================================================================
// REGISTRATION HANDLERS
// ============================================================================
// handleRegister - Password-based registration
func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
CommunityName string `json:"communityName"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// Validate password strength
if err := auth.ValidatePasswordStrength(req.Password); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Hash password (validates it can be hashed)
if _, err := auth.HashPassword(req.Password); err != nil {
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
return
}
// TODO: Store in database
// - Create community
// - Create admin_user with password_hash (use the hash above)
// - Generate master key and wrap it
userID := "user-" + req.Username
communityID := "comm-" + req.Username
// Generate session token
token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour)
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"token": token,
"userId": userID,
"communityId": communityID,
"username": req.Username,
"masterKey": "this-is-a-32-byte-master-key-xyz", // TODO: Generate real key
})
}
// handleRegisterPasskeyBegin - Start Passkey registration
func (g *Gateway) handleRegisterPasskeyBegin(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
options, err := webauthn.CreateRegistrationOptions(
"user-"+req.Username,
req.Username,
req.DisplayName,
"ArmaAdmin Zero-Knowledge Cloud",
"localhost", // TODO: Load from env
)
if err != nil {
http.Error(w, "Failed to create options", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(options)
}
// handleRegisterPasskeyFinish - Complete Passkey registration
func (g *Gateway) handleRegisterPasskeyFinish(w http.ResponseWriter, r *http.Request) {
// TODO: Verify credential and store in database
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"message": "Passkey registered successfully",
})
}
// ============================================================================
// LOGIN HANDLERS
// ============================================================================
// handleLoginPassword - Password-based login
func (g *Gateway) handleLoginPassword(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// TODO: Fetch user from database and verify password
// For DEMO mode: Accept any password (bypass authentication)
log.Printf("[DEMO] Login attempt for user: %s (auto-accepting)", req.Username)
userID := "user-" + req.Username
communityID := "comm-" + req.Username
// Generate JWT
token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour)
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"token": token,
"userId": userID,
"communityId": communityID,
"username": req.Username,
"masterKey": "this-is-a-32-byte-master-key-xyz",
})
}
// handleLoginPasskeyBegin - Start Passkey authentication
func (g *Gateway) handleLoginPasskeyBegin(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// TODO: Fetch user's credentials from DB
options, err := webauthn.CreateAuthenticationOptions(
"localhost", // TODO: Load from env
[]string{}, // TODO: Fetch credential IDs
)
if err != nil {
http.Error(w, "Failed to create options", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(options)
}
// handleLoginPasskeyFinish - Complete Passkey authentication
func (g *Gateway) handleLoginPasskeyFinish(w http.ResponseWriter, r *http.Request) {
// TODO: Verify signature and create session
userID := "user-demo"
communityID := "comm-demo"
username := "demo"
token, err := auth.GenerateJWT(userID, communityID, username, 7*24*time.Hour)
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"token": token,
"userId": userID,
"communityId": communityID,
"username": username,
"masterKey": "this-is-a-32-byte-master-key-xyz",
})
}
// ============================================================================
// SESSION MANAGEMENT
// ============================================================================
// handleLogout - Invalidate session
func (g *Gateway) handleLogout(w http.ResponseWriter, r *http.Request) {
// Extract token from Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "Missing token", http.StatusUnauthorized)
return
}
// TODO: Invalidate session in database
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "logged out"})
}
// handleMe - Get current user info
func (g *Gateway) handleMe(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "Missing token", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := auth.VerifyJWT(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"userId": claims.UserID,
"username": claims.Username,
"communityId": claims.CommunityID,
})
}
// Player Search Handler (Blind Index)
func (g *Gateway) handlePlayerSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "Missing query parameter", http.StatusBadRequest)
return
}
// TODO: Search using blind index
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"results": []map[string]string{},
})
}
// DSGVO Export Handler
func (g *Gateway) handleDSGVOExport(w http.ResponseWriter, r *http.Request) {
playerID := r.URL.Query().Get("playerId")
if playerID == "" {
http.Error(w, "Missing playerId", http.StatusBadRequest)
return
}
// TODO: Fetch all encrypted logs for this player via blind index
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Disposition", "attachment; filename=player-data.json")
json.NewEncoder(w).Encode(map[string]interface{}{
"playerId": playerID,
"exportDate": "2026-04-30",
"logs": []interface{}{},
})
}
// DSGVO Delete Handler
func (g *Gateway) handleDSGVODelete(w http.ResponseWriter, r *http.Request) {
var req struct {
PlayerID string `json:"playerId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// TODO: Delete all records matching the blind index
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
}
// Shared Hosting Ingestion Endpoint
func (g *Gateway) handleIngest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
apiKey := r.Header.Get("X-API-Key")
if apiKey == "" {
http.Error(w, "Missing API key", http.StatusUnauthorized)
return
}
// TODO: Validate API key against database
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
// Forward to NATS for storage
communityID := "comm-123-abc" // TODO: Get from API key
g.natsClient.PublishLog(context.Background(), communityID, "api", body)
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("OK"))
}
func main() {
natsURL := os.Getenv("NATS_URL")
if natsURL == "" {
natsURL = "nats://localhost:4222"
}
nc, err := internal_nats.Connect(natsURL)
if err != nil {
log.Fatalf("NATS error: %v", err)
}
defer nc.Close()
nc.SetupStream(context.Background(), "LOGS", []string{"logs.>"})
gateway := &Gateway{
natsClient: nc,
dashboards: make(map[*websocket.Conn]bool),
}
// Listen to NATS and push to Dashboards ONLY
go func() {
nc.Conn.Subscribe("logs.>", func(m *nats.Msg) {
gateway.broadcast(websocket.BinaryMessage, m.Data)
})
nc.Conn.Subscribe("telemetry.>", func(m *nats.Msg) {
gateway.broadcast(websocket.TextMessage, m.Data)
})
}()
// WebSocket endpoint
http.HandleFunc("/ws", gateway.handleWebSocket)
// Registration endpoints (Password + Passkey)
http.HandleFunc("/api/auth/register", gateway.handleRegister)
http.HandleFunc("/api/auth/register/passkey/begin", gateway.handleRegisterPasskeyBegin)
http.HandleFunc("/api/auth/register/passkey/finish", gateway.handleRegisterPasskeyFinish)
// Login endpoints (Password + Passkey)
http.HandleFunc("/api/auth/login/password", gateway.handleLoginPassword)
http.HandleFunc("/api/auth/login/passkey/begin", gateway.handleLoginPasskeyBegin)
http.HandleFunc("/api/auth/login/passkey/finish", gateway.handleLoginPasskeyFinish)
// Session management
http.HandleFunc("/api/auth/logout", gateway.handleLogout)
http.HandleFunc("/api/auth/me", gateway.handleMe)
// Player roster & DSGVO endpoints
http.HandleFunc("/api/players/search", gateway.handlePlayerSearch)
http.HandleFunc("/api/dsgvo/export", gateway.handleDSGVOExport)
http.HandleFunc("/api/dsgvo/delete", gateway.handleDSGVODelete)
// Shared hosting endpoint (for Nitrado customers)
http.HandleFunc("/api/ingest", gateway.handleIngest)
log.Println("Gateway listening on :8080 (Full MVP)")
log.Fatal(http.ListenAndServe(":8080", nil))
}