From 666c9c9eb8a4911414d5edea6fc70b6f98a781e8 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sun, 4 May 2025 17:25:29 +0200 Subject: [PATCH] feat: return json responses --- api/rcond.yaml | 169 ++++++++++++++++++++++++++++++++++++++++--- pkg/http/handlers.go | 83 ++++++++++++--------- 2 files changed, 209 insertions(+), 43 deletions(-) diff --git a/api/rcond.yaml b/api/rcond.yaml index a81f2a9..e83f0a4 100644 --- a/api/rcond.yaml +++ b/api/rcond.yaml @@ -15,6 +15,14 @@ components: in: header name: X-API-Token description: API token for authentication + schemas: + Error: + type: object + properties: + error: + type: string + description: Error message + example: "some error message" security: - ApiKeyAuth: [] @@ -36,6 +44,12 @@ paths: status: type: string example: "healthy" + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /network/ap: post: @@ -78,10 +92,22 @@ paths: example: "7d706027-727c-4d4c-a816-f0e1b99db8ab" '400': description: Invalid request payload + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /network/interface/{interface}: put: @@ -111,12 +137,32 @@ paths: responses: '200': description: Network interface brought up successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" '400': description: Invalid request payload + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' delete: summary: Deactivate network connection description: Deactivates the specified network connection @@ -131,12 +177,32 @@ paths: responses: '200': description: Network interface brought down successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" '400': description: Invalid request payload + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /network/connection/{uuid}: delete: @@ -153,12 +219,32 @@ paths: responses: '200': description: Connection profile removed successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" '400': description: Invalid request payload + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /hostname: get: @@ -168,15 +254,26 @@ paths: '200': description: Hostname retrieved successfully content: - text/plain: + application/json: schema: - type: string - description: Current hostname - example: "MyHostname" + type: object + properties: + hostname: + type: string + description: Current hostname + example: "MyHostname" '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' post: summary: Set system hostname description: Sets a new system hostname @@ -196,12 +293,32 @@ paths: responses: '200': description: Hostname set successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" '400': description: Invalid request payload + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /users/{user}/keys: post: @@ -232,17 +349,32 @@ paths: '200': description: Key added successfully content: - text/plain: + application/json: schema: - type: string - description: Fingerprint of the added key - example: "SHA256:abcdef1234567890..." + type: object + properties: + fingerprint: + type: string + description: Fingerprint of the added key + example: "SHA256:abcdef1234567890..." '400': description: Invalid request payload or SSH key format + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /users/{user}/keys/{fingerprint}: delete: @@ -261,15 +393,34 @@ paths: required: true schema: type: string - format: string description: URL-safe Base64 encoded fingerprint of the key to remove example: "U0hBMjU2OmFiY2RlZjEyMzQ1Njc4OTAuLi4=" responses: '200': description: Key removed successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" '400': description: Invalid request payload or fingerprint + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '401': description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/pkg/http/handlers.go b/pkg/http/handlers.go index 68b1eb3..ec9a249 100644 --- a/pkg/http/handlers.go +++ b/pkg/http/handlers.go @@ -11,10 +11,6 @@ import ( "github.com/gorilla/mux" ) -const ( - NETWORK_CONNECTION_UUID = "7d706027-727c-4d4c-a816-f0e1b99db8ab" -) - type configureAPRequest struct { Interface string `json:"interface"` SSID string `json:"ssid"` @@ -34,15 +30,25 @@ type authorizedKeyRequest struct { PubKey string `json:"pubkey"` } +type errorResponse struct { + Error string `json:"error"` +} + +func writeError(w http.ResponseWriter, message string, code int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(errorResponse{Error: message}) +} + func HandleConfigureAP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req configureAPRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + writeError(w, err.Error(), http.StatusBadRequest) return } @@ -50,7 +56,7 @@ func HandleConfigureAP(w http.ResponseWriter, r *http.Request) { uuid, err := network.ConfigureAP(req.Interface, req.SSID, req.Password) if err != nil { log.Printf("Failed to configure access point on interface %s: %v", req.Interface, err) - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } log.Printf("Successfully configured access point on interface %s with UUID %s", req.Interface, uuid) @@ -64,19 +70,20 @@ func HandleConfigureAP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf("Failed to encode response: %v", err) - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } } + func HandleNetworkUp(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req networkUpRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + writeError(w, err.Error(), http.StatusBadRequest) return } vars := mux.Vars(r) @@ -85,84 +92,90 @@ func HandleNetworkUp(w http.ResponseWriter, r *http.Request) { log.Printf("Bringing up network interface %s with UUID %s", iface, req.UUID) if err := network.Up(iface, req.UUID); err != nil { log.Printf("Failed to bring up network interface %s: %v", iface, err) - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } log.Printf("Successfully brought up network interface %s", iface) - w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } func HandleNetworkDown(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) return } vars := mux.Vars(r) iface := vars["interface"] if err := network.Down(iface); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } func HandleNetworkRemove(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) return } vars := mux.Vars(r) uuid := vars["uuid"] if err := network.Remove(uuid); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } func HandleGetHostname(w http.ResponseWriter, r *http.Request) { hostname, err := network.GetHostname() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } - w.Write([]byte(hostname)) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"hostname": hostname}) } func HandleSetHostname(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req setHostnameRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + writeError(w, err.Error(), http.StatusBadRequest) return } if err := network.SetHostname(req.Hostname); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } func HandleAddAuthorizedKey(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req authorizedKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + writeError(w, err.Error(), http.StatusBadRequest) return } vars := mux.Vars(r) @@ -170,42 +183,44 @@ func HandleAddAuthorizedKey(w http.ResponseWriter, r *http.Request) { fingerprint, err := user.AddAuthorizedKey(username, req.PubKey) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } - w.Write([]byte(fingerprint)) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"fingerprint": fingerprint}) } func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) return } vars := mux.Vars(r) fingerprint := vars["fingerprint"] if fingerprint == "" { - http.Error(w, "fingerprint parameter is required", http.StatusBadRequest) + writeError(w, "fingerprint parameter is required", http.StatusBadRequest) return } username := vars["user"] if username == "" { - http.Error(w, "user parameter is required", http.StatusBadRequest) + writeError(w, "user parameter is required", http.StatusBadRequest) return } fingerprintBytes, err := base64.RawURLEncoding.DecodeString(fingerprint) if err != nil { - http.Error(w, "invalid fingerprint base64", http.StatusBadRequest) + writeError(w, "invalid fingerprint base64", http.StatusBadRequest) return } if err := user.RemoveAuthorizedKey(username, string(fingerprintBytes)); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeError(w, err.Error(), http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) }