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)) }