diff --git a/internal/utils/utils.go b/internal/utils/utils.go index b45115f..7ec2e07 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -4,12 +4,17 @@ import ( "fmt" "log" "net/http" + "os" "strings" "github.com/dustinpianalto/errors" + "github.com/dustinpianalto/quartermaster" + "github.com/golang-jwt/jwt" "github.com/gorilla/mux" ) +var jwtKey = []byte(os.Getenv("JWT_KEY")) + func Mount(r *mux.Router, path string, handler http.Handler) { r.PathPrefix(path).Handler( http.StripPrefix( @@ -77,3 +82,32 @@ func (rh RootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func getErrorBody(s string) []byte { return []byte(fmt.Sprintf("{\"error\": \"%s\"}", s)) } + +func IsAuthenticated(r *http.Request) (*jwt.Token, error) { + const method errors.Method = "utils/IsAuthenticated" + c, err := r.Cookie("token") + if err != nil { + if err == http.ErrNoCookie { + return nil, errors.E(method, errors.Incorrect, "cookie not found", err) + } + return nil, errors.E(method, errors.Malformed, "failed to get cookie data", err) + } + tknStr := c.Value + claims := &quartermaster.Claims{} + tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + if err != nil { + if err == jwt.ErrSignatureInvalid { + return nil, errors.E(method, errors.Incorrect, "cookie is invalid", err) + } + e, _ := err.(*jwt.ValidationError) + if e.Inner == jwt.ErrInvalidKeyType { + return nil, errors.E(method, errors.Internal, err) + } else if e.Inner == jwt.ErrHashUnavailable { + return nil, errors.E(method, errors.Internal, err) + } + return nil, errors.E(method, errors.Malformed, "failed to parse cookie", err) + } + return tkn, nil +} diff --git a/locations.go b/locations.go index 5a4e0b4..610fd42 100644 --- a/locations.go +++ b/locations.go @@ -1,7 +1,7 @@ package quartermaster type Location struct { - ID int `json:"id"` + ID int `json:"id,omitempty"` Name string `json:"name"` Description string `json:"description"` Parent *Location `json:"parent,omitempty"` diff --git a/pkg/api/api.go b/pkg/api/api.go index 200fe63..66cb590 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -2,14 +2,15 @@ package api import ( "github.com/dustinpianalto/quartermaster/internal/utils" + "github.com/dustinpianalto/quartermaster/pkg/api/locations" "github.com/dustinpianalto/quartermaster/pkg/api/users" "github.com/gorilla/mux" ) func GetRouter() *mux.Router { router := mux.NewRouter().StrictSlash(true) - router.HandleFunc("/", healthcheck).Methods("GET") router.HandleFunc("/healthcheck", healthcheck).Methods("GET") utils.Mount(router, "/users", users.GetRouter()) + utils.Mount(router, "/locations", locations.GetRouter()) return router } diff --git a/pkg/api/location/router.go b/pkg/api/location/router.go deleted file mode 100644 index 64ac0cc..0000000 --- a/pkg/api/location/router.go +++ /dev/null @@ -1,9 +0,0 @@ -package location - -import "github.com/gorilla/mux" - -func GetRouter() *mux.Router { - router := mux.NewRouter().StrictSlash(true) - router.HandleFunc("/", location).Methods("GET") - return router -} diff --git a/pkg/api/location/views.go b/pkg/api/location/views.go deleted file mode 100644 index c744035..0000000 --- a/pkg/api/location/views.go +++ /dev/null @@ -1,13 +0,0 @@ -package location - -import "net/http" - -func location(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - addLocation(w, r) - } -} - -func addLocation(w http.ResponseWriter, r *http.Request) { - -} diff --git a/pkg/api/locations/router.go b/pkg/api/locations/router.go new file mode 100644 index 0000000..bd00a00 --- /dev/null +++ b/pkg/api/locations/router.go @@ -0,0 +1,12 @@ +package locations + +import ( + "github.com/dustinpianalto/quartermaster/internal/utils" + "github.com/gorilla/mux" +) + +func GetRouter() *mux.Router { + router := mux.NewRouter().StrictSlash(true) + router.Handle("/", utils.RootHandler(locations)) + return router +} diff --git a/pkg/api/locations/views.go b/pkg/api/locations/views.go new file mode 100644 index 0000000..511ddd5 --- /dev/null +++ b/pkg/api/locations/views.go @@ -0,0 +1,86 @@ +package locations + +import ( + "encoding/json" + "net/http" + + "github.com/dustinpianalto/errors" + "github.com/dustinpianalto/quartermaster" + "github.com/dustinpianalto/quartermaster/internal/utils" + "github.com/dustinpianalto/quartermaster/pkg/services" +) + +func locations(w http.ResponseWriter, r *http.Request) error { + const method = "locations" + token, err := utils.IsAuthenticated(r) + if err != nil { + return errors.E(method, errors.Permission, err) + } + if !token.Valid { + return errors.E(method, errors.Permission, "user not authenticated") + } + user, err := services.UserService.User(token.Claims.(*quartermaster.Claims).Username) + if err != nil { + return errors.E(method, errors.Permission, err) + } + if r.Method == "POST" { + err = addLocation(w, r, user) + if err != nil { + return errors.E(method, "there was a problem adding the location", err) + } + } else if r.Method == "GET" { + err = getTopLocations(w, r, user) + if err != nil { + return errors.E(method, "there was a problem getting locations", err) + } + } else { + return errors.E(method, errors.Malformed, "http method not allowed") + } + return nil +} + +func addLocation(w http.ResponseWriter, r *http.Request, u *quartermaster.User) error { + const method errors.Method = "locations/addLocation" + var l *quartermaster.Location + err := json.NewDecoder(r.Body).Decode(&l) + if err != nil { + return errors.E(method, errors.Malformed, "failed to decode location request", err) + } + if l.Name == "" || l.Description == "" { + return errors.E(method, errors.Malformed, "name and description are required") + } + if l.Parent != nil { + _, err := services.LocationService.Location(l.Parent.ID, u) + if err != nil { + return errors.E(method, errors.Malformed, "parent does not exist", err) + } + } + l, err = services.LocationService.AddLocation(l, u) + if err != nil { + return errors.E(method, errors.Internal, err) + } + lJson, err := json.Marshal(l) + if err != nil { + return errors.E(method, errors.Internal, "error marshalling location", err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + w.Write(lJson) + return nil +} + +func getTopLocations(w http.ResponseWriter, r *http.Request, u *quartermaster.User) error { + const method = "locations/getTopLocations" + locations, err := services.LocationService.GetTopLocations(u) + if err != nil { + return errors.E(method, errors.Internal, "error getting locations", err) + } + locationsJson, err := json.Marshal(locations) + if err != nil { + return errors.E(method, errors.Internal, "error marshalling locations", err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(locationsJson) + return nil +} diff --git a/pkg/api/users/views.go b/pkg/api/users/views.go index dcfb09f..c823825 100644 --- a/pkg/api/users/views.go +++ b/pkg/api/users/views.go @@ -13,6 +13,7 @@ import ( "golang.org/x/crypto/bcrypt" ) +// TODO: refactor login and refresh endpoints so we only have to import this in the internal.utils package var jwtKey = []byte(os.Getenv("JWT_KEY")) func loginHandler(w http.ResponseWriter, r *http.Request) error {