From 79ced404709f9fb23b26178ae9a03c68f29d0233 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 26 May 2018 22:18:26 -0500 Subject: [PATCH] Most API features now work - Switched from Vestigo to ozzo-routing in a misguided attempt to get JSON to parse as a form; not switching back at this point - Fixed error with request SQL for individual requests - Parsing JSON body works now Still need to handle "no rows found" for journal/answered lists; this isn't an error for new users --- src/api/data/data.go | 13 ++-- src/api/routes/handlers.go | 127 ++++++++++++++++++++++--------------- src/api/routes/router.go | 28 ++++---- src/api/routes/routes.go | 16 +++-- 4 files changed, 107 insertions(+), 77 deletions(-) diff --git a/src/api/data/data.go b/src/api/data/data.go index bc12d97..8fe2fb0 100644 --- a/src/api/data/data.go +++ b/src/api/data/data.go @@ -152,10 +152,13 @@ func AddNote(userID, reqID, note string) int { // Answered retrieves all answered requests for the given user. func Answered(userID string) []JournalRequest { rows, err := db.Query(currentRequestSQL+ - `WHERE "userId" = $1 + ` WHERE "userId" = $1 AND "lastStatus" = 'Answered' ORDER BY "asOf" DESC`, userID) + if err == sql.ErrNoRows { + return make([]JournalRequest, 0) + } if err != nil { log.Print(err) return nil @@ -168,9 +171,9 @@ func Answered(userID string) []JournalRequest { func ByID(userID, reqID string) (*JournalRequest, bool) { req := JournalRequest{} err := db.QueryRow(currentRequestSQL+ - `WHERE "requestId" = $1 - AND "userId = $2`, - userID, reqID).Scan( + ` WHERE "requestId" = $1 + AND "userId" = $2`, + reqID, userID).Scan( &req.RequestID, &req.Text, &req.AsOf, &req.LastStatus, ) if err != nil { @@ -235,7 +238,7 @@ func FullByID(userID, reqID string) (*JournalRequest, bool) { // Journal retrieves the current user's active prayer journal. func Journal(userID string) []JournalRequest { - rows, err := db.Query(journalSQL, userID) + rows, err := db.Query(journalSQL+` ORDER BY "asOf"`, userID) if err != nil { log.Print(err) return nil diff --git a/src/api/routes/handlers.go b/src/api/routes/handlers.go index 1946df6..7add6b6 100644 --- a/src/api/routes/handlers.go +++ b/src/api/routes/handlers.go @@ -9,132 +9,155 @@ import ( "github.com/danieljsummers/myPrayerJournal/src/api/data" jwt "github.com/dgrijalva/jwt-go" - "github.com/husobee/vestigo" + routing "github.com/go-ozzo/ozzo-routing" ) /* Support */ // Set the content type, the HTTP error code, and return the error message. -func sendError(w http.ResponseWriter, r *http.Request, err error) { +func sendError(c *routing.Context, err error) error { + w := c.Response w.Header().Set("Content-Type", "application/json; encoding=UTF-8") w.WriteHeader(http.StatusInternalServerError) if err := json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}); err != nil { log.Print("Error creating error JSON: " + err.Error()) } + return err } // Set the content type and return the JSON to the user. -func sendJSON(w http.ResponseWriter, r *http.Request, result interface{}) { +func sendJSON(c *routing.Context, result interface{}) error { + w := c.Response w.Header().Set("Content-Type", "application/json; encoding=UTF-8") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(result); err != nil { - sendError(w, r, err) + return sendError(c, err) } + return nil +} + +// Parse the request body as JSON. +func parseJSON(c *routing.Context) (map[string]interface{}, error) { + payload := make(map[string]interface{}) + if err := json.NewDecoder(c.Request.Body).Decode(&payload); err != nil { + log.Println("Error decoding JSON:", err) + return payload, err + } + return payload, nil } // userID is a convenience function to extract the subscriber ID from the user's JWT. // NOTE: Do not call this from public routes; there are a lot of type assertions that won't be true if the request // hasn't gone through the authorization process. -func userID(r *http.Request) string { - return r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)["sub"].(string) +func userID(c *routing.Context) string { + return c.Request.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)["sub"].(string) } /* Handlers */ // GET: /api/journal/ -func journal(w http.ResponseWriter, r *http.Request) { - reqs := data.Journal(userID(r)) +func journal(c *routing.Context) error { + reqs := data.Journal(userID(c)) if reqs == nil { reqs = []data.JournalRequest{} } - sendJSON(w, r, reqs) + return sendJSON(c, reqs) } // POST: /api/request/ -func requestAdd(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - sendError(w, r, err) +func requestAdd(c *routing.Context) error { + payload, err := parseJSON(c) + if err != nil { + return sendError(c, err) } - result, ok := data.AddNew(userID(r), r.FormValue("requestText")) + result, ok := data.AddNew(userID(c), payload["requestText"].(string)) if !ok { - sendError(w, r, errors.New("error adding request")) + return sendError(c, errors.New("error adding request")) } - sendJSON(w, r, result) + return sendJSON(c, result) } -// GET: /api/request/:id -func requestGet(w http.ResponseWriter, r *http.Request) { - request, ok := data.ByID(userID(r), vestigo.Param(r, "id")) +// GET: /api/request/ +func requestGet(c *routing.Context) error { + request, ok := data.ByID(userID(c), c.Param("id")) if !ok { - sendError(w, r, errors.New("error retrieving request")) + return sendError(c, errors.New("error retrieving request")) } - sendJSON(w, r, request) + return sendJSON(c, request) } -// GET: /api/request/:id/complete -func requestGetComplete(w http.ResponseWriter, r *http.Request) { - request, ok := data.FullByID(userID(r), vestigo.Param(r, "id")) +// GET: /api/request//complete +func requestGetComplete(c *routing.Context) error { + request, ok := data.FullByID(userID(c), c.Param("id")) if !ok { - sendError(w, r, errors.New("error retrieving request")) + return sendError(c, errors.New("error retrieving request")) } - request.Notes = data.NotesByID(userID(r), vestigo.Param(r, "id")) - sendJSON(w, r, request) + request.Notes = data.NotesByID(userID(c), c.Param("id")) + return sendJSON(c, request) } -// GET: /api/request/:id/full -func requestGetFull(w http.ResponseWriter, r *http.Request) { - request, ok := data.FullByID(userID(r), vestigo.Param(r, "id")) +// GET: /api/request//full +func requestGetFull(c *routing.Context) error { + request, ok := data.FullByID(userID(c), c.Param("id")) if !ok { - sendError(w, r, errors.New("error retrieving request")) + return sendError(c, errors.New("error retrieving request")) } - sendJSON(w, r, request) + return sendJSON(c, request) } -// POST: /api/request/:id/history -func requestAddHistory(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - sendError(w, r, err) +// POST: /api/request//history +func requestAddHistory(c *routing.Context) error { + payload, err := parseJSON(c) + if err != nil { + return sendError(c, err) } - w.WriteHeader(data.AddHistory(userID(r), vestigo.Param(r, "id"), r.FormValue("status"), r.FormValue("updateText"))) + c.Response.WriteHeader( + data.AddHistory(userID(c), c.Param("id"), payload["status"].(string), payload["updateText"].(string))) + return nil } -// POST: /api/request/:id/note -func requestAddNote(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - sendError(w, r, err) +// POST: /api/request//note +func requestAddNote(c *routing.Context) error { + payload, err := parseJSON(c) + if err != nil { + return sendError(c, err) } - w.WriteHeader(data.AddNote(userID(r), vestigo.Param(r, "id"), r.FormValue("notes"))) + c.Response.WriteHeader(data.AddNote(userID(c), c.Param("id"), payload["notes"].(string))) + return nil } -// GET: /api/request/:id/notes -func requestGetNotes(w http.ResponseWriter, r *http.Request) { - notes := data.NotesByID(userID(r), vestigo.Param(r, "id")) +// GET: /api/request//notes +func requestGetNotes(c *routing.Context) error { + notes := data.NotesByID(userID(c), c.Param("id")) if notes == nil { - w.WriteHeader(http.StatusNotFound) - return + c.Response.WriteHeader(http.StatusNotFound) + return errors.New("Not Found") } - sendJSON(w, r, notes) + return sendJSON(c, notes) } // GET: /api/request/answered -func requestsAnswered(w http.ResponseWriter, r *http.Request) { - reqs := data.Answered(userID(r)) +func requestsAnswered(c *routing.Context) error { + reqs := data.Answered(userID(c)) if reqs == nil { reqs = []data.JournalRequest{} } - sendJSON(w, r, reqs) + return sendJSON(c, reqs) } // GET: /* -func staticFiles(w http.ResponseWriter, r *http.Request) { +func staticFiles(c *routing.Context) error { // serve index for known routes handled client-side by the app + r := c.Request + w := c.Response for _, prefix := range ClientPrefixes { if strings.HasPrefix(r.URL.Path, prefix) { w.Header().Add("Content-Type", "text/html") http.ServeFile(w, r, "./public/index.html") - return + return nil } } // 404 here is fine; quit hacking, y'all... http.ServeFile(w, r, "./public"+r.URL.Path) + return nil } diff --git a/src/api/routes/router.go b/src/api/routes/router.go index 94d4e83..47dc95e 100644 --- a/src/api/routes/router.go +++ b/src/api/routes/router.go @@ -5,11 +5,14 @@ import ( "errors" "fmt" "io/ioutil" + "log" "net/http" "github.com/auth0/go-jwt-middleware" jwt "github.com/dgrijalva/jwt-go" - "github.com/husobee/vestigo" + "github.com/go-ozzo/ozzo-routing" + "github.com/go-ozzo/ozzo-routing/access" + "github.com/go-ozzo/ozzo-routing/fault" ) // AuthConfig contains the Auth0 configuration passed from the "auth" JSON object. @@ -96,25 +99,24 @@ var authZero = jwtmiddleware.New(jwtmiddleware.Options{ SigningMethod: jwt.SigningMethodRS256, }) -// authMiddleware is a wrapper for the Auth0 middleware above with a signature Vestigo recognizes. -func authMiddleware(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - err := authZero.CheckJWT(w, r) - if err == nil { - f(w, r) - } - } +// authMiddleware is a wrapper for the Auth0 middleware above with a signature ozzo-routing recognizes. +func authMiddleware(c *routing.Context) error { + return authZero.CheckJWT(c.Response, c.Request) } // NewRouter returns a configured router to handle all incoming requests. -func NewRouter(cfg *AuthConfig) *vestigo.Router { +func NewRouter(cfg *AuthConfig) *routing.Router { authCfg = cfg - router := vestigo.NewRouter() + router := routing.New() + router.Use( + access.Logger(log.Printf), // TODO: remove before go-live + fault.Recovery(log.Printf), + ) for _, route := range routes { if route.IsPublic { - router.Add(route.Method, route.Pattern, route.Func) + router.To(route.Method, route.Pattern, route.Func) } else { - router.Add(route.Method, route.Pattern, route.Func, authMiddleware) + router.To(route.Method, route.Pattern, authMiddleware, route.Func) } } return router diff --git a/src/api/routes/routes.go b/src/api/routes/routes.go index ac8a5d6..c022f97 100644 --- a/src/api/routes/routes.go +++ b/src/api/routes/routes.go @@ -3,6 +3,8 @@ package routes import ( "net/http" + + routing "github.com/go-ozzo/ozzo-routing" ) // Route is a route served in the application. @@ -10,7 +12,7 @@ type Route struct { Name string Method string Pattern string - Func http.HandlerFunc + Func routing.Handler IsPublic bool } @@ -36,42 +38,42 @@ var routes = Routes{ Route{ "GetRequestByID", http.MethodGet, - "/api/request/:id", + "/api/request/", requestGet, false, }, Route{ "GetCompleteRequestByID", http.MethodGet, - "/api/request/:id/complete", + "/api/request//complete", requestGetComplete, false, }, Route{ "GetFullRequestByID", http.MethodGet, - "/api/request/:id/full", + "/api/request//full", requestGetFull, false, }, Route{ "AddNewHistoryEntry", http.MethodPost, - "/api/request/:id/history", + "/api/request//history", requestAddHistory, false, }, Route{ "AddNewNote", http.MethodPost, - "/api/request/:id/note", + "/api/request//note", requestAddNote, false, }, Route{ "GetNotesForRequest", http.MethodGet, - "/api/request/:id/notes", + "/api/request//notes", requestGetNotes, false, },