diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 77072ab..339867d 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -524,7 +524,7 @@ func (g *Gateway) broadcast(messageType int, data []byte) { func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } @@ -535,7 +535,7 @@ func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) { CommunityName string `json:"communityName"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } @@ -552,7 +552,7 @@ func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) { hash, err := auth.HashPassword(req.Password) if err != nil { - http.Error(w, "Failed to hash password", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to hash password") return } @@ -565,19 +565,6 @@ func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) { return } - communityID := uuid.NewString() - communityName := req.CommunityName - if communityName == "" { - communityName = req.Username + "'s Team" - } - - community := &DevCommunity{ - ID: communityID, - Name: communityName, - CreatedAt: time.Now(), - } - g.store.communities[communityID] = community - userID := uuid.NewString() user := &DevUser{ ID: userID, @@ -589,28 +576,23 @@ func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) { g.store.users[userID] = user g.store.usersByName[strings.ToLower(req.Username)] = user - membership := DevMembership{ - UserID: userID, - CommunityID: communityID, - Role: "owner", - } - g.store.memberships = append(g.store.memberships, membership) + log.Printf("[DEV] Registered user: %s (Pending onboarding)", req.Username) - log.Printf("[DEV] Registered user: %s, Created community: %s", req.Username, communityName) - - token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour) + // Token with empty communityId indicates onboarding state + token, err := auth.GenerateJWT(userID, "", req.Username, 7*24*time.Hour) if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to generate token") return } - writeJSON(w, http.StatusOK, map[string]string{ + writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "success", "token": token, "userId": userID, - "communityId": communityID, + "communityId": "", "username": req.Username, - "role": "owner", + "role": "", + "communities": []interface{}{}, "masterKey": "this-is-a-32-byte-master-key-xyz", }) return @@ -618,19 +600,19 @@ func (g *Gateway) handleRegister(w http.ResponseWriter, r *http.Request) { // Non-dev mode fallback userID := "user-" + req.Username - communityID := "comm-" + req.Username - token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour) + token, err := auth.GenerateJWT(userID, "", req.Username, 7*24*time.Hour) if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to generate token") return } - writeJSON(w, http.StatusOK, map[string]string{ + writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "success", "token": token, "userId": userID, - "communityId": communityID, + "communityId": "", "username": req.Username, - "role": "owner", + "role": "", + "communities": []interface{}{}, "masterKey": "this-is-a-32-byte-master-key-xyz", }) } @@ -642,7 +624,7 @@ func (g *Gateway) handleRegisterPasskeyBegin(w http.ResponseWriter, r *http.Requ Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } @@ -654,7 +636,7 @@ func (g *Gateway) handleRegisterPasskeyBegin(w http.ResponseWriter, r *http.Requ "localhost", ) if err != nil { - http.Error(w, "Failed to create options", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to create options") return } @@ -751,13 +733,13 @@ func (g *Gateway) handlePasskeys(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") } } func (g *Gateway) handleAddPasskeyBegin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } claims, err := g.requireAuth(r) @@ -785,7 +767,7 @@ func (g *Gateway) handleAddPasskeyBegin(w http.ResponseWriter, r *http.Request) func (g *Gateway) handleAddPasskeyFinish(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } claims, err := g.requireAuth(r) @@ -861,7 +843,7 @@ func (g *Gateway) handleNodes(w http.ResponseWriter, r *http.Request) { Endpoint string `json:"endpoint"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } req.Name = strings.TrimSpace(req.Name) @@ -917,14 +899,14 @@ func (g *Gateway) handleNodes(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") } } func (g *Gateway) handleNodeWebSocket(w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") if token == "" { - http.Error(w, "Missing token", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Missing token") return } @@ -939,7 +921,7 @@ func (g *Gateway) handleNodeWebSocket(w http.ResponseWriter, r *http.Request) { g.store.mu.RUnlock() if node == nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Invalid token") return } @@ -987,11 +969,11 @@ func (g *Gateway) handleNodeWebSocket(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleDevNodePing(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } if !g.devMode { - http.Error(w, "Not found", http.StatusNotFound) + writeError(w, http.StatusNotFound, "Not found") return } nodeID := r.URL.Query().Get("id") @@ -1043,7 +1025,7 @@ func (g *Gateway) monitorNodes() { func (g *Gateway) handleLoginPassword(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } @@ -1052,7 +1034,7 @@ func (g *Gateway) handleLoginPassword(w http.ResponseWriter, r *http.Request) { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } @@ -1105,7 +1087,7 @@ func (g *Gateway) handleLoginPassword(w http.ResponseWriter, r *http.Request) { token, err := auth.GenerateJWT(user.ID, defaultMembership.CommunityID, user.Username, 7*24*time.Hour) if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to generate token") return } @@ -1128,17 +1110,20 @@ func (g *Gateway) handleLoginPassword(w http.ResponseWriter, r *http.Request) { communityID := "comm-" + req.Username token, err := auth.GenerateJWT(userID, communityID, req.Username, 7*24*time.Hour) if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to generate token") return } - writeJSON(w, http.StatusOK, map[string]string{ + writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "success", "token": token, "userId": userID, "communityId": communityID, "username": req.Username, "role": "owner", - "masterKey": "this-is-a-32-byte-master-key-xyz", + "communities": []map[string]string{ + {"id": communityID, "name": req.Username + "'s Team", "role": "owner"}, + }, + "masterKey": "this-is-a-32-byte-master-key-xyz", }) } @@ -1147,13 +1132,13 @@ func (g *Gateway) handleLoginPasskeyBegin(w http.ResponseWriter, r *http.Request Username string `json:"username"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } options, err := webauthn.CreateAuthenticationOptions("localhost", []string{}) if err != nil { - http.Error(w, "Failed to create options", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to create options") return } @@ -1164,7 +1149,7 @@ func (g *Gateway) handleLoginPasskeyBegin(w http.ResponseWriter, r *http.Request func (g *Gateway) handleLoginPasskeyFinish(w http.ResponseWriter, r *http.Request) { token, err := auth.GenerateJWT("user-demo", "comm-demo", "demo", 7*24*time.Hour) if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to generate token") return } writeJSON(w, http.StatusOK, map[string]string{ @@ -1190,7 +1175,7 @@ func (g *Gateway) handleLogout(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleMe(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1204,8 +1189,14 @@ func (g *Gateway) handleMe(w http.ResponseWriter, r *http.Request) { if g.devMode { g.store.mu.RLock() if user, exists := g.store.users[claims.UserID]; exists { - resp["role"] = user.Role resp["email"] = user.Email + // Find role in current community + for _, m := range g.store.memberships { + if m.UserID == claims.UserID && m.CommunityID == claims.CommunityID { + resp["role"] = m.Role + break + } + } } g.store.mu.RUnlock() } @@ -1216,7 +1207,7 @@ func (g *Gateway) handleMe(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1233,7 +1224,12 @@ func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) { g.store.mu.RLock() if user, exists := g.store.users[claims.UserID]; exists { resp["email"] = user.Email - resp["role"] = user.Role + for _, m := range g.store.memberships { + if m.UserID == claims.UserID && m.CommunityID == claims.CommunityID { + resp["role"] = m.Role + break + } + } } g.store.mu.RUnlock() } @@ -1247,7 +1243,7 @@ func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) { NewPassword string `json:"newPassword"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } @@ -1261,7 +1257,7 @@ func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) { user, exists := g.store.users[claims.UserID] if !exists { - http.Error(w, "User not found", http.StatusNotFound) + writeError(w, http.StatusNotFound, "User not found") return } @@ -1292,15 +1288,23 @@ func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) { } hash, err := auth.HashPassword(req.NewPassword) if err != nil { - http.Error(w, "Failed to hash password", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to hash password") return } user.PasswordHash = hash } - token, err := auth.GenerateJWT(user.ID, user.CommunityID, user.Username, 7*24*time.Hour) + var role string + for _, m := range g.store.memberships { + if m.UserID == user.ID && m.CommunityID == claims.CommunityID { + role = m.Role + break + } + } + + token, err := auth.GenerateJWT(user.ID, claims.CommunityID, user.Username, 7*24*time.Hour) if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "Failed to generate token") return } @@ -1309,12 +1313,12 @@ func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) { "token": token, "username": user.Username, "email": user.Email, - "role": user.Role, - "communityId": user.CommunityID, + "role": role, + "communityId": claims.CommunityID, }) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") } } @@ -1322,10 +1326,117 @@ func (g *Gateway) handleProfile(w http.ResponseWriter, r *http.Request) { // SERVER MANAGEMENT // ============================================================================ +// ============================================================================ +// COMMUNITIES +// ============================================================================ + +func (g *Gateway) handleCreateCommunity(w http.ResponseWriter, r *http.Request) { + claims, err := g.requireAuth(r) + if err != nil { + writeError(w, http.StatusUnauthorized, "Unauthorized") + return + } + + var req struct { + Name string `json:"name"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Invalid request") + return + } + + communityName := strings.TrimSpace(req.Name) + if communityName == "" { + writeError(w, http.StatusBadRequest, "Community name is required") + return + } + + g.store.mu.Lock() + defer g.store.mu.Unlock() + + communityID := uuid.NewString() + community := &DevCommunity{ + ID: communityID, + Name: communityName, + CreatedAt: time.Now(), + } + g.store.communities[communityID] = community + + membership := DevMembership{ + UserID: claims.UserID, + CommunityID: communityID, + Role: "owner", + } + g.store.memberships = append(g.store.memberships, membership) + + log.Printf("[DEV] User %s created community: %s", claims.Username, communityName) + + // Issue a new token for the new community + token, _ := auth.GenerateJWT(claims.UserID, communityID, claims.Username, 7*24*time.Hour) + + writeJSON(w, http.StatusCreated, map[string]interface{}{ + "token": token, + "communityId": communityID, + "name": communityName, + "role": "owner", + }) +} + +func (g *Gateway) handleJoinCommunity(w http.ResponseWriter, r *http.Request) { + claims, err := g.requireAuth(r) + if err != nil { + writeError(w, http.StatusUnauthorized, "Unauthorized") + return + } + + var req struct { + CommunityID string `json:"communityId"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Invalid request") + return + } + + g.store.mu.Lock() + defer g.store.mu.Unlock() + + community, exists := g.store.communities[req.CommunityID] + if !exists { + writeError(w, http.StatusNotFound, "Community not found") + return + } + + // Check if already a member + for _, m := range g.store.memberships { + if m.UserID == claims.UserID && m.CommunityID == req.CommunityID { + writeError(w, http.StatusConflict, "Already a member") + return + } + } + + membership := DevMembership{ + UserID: claims.UserID, + CommunityID: req.CommunityID, + Role: "member", + } + g.store.memberships = append(g.store.memberships, membership) + + log.Printf("[DEV] User %s joined community: %s", claims.Username, community.Name) + + token, _ := auth.GenerateJWT(claims.UserID, req.CommunityID, claims.Username, 7*24*time.Hour) + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "token": token, + "communityId": req.CommunityID, + "name": community.Name, + "role": "member", + }) +} + func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1355,7 +1466,7 @@ func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { EncryptedAutoMessages string `json:"encryptedAutoMessages"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } req.Name = strings.TrimSpace(req.Name) @@ -1407,7 +1518,7 @@ func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { EncryptedAutoMessages string `json:"encryptedAutoMessages"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } @@ -1416,7 +1527,7 @@ func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { server, exists := g.store.servers[req.ID] if !exists || server.CommunityID != claims.CommunityID { - http.Error(w, "Server not found", http.StatusNotFound) + writeError(w, http.StatusNotFound, "Server not found") return } @@ -1446,7 +1557,7 @@ func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: serverID := r.URL.Query().Get("id") if serverID == "" { - http.Error(w, "Missing id", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Missing id") return } @@ -1454,7 +1565,7 @@ func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { server, exists := g.store.servers[serverID] if !exists || server.CommunityID != claims.CommunityID { g.store.mu.Unlock() - http.Error(w, "Not found", http.StatusNotFound) + writeError(w, http.StatusNotFound, "Not found") return } delete(g.store.servers, serverID) @@ -1474,7 +1585,7 @@ func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") } } @@ -1485,7 +1596,7 @@ func (g *Gateway) handleServers(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleUserSearch(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1502,15 +1613,17 @@ func (g *Gateway) handleUserSearch(w http.ResponseWriter, r *http.Request) { } var results []userResult - for _, u := range g.store.users { - if u.CommunityID == claims.CommunityID && u.ID != claims.UserID { - if query == "" || strings.Contains(strings.ToLower(u.Username), query) { - results = append(results, userResult{ - ID: u.ID, - Username: u.Username, - Email: u.Email, - Role: u.Role, - }) + for _, m := range g.store.memberships { + if m.CommunityID == claims.CommunityID && m.UserID != claims.UserID { + if u, ok := g.store.users[m.UserID]; ok { + if query == "" || strings.Contains(strings.ToLower(u.Username), query) { + results = append(results, userResult{ + ID: u.ID, + Username: u.Username, + Email: u.Email, + Role: m.Role, + }) + } } } } @@ -1528,7 +1641,7 @@ func (g *Gateway) handleUserSearch(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handlePermissions(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1557,7 +1670,7 @@ func (g *Gateway) handlePermissions(w http.ResponseWriter, r *http.Request) { Scopes []string `json:"scopes"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } if req.UserID == "" { @@ -1609,21 +1722,21 @@ func (g *Gateway) handlePermissions(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: permID := r.URL.Query().Get("id") if permID == "" { - http.Error(w, "Missing id", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Missing id") return } g.store.mu.Lock() defer g.store.mu.Unlock() perm, exists := g.store.permissions[permID] if !exists || perm.CommunityID != claims.CommunityID { - http.Error(w, "Not found", http.StatusNotFound) + writeError(w, http.StatusNotFound, "Not found") return } delete(g.store.permissions, permID) writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"}) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") } } @@ -1634,7 +1747,7 @@ func (g *Gateway) handlePermissions(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handlePlayerNotes(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1667,7 +1780,7 @@ func (g *Gateway) handlePlayerNotes(w http.ResponseWriter, r *http.Request) { Content string `json:"content"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } @@ -1688,14 +1801,14 @@ func (g *Gateway) handlePlayerNotes(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, note) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") } } func (g *Gateway) handlePlayers(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1728,12 +1841,12 @@ func (g *Gateway) handlePlayers(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleKick(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1742,7 +1855,7 @@ func (g *Gateway) handleKick(w http.ResponseWriter, r *http.Request) { Reason string `json:"reason"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } if req.PlayerNameHash == "" { @@ -1779,12 +1892,12 @@ func (g *Gateway) handleKick(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleBan(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1794,7 +1907,7 @@ func (g *Gateway) handleBan(w http.ResponseWriter, r *http.Request) { DurationMinutes int `json:"durationMinutes"` // 0 = permanent } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } if req.PlayerNameHash == "" { @@ -1837,7 +1950,7 @@ func (g *Gateway) handleBan(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleBans(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1862,14 +1975,14 @@ func (g *Gateway) handleBans(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: banID := r.URL.Query().Get("id") if banID == "" { - http.Error(w, "Missing id", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Missing id") return } g.store.mu.Lock() ban, exists := g.store.bans[banID] if !exists || ban.CommunityID != claims.CommunityID { g.store.mu.Unlock() - http.Error(w, "Not found", http.StatusNotFound) + writeError(w, http.StatusNotFound, "Not found") return } delete(g.store.bans, banID) @@ -1877,7 +1990,7 @@ func (g *Gateway) handleBans(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "unbanned"}) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") } } @@ -1888,7 +2001,7 @@ func (g *Gateway) handleBans(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handlePlayerSearch(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1922,7 +2035,7 @@ func (g *Gateway) handlePlayerSearch(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleLogs(w http.ResponseWriter, r *http.Request) { claims, err := g.requireAuth(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -1944,7 +2057,7 @@ func (g *Gateway) handleLogs(w http.ResponseWriter, r *http.Request) { 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) + writeError(w, http.StatusBadRequest, "Missing playerId") return } w.Header().Set("Content-Type", "application/json") @@ -1961,7 +2074,7 @@ func (g *Gateway) handleDSGVODelete(w http.ResponseWriter, r *http.Request) { PlayerID string `json:"playerId"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Invalid request") return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) @@ -1969,17 +2082,17 @@ func (g *Gateway) handleDSGVODelete(w http.ResponseWriter, r *http.Request) { func (g *Gateway) handleIngest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, http.StatusMethodNotAllowed, "Method not allowed") return } apiKey := r.Header.Get("X-API-Key") if apiKey == "" { - http.Error(w, "Missing API key", http.StatusUnauthorized) + writeError(w, http.StatusUnauthorized, "Missing API key") return } body, err := io.ReadAll(r.Body) if err != nil { - http.Error(w, "Failed to read body", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "Failed to read body") return } communityID := "comm-123-abc" @@ -2052,6 +2165,9 @@ func main() { http.HandleFunc("/api/dev/nodes/ping", gateway.handleDevNodePing) } + http.HandleFunc("/api/communities/create", gateway.handleCreateCommunity) + http.HandleFunc("/api/communities/join", gateway.handleJoinCommunity) + http.HandleFunc("/api/servers", gateway.handleServers) http.HandleFunc("/api/users/search", gateway.handleUserSearch) http.HandleFunc("/api/permissions", gateway.handlePermissions) diff --git a/gateway b/gateway new file mode 100755 index 0000000..738844a Binary files /dev/null and b/gateway differ diff --git a/main b/main new file mode 100755 index 0000000..782315a Binary files /dev/null and b/main differ diff --git a/tmp/build-errors.log b/tmp/build-errors.log deleted file mode 100644 index a5c630d..0000000 --- a/tmp/build-errors.log +++ /dev/null @@ -1 +0,0 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/tmp/gateway b/tmp/gateway index 6875c37..b25cf03 100755 Binary files a/tmp/gateway and b/tmp/gateway differ diff --git a/tmp/worker b/tmp/worker index a0182e3..3f4e851 100755 Binary files a/tmp/worker and b/tmp/worker differ diff --git a/web/dashboard/src/App.tsx b/web/dashboard/src/App.tsx index 4fb0a26..798d371 100644 --- a/web/dashboard/src/App.tsx +++ b/web/dashboard/src/App.tsx @@ -144,28 +144,42 @@ const EditServerDialog: React.FC<{ }> = ({ server, nodes, open, onOpenChange, onSave }) => { const { encrypt, decrypt } = useVault(); const [loading, setLoading] = useState(false); + const [activeTab, setTab] = useState<'infrastructure' | 'automessages'>('infrastructure'); + const [formData, setFormData] = useState({ name: '', description: '', workerId: '', storageId: '', + logPath: '', rconAddress: '', rconPort: 2302, rconPass: '', }); + const [autoMsgs, setAutoMsgs] = useState<{ motd: string, messages: { content: string, interval: number }[] }>({ + motd: '', + messages: [] + }); + useEffect(() => { const loadEncryptedData = async () => { if (server) { let rconDetails = { address: '', port: 2302, pass: '' }; + let autoMsgDetails = { motd: '', messages: [] }; if (server.encryptedRcon) { try { const decrypted = await decrypt(base64ToUint8Array(server.encryptedRcon)); rconDetails = JSON.parse(decrypted); - } catch (e) { - console.error('Failed to decrypt RCON credentials', e); - } + } catch (e) { console.error('Failed to decrypt RCON', e); } + } + + if (server.encryptedAutoMessages) { + try { + const decrypted = await decrypt(base64ToUint8Array(server.encryptedAutoMessages)); + autoMsgDetails = JSON.parse(decrypted); + } catch (e) { console.error('Failed to decrypt AutoMsgs', e); } } setFormData({ @@ -173,10 +187,13 @@ const EditServerDialog: React.FC<{ description: server.description || '', workerId: server.workerId || '', storageId: server.storageId || '', + logPath: server.logPath || '', rconAddress: rconDetails.address || '', rconPort: rconDetails.port || 2302, - rconPass: '', // Keep pass hidden + rconPass: '', }); + + setAutoMsgs(autoMsgDetails); } }; @@ -188,18 +205,18 @@ const EditServerDialog: React.FC<{ if (!server) return; setLoading(true); try { - // 1. Bundle RCON details + // 1. Encrypt RCON const rconData = JSON.stringify({ address: formData.rconAddress, port: formData.rconPort, pass: formData.rconPass || '', }); + const encryptedRcon = uint8ArrayToBase64(await encrypt(rconData)); - // 2. Encrypt with Community Master Key (E2EE) - const encrypted = await encrypt(rconData); - const encryptedBlob = uint8ArrayToBase64(encrypted); + // 2. Encrypt AutoMessages + const encryptedAutoMsgs = uint8ArrayToBase64(await encrypt(JSON.stringify(autoMsgs))); - // 3. Transmit to backend + // 3. Transmit const res = await fetch('/api/servers', { method: 'PUT', headers: apiHeaders(), @@ -209,7 +226,9 @@ const EditServerDialog: React.FC<{ description: formData.description, workerId: formData.workerId, storageId: formData.storageId, - encryptedRcon: encryptedBlob + logPath: formData.logPath, + encryptedRcon, + encryptedAutoMessages: encryptedAutoMsgs }), }); if (!res.ok) throw new Error('Update failed'); @@ -222,85 +241,162 @@ const EditServerDialog: React.FC<{ } }; - const workers = nodes.filter(n => n.type === 'worker'); - const storages = nodes.filter(n => n.type === 'storage'); + const addMsg = () => setAutoMsgs({ ...autoMsgs, messages: [...autoMsgs.messages, { content: '', interval: 300 }] }); + const removeMsg = (i: number) => setAutoMsgs({ ...autoMsgs, messages: autoMsgs.messages.filter((_, idx) => idx !== i) }); return ( - - - Configure Operational Node - - Modify infrastructure assignment and security parameters + + + Node Orchestration + + Configure infrastructure topology and automated tactical protocols -
-
-
- - setFormData({...formData, name: e.target.value})} className="bg-background/50" /> -
-
- - setFormData({...formData, description: e.target.value})} className="bg-background/50" /> -
-
- -
-
- - -
-
- - -
-
+ Auto-Messages + + -
-
- - RCON Protocol Configuration -
-
-
- - setFormData({...formData, rconAddress: e.target.value})} className="bg-background/50" /> -
-
- - setFormData({...formData, rconPort: parseInt(e.target.value)})} className="bg-background/50" /> -
-
-
- - setFormData({...formData, rconPass: e.target.value})} className="bg-background/50" /> -
-
+ + {activeTab === 'infrastructure' ? ( +
+
+
+ + setFormData({...formData, name: e.target.value})} className="bg-background/50" /> +
+
+ + setFormData({...formData, logPath: e.target.value})} className="bg-background/50" /> +
+
- - - - - +
+
+ + +
+
+ + +
+
+ +
+
+ RCON Protocol +
+
+
+ + setFormData({...formData, rconAddress: e.target.value})} className="bg-background/50" /> +
+
+ + setFormData({...formData, rconPort: parseInt(e.target.value)})} className="bg-background/50" /> +
+
+
+ + setFormData({...formData, rconPass: e.target.value})} className="bg-background/50" /> +
+
+
+ ) : ( +
+
+ +