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
This commit is contained in:
Daniel J. Summers 2018-05-26 22:18:26 -05:00
parent bad430fc37
commit 79ced40470
4 changed files with 107 additions and 77 deletions

View File

@ -152,10 +152,13 @@ func AddNote(userID, reqID, note string) int {
// Answered retrieves all answered requests for the given user. // Answered retrieves all answered requests for the given user.
func Answered(userID string) []JournalRequest { func Answered(userID string) []JournalRequest {
rows, err := db.Query(currentRequestSQL+ rows, err := db.Query(currentRequestSQL+
`WHERE "userId" = $1 ` WHERE "userId" = $1
AND "lastStatus" = 'Answered' AND "lastStatus" = 'Answered'
ORDER BY "asOf" DESC`, ORDER BY "asOf" DESC`,
userID) userID)
if err == sql.ErrNoRows {
return make([]JournalRequest, 0)
}
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return nil return nil
@ -168,9 +171,9 @@ func Answered(userID string) []JournalRequest {
func ByID(userID, reqID string) (*JournalRequest, bool) { func ByID(userID, reqID string) (*JournalRequest, bool) {
req := JournalRequest{} req := JournalRequest{}
err := db.QueryRow(currentRequestSQL+ err := db.QueryRow(currentRequestSQL+
`WHERE "requestId" = $1 ` WHERE "requestId" = $1
AND "userId = $2`, AND "userId" = $2`,
userID, reqID).Scan( reqID, userID).Scan(
&req.RequestID, &req.Text, &req.AsOf, &req.LastStatus, &req.RequestID, &req.Text, &req.AsOf, &req.LastStatus,
) )
if err != nil { if err != nil {
@ -235,7 +238,7 @@ func FullByID(userID, reqID string) (*JournalRequest, bool) {
// Journal retrieves the current user's active prayer journal. // Journal retrieves the current user's active prayer journal.
func Journal(userID string) []JournalRequest { func Journal(userID string) []JournalRequest {
rows, err := db.Query(journalSQL, userID) rows, err := db.Query(journalSQL+` ORDER BY "asOf"`, userID)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return nil return nil

View File

@ -9,132 +9,155 @@ import (
"github.com/danieljsummers/myPrayerJournal/src/api/data" "github.com/danieljsummers/myPrayerJournal/src/api/data"
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"github.com/husobee/vestigo" routing "github.com/go-ozzo/ozzo-routing"
) )
/* Support */ /* Support */
// Set the content type, the HTTP error code, and return the error message. // 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.Header().Set("Content-Type", "application/json; encoding=UTF-8")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
if err := json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}); err != nil { if err := json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}); err != nil {
log.Print("Error creating error JSON: " + err.Error()) log.Print("Error creating error JSON: " + err.Error())
} }
return err
} }
// Set the content type and return the JSON to the user. // 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.Header().Set("Content-Type", "application/json; encoding=UTF-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(result); err != nil { 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. // 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 // 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. // hasn't gone through the authorization process.
func userID(r *http.Request) string { func userID(c *routing.Context) string {
return r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)["sub"].(string) return c.Request.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)["sub"].(string)
} }
/* Handlers */ /* Handlers */
// GET: /api/journal/ // GET: /api/journal/
func journal(w http.ResponseWriter, r *http.Request) { func journal(c *routing.Context) error {
reqs := data.Journal(userID(r)) reqs := data.Journal(userID(c))
if reqs == nil { if reqs == nil {
reqs = []data.JournalRequest{} reqs = []data.JournalRequest{}
} }
sendJSON(w, r, reqs) return sendJSON(c, reqs)
} }
// POST: /api/request/ // POST: /api/request/
func requestAdd(w http.ResponseWriter, r *http.Request) { func requestAdd(c *routing.Context) error {
if err := r.ParseForm(); err != nil { payload, err := parseJSON(c)
sendError(w, r, err) 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 { 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 // GET: /api/request/<id>
func requestGet(w http.ResponseWriter, r *http.Request) { func requestGet(c *routing.Context) error {
request, ok := data.ByID(userID(r), vestigo.Param(r, "id")) request, ok := data.ByID(userID(c), c.Param("id"))
if !ok { 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 // GET: /api/request/<id>/complete
func requestGetComplete(w http.ResponseWriter, r *http.Request) { func requestGetComplete(c *routing.Context) error {
request, ok := data.FullByID(userID(r), vestigo.Param(r, "id")) request, ok := data.FullByID(userID(c), c.Param("id"))
if !ok { 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")) request.Notes = data.NotesByID(userID(c), c.Param("id"))
sendJSON(w, r, request) return sendJSON(c, request)
} }
// GET: /api/request/:id/full // GET: /api/request/<id>/full
func requestGetFull(w http.ResponseWriter, r *http.Request) { func requestGetFull(c *routing.Context) error {
request, ok := data.FullByID(userID(r), vestigo.Param(r, "id")) request, ok := data.FullByID(userID(c), c.Param("id"))
if !ok { 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 // POST: /api/request/<id>/history
func requestAddHistory(w http.ResponseWriter, r *http.Request) { func requestAddHistory(c *routing.Context) error {
if err := r.ParseForm(); err != nil { payload, err := parseJSON(c)
sendError(w, r, err) 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 // POST: /api/request/<id>/note
func requestAddNote(w http.ResponseWriter, r *http.Request) { func requestAddNote(c *routing.Context) error {
if err := r.ParseForm(); err != nil { payload, err := parseJSON(c)
sendError(w, r, err) 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 // GET: /api/request/<id>/notes
func requestGetNotes(w http.ResponseWriter, r *http.Request) { func requestGetNotes(c *routing.Context) error {
notes := data.NotesByID(userID(r), vestigo.Param(r, "id")) notes := data.NotesByID(userID(c), c.Param("id"))
if notes == nil { if notes == nil {
w.WriteHeader(http.StatusNotFound) c.Response.WriteHeader(http.StatusNotFound)
return return errors.New("Not Found")
} }
sendJSON(w, r, notes) return sendJSON(c, notes)
} }
// GET: /api/request/answered // GET: /api/request/answered
func requestsAnswered(w http.ResponseWriter, r *http.Request) { func requestsAnswered(c *routing.Context) error {
reqs := data.Answered(userID(r)) reqs := data.Answered(userID(c))
if reqs == nil { if reqs == nil {
reqs = []data.JournalRequest{} reqs = []data.JournalRequest{}
} }
sendJSON(w, r, reqs) return sendJSON(c, reqs)
} }
// GET: /* // 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 // serve index for known routes handled client-side by the app
r := c.Request
w := c.Response
for _, prefix := range ClientPrefixes { for _, prefix := range ClientPrefixes {
if strings.HasPrefix(r.URL.Path, prefix) { if strings.HasPrefix(r.URL.Path, prefix) {
w.Header().Add("Content-Type", "text/html") w.Header().Add("Content-Type", "text/html")
http.ServeFile(w, r, "./public/index.html") http.ServeFile(w, r, "./public/index.html")
return return nil
} }
} }
// 404 here is fine; quit hacking, y'all... // 404 here is fine; quit hacking, y'all...
http.ServeFile(w, r, "./public"+r.URL.Path) http.ServeFile(w, r, "./public"+r.URL.Path)
return nil
} }

View File

@ -5,11 +5,14 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"github.com/auth0/go-jwt-middleware" "github.com/auth0/go-jwt-middleware"
jwt "github.com/dgrijalva/jwt-go" 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. // AuthConfig contains the Auth0 configuration passed from the "auth" JSON object.
@ -96,25 +99,24 @@ var authZero = jwtmiddleware.New(jwtmiddleware.Options{
SigningMethod: jwt.SigningMethodRS256, SigningMethod: jwt.SigningMethodRS256,
}) })
// authMiddleware is a wrapper for the Auth0 middleware above with a signature Vestigo recognizes. // authMiddleware is a wrapper for the Auth0 middleware above with a signature ozzo-routing recognizes.
func authMiddleware(f http.HandlerFunc) http.HandlerFunc { func authMiddleware(c *routing.Context) error {
return func(w http.ResponseWriter, r *http.Request) { return authZero.CheckJWT(c.Response, c.Request)
err := authZero.CheckJWT(w, r)
if err == nil {
f(w, r)
}
}
} }
// NewRouter returns a configured router to handle all incoming requests. // NewRouter returns a configured router to handle all incoming requests.
func NewRouter(cfg *AuthConfig) *vestigo.Router { func NewRouter(cfg *AuthConfig) *routing.Router {
authCfg = cfg 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 { for _, route := range routes {
if route.IsPublic { if route.IsPublic {
router.Add(route.Method, route.Pattern, route.Func) router.To(route.Method, route.Pattern, route.Func)
} else { } else {
router.Add(route.Method, route.Pattern, route.Func, authMiddleware) router.To(route.Method, route.Pattern, authMiddleware, route.Func)
} }
} }
return router return router

View File

@ -3,6 +3,8 @@ package routes
import ( import (
"net/http" "net/http"
routing "github.com/go-ozzo/ozzo-routing"
) )
// Route is a route served in the application. // Route is a route served in the application.
@ -10,7 +12,7 @@ type Route struct {
Name string Name string
Method string Method string
Pattern string Pattern string
Func http.HandlerFunc Func routing.Handler
IsPublic bool IsPublic bool
} }
@ -36,42 +38,42 @@ var routes = Routes{
Route{ Route{
"GetRequestByID", "GetRequestByID",
http.MethodGet, http.MethodGet,
"/api/request/:id", "/api/request/<id>",
requestGet, requestGet,
false, false,
}, },
Route{ Route{
"GetCompleteRequestByID", "GetCompleteRequestByID",
http.MethodGet, http.MethodGet,
"/api/request/:id/complete", "/api/request/<id>/complete",
requestGetComplete, requestGetComplete,
false, false,
}, },
Route{ Route{
"GetFullRequestByID", "GetFullRequestByID",
http.MethodGet, http.MethodGet,
"/api/request/:id/full", "/api/request/<id>/full",
requestGetFull, requestGetFull,
false, false,
}, },
Route{ Route{
"AddNewHistoryEntry", "AddNewHistoryEntry",
http.MethodPost, http.MethodPost,
"/api/request/:id/history", "/api/request/<id>/history",
requestAddHistory, requestAddHistory,
false, false,
}, },
Route{ Route{
"AddNewNote", "AddNewNote",
http.MethodPost, http.MethodPost,
"/api/request/:id/note", "/api/request/<id>/note",
requestAddNote, requestAddNote,
false, false,
}, },
Route{ Route{
"GetNotesForRequest", "GetNotesForRequest",
http.MethodGet, http.MethodGet,
"/api/request/:id/notes", "/api/request/<id>/notes",
requestGetNotes, requestGetNotes,
false, false,
}, },