From e115492f0d121308f1079679aecae3d2c497d523 Mon Sep 17 00:00:00 2001 From: Joshua Herring Date: Thu, 28 May 2026 09:41:36 -0400 Subject: [PATCH] basic API handler --- api.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 api.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..563096e --- /dev/null +++ b/api.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "faculty_media_report/dbi" + "github.com/golang-jwt/jwt/v5" +) + +type jsonRPCRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + ID *int `json:"id"` +} + +type jsonRPCErrorBody struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type jsonRPCResponse struct { + Jsonrpc string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error *jsonRPCErrorBody `json:"error,omitempty"` + ID *int `json:"id"` +} + +func writeJSONRPC(w http.ResponseWriter, resp jsonRPCResponse) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +func jsonRPCErr(w http.ResponseWriter, code int, message string, id *int) { + writeJSONRPC(w, jsonRPCResponse{ + Jsonrpc: "2.0", + Error: &jsonRPCErrorBody{Code: code, Message: message}, + ID: id, + }) +} + +func handleAPI(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if !strings.HasPrefix(authHeader, "Bearer ") { + http.Error(w, "missing or invalid Authorization header", http.StatusUnauthorized) + return + } + tokenStr := strings.TrimPrefix(authHeader, "Bearer ") + + db, err := dbi.GetDbConn() + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + defer db.Close() + + conn, err := db.Conn(context.Background()) + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + defer conn.Close() + + var user dbi.User + token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) + } + sub, err := t.Claims.GetSubject() + if err != nil { + return nil, fmt.Errorf("missing sub claim") + } + user, err = dbi.GetUser(conn, sub) + if err != nil { + return nil, fmt.Errorf("user not found") + } + return []byte(user.APIKey), nil + }, jwt.WithExpirationRequired()) + + if err != nil || !token.Valid { + if err != nil { + http.Error(w, "unauthorized: "+err.Error(), http.StatusUnauthorized) + } else { + http.Error(w, "unauthorized", http.StatusUnauthorized) + } + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + http.Error(w, "invalid token claims", http.StatusUnauthorized) + return + } + + iat, err := claims.GetIssuedAt() + if err != nil || iat == nil { + http.Error(w, "missing iat claim", http.StatusUnauthorized) + return + } + if iat.After(time.Now()) { + http.Error(w, "iat is in the future", http.StatusUnauthorized) + return + } + + var req jsonRPCRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonRPCErr(w, -32700, "Parse error", nil) + return + } + if req.Jsonrpc != "2.0" { + jsonRPCErr(w, -32600, `Invalid Request: jsonrpc must be "2.0"`, req.ID) + return + } + if req.Method == "" { + jsonRPCErr(w, -32600, "Invalid Request: method is required", req.ID) + return + } + if req.Params == nil { + jsonRPCErr(w, -32600, "Invalid Request: params is required", req.ID) + return + } + if req.ID == nil { + jsonRPCErr(w, -32600, "Invalid Request: id is required", req.ID) + return + } + + _ = user + jsonRPCErr(w, -32601, "Method not found", req.ID) +}