DustyP 4 years ago
parent 35b2afba37
commit 5c8e734e81

@ -6,10 +6,11 @@ import (
"os"
"github.com/dustinpianalto/quartermaster/internal/postgres"
"github.com/dustinpianalto/quartermaster/internal/utils"
"github.com/dustinpianalto/quartermaster/pkg/api"
"github.com/dustinpianalto/quartermaster/pkg/services"
"github.com/dustinpianalto/quartermaster/pkg/ui"
"github.com/dustinpianalto/quartermaster/pkg/utils"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
@ -19,8 +20,13 @@ func main() {
services.InitServices()
router := mux.NewRouter()
utils.Mount(router, "/api", api.GetRouter())
utils.Mount(router, "/api/v1", api.GetRouter())
utils.Mount(router, "/", ui.GetRouter())
log.Fatal(http.ListenAndServe(":8000", router))
headersOk := handlers.AllowedHeaders([]string{"Content-Type", "Authorization"})
credentialsOk := handlers.AllowCredentials()
originsOk := handlers.AllowedOrigins([]string{"*"})
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
log.Fatal(http.ListenAndServe(":8000", handlers.CORS(credentialsOk, originsOk, methodsOk, headersOk)(router)))
}

@ -6,6 +6,7 @@ require (
github.com/dustinpianalto/errors v0.0.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-migrate/migrate/v4 v4.15.1
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.4
github.com/markbates/pkger v0.17.1
@ -13,8 +14,10 @@ require (
)
require (
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/gobuffalo/here v0.6.5 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
go.uber.org/atomic v1.9.0 // indirect
)

@ -311,12 +311,6 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustinpianalto/errors v0.0.0-20211118053345-3fdc109b1e99 h1:O77IGSqF4gOJQV3wGPHbSDK/5ESgkaoWzifwiqhqbWg=
github.com/dustinpianalto/errors v0.0.0-20211118053345-3fdc109b1e99/go.mod h1:Fo865gGhrM1eyVIp5H5U8kQkZtFc/daiU3QBpUCd+B4=
github.com/dustinpianalto/errors v0.0.0-20211118085456-cf0fb5b111e2 h1:e7d1C9hgZjmwM/7KGBfTQYPlqQgeKZySZGr8/vgJGaM=
github.com/dustinpianalto/errors v0.0.0-20211118085456-cf0fb5b111e2/go.mod h1:Fo865gGhrM1eyVIp5H5U8kQkZtFc/daiU3QBpUCd+B4=
github.com/dustinpianalto/errors v0.0.1 h1:Js+9tSiJI/VgOiz0PtRSA8XRpnr1YWHigacYuanufRg=
github.com/dustinpianalto/errors v0.0.1/go.mod h1:Fo865gGhrM1eyVIp5H5U8kQkZtFc/daiU3QBpUCd+B4=
github.com/dustinpianalto/errors v0.0.2 h1:uD6ZapWpOxuA0qLOPWCvYnIe8A0P0Y3IlCTBY5QfhvE=
github.com/dustinpianalto/errors v0.0.2/go.mod h1:Fo865gGhrM1eyVIp5H5U8kQkZtFc/daiU3QBpUCd+B4=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
@ -334,6 +328,9 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@ -510,6 +507,8 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
@ -825,6 +824,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg=

@ -0,0 +1,105 @@
package jwt
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/dustinpianalto/errors"
"github.com/dustinpianalto/quartermaster"
"github.com/golang-jwt/jwt"
)
var secretKey = []byte(os.Getenv("JWT_KEY"))
func AuthenticateJWTToken(req *http.Request) (*quartermaster.Claims, error) {
const method = errors.Method("jwt/AuthenticateJWTToken")
jwtToken, err := extractJWTToken(req)
if err != nil {
return nil, errors.E(method, "could not authenticate token", err)
}
claims, err := ParseJWT(jwtToken, secretKey)
if err != nil {
return nil, errors.E(method, "could not parse token", err)
}
return claims, nil
}
// ExtractJWTToken extracts bearer token from Authorization header
func extractJWTToken(req *http.Request) (string, error) {
const method = errors.Method("jwt/extractJWTToken")
tokenString := req.Header.Get("Authorization")
if tokenString == "" {
return "", errors.E(method, errors.Malformed, "token not found")
}
tokenString, err := stripTokenPrefix(tokenString)
if err != nil {
return "", errors.E(method, "error formatting token", err)
}
return tokenString, nil
}
// Strips 'Token' or 'Bearer' prefix from token string
func stripTokenPrefix(tok string) (string, error) {
// split token to 2 parts
tokenParts := strings.Split(tok, " ")
if len(tokenParts) < 2 {
return tokenParts[0], nil
}
return tokenParts[1], nil
}
func ParseJWT(tokenString string, key []byte) (*quartermaster.Claims, error) {
const method = errors.Method("jwt/ParseJWT")
var claims *quartermaster.Claims = &quartermaster.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// validate the alg is what is expected:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.E(errors.Method("jwt/ParseJWT/Parse"), errors.Malformed, fmt.Sprintf("unexpected signing method: %v", token.Header["alg"]))
}
return key, nil
})
if err != nil {
return nil, errors.E(method, "error parsing token", err)
}
if token.Valid {
return claims, nil
} else if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, errors.E(method, errors.Malformed, "token is malformed")
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
// Token is either expired or not active yet
return nil, errors.E(method, errors.Permission, "token is either expired or not yet valid")
} else {
return nil, errors.E(method, errors.Internal, "unknown error with token")
}
} else {
return nil, errors.E(method, errors.Permission, "token is not valid")
}
}
func CreateJWTToken(claims quartermaster.Claims) (string, error) {
const method = errors.Method("jwt/CreateJWTToken")
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(secretKey)
if err != nil {
return "", errors.E(method, errors.Internal, "error signing token", err)
}
return tokenString, nil
}

@ -4,7 +4,9 @@ import (
"database/sql"
"log"
"github.com/dustinpianalto/errors"
"github.com/dustinpianalto/quartermaster"
"github.com/dustinpianalto/quartermaster/internal/utils"
)
type itemService struct {
@ -12,18 +14,24 @@ type itemService struct {
}
func (s itemService) Item(id int, user *quartermaster.User) (*quartermaster.Item, error) {
var method errors.Method = "postgres/Items"
var i quartermaster.Item
queryString := "SELECT id, name, description, size, unit, barcode, nutrition_id FROM items WHERE id = $1 AND owner_id = $2"
row := s.db.QueryRow(queryString, id, user.ID)
var nutrition_id sql.NullInt32
err := row.Scan(&i.ID, &i.Name, &i.Description, &i.Size, &i.Unit, &i.Barcode, &nutrition_id)
var unit string
err := row.Scan(&i.ID, &i.Name, &i.Description, &i.Size, &unit, &i.Barcode, &nutrition_id)
if err != nil {
return nil, err
return nil, errors.E(method, errors.Internal, "error getting item data", err)
}
i.Unit, err = utils.UnitFromString(unit)
if err != nil {
return nil, errors.E(method, err)
}
if nutrition_id.Valid {
n, err := NutritionService.Nutrition(int(nutrition_id.Int32))
if err != nil {
return nil, err
return nil, errors.E(method, "error getting nutrition data", err)
}
i.Nutrition = n
} else {
@ -40,14 +48,17 @@ func (s itemService) AddItem(i *quartermaster.Item, l *quartermaster.Location, u
if err != nil {
log.Println(err)
}
}
queryString := "INSERT INTO items (name, description, size, unit, barcode, nutrition_id, owner_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id"
err := s.db.QueryRow(queryString, i.Name, i.Description, i.Size, i.Unit, i.Barcode, i.Nutrition.ID, user.ID).Scan(&i.ID)
err = s.db.QueryRow(queryString, i.Name, i.Description, i.Size, i.Unit.String(), i.Barcode, i.Nutrition.ID, user.ID).Scan(&i.ID)
} else {
queryString := "INSERT INTO items (name, description, size, unit, barcode, nutrition_id, owner_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id"
err = s.db.QueryRow(queryString, i.Name, i.Description, i.Size, i.Unit.String(), i.Barcode, nil, user.ID).Scan(&i.ID)
}
if err != nil {
return nil, err
}
}
queryString := "INSERT INTO x_items_locations (item_id, location_id, count) VALUES ($1, $2, 1) ON DUPLICATE KEY UPDATE count = count + 1"
queryString := "INSERT INTO x_items_locations (item_id, location_id, count) VALUES ($1, $2, 1) ON CONFLICT (item_id, location_id) DO UPDATE SET count = x_items_locations.count + 1"
_, err = s.db.Exec(queryString, i.ID, l.ID)
return i, err
}
@ -93,7 +104,11 @@ func (s itemService) GetItemByBarcode(b string, user *quartermaster.User) (*quar
return s.Item(id, user)
}
func (s itemService) RemoveItem(i *quartermaster.Item, l *quartermaster.Location) error {
func (s itemService) RemoveItem(i *quartermaster.Item, l *quartermaster.Location, u *quartermaster.User) error {
const method errors.Method = "itemService/RemoveItem"
if ok, err := s.IsOwner(i, u); !ok {
return errors.E(method, err)
}
queryString := "UPDATE x_items_locations SET count = count - 1 WHERE item_id = $1 AND location_id = $2 RETURNING count"
var count int
err := s.db.QueryRow(queryString, i.ID, l.ID).Scan(&count)
@ -109,7 +124,7 @@ func (s itemService) RemoveItem(i *quartermaster.Item, l *quartermaster.Location
}
func (s itemService) MoveItem(i *quartermaster.Item, old, new *quartermaster.Location, user *quartermaster.User) error {
err := s.RemoveItem(i, old)
err := s.RemoveItem(i, old, user)
if err != nil {
return err
}
@ -136,3 +151,41 @@ func (s itemService) UpdateItem(i *quartermaster.Item, user *quartermaster.User)
_, err = s.db.Exec(queryString, i.ID, i.Name, i.Description, i.Size, i.Unit, i.Barcode, i.Nutrition.ID, user.ID)
return err
}
func (s itemService) IsOwner(i *quartermaster.Item, u *quartermaster.User) (bool, error) {
const method errors.Method = "itemService/IsOwner"
var ownerID int
queryString := "SELECT owner_id FROM items WHERE id = $1"
row := s.db.QueryRow(queryString, i.ID)
err := row.Scan(&ownerID)
if err != nil {
return false, errors.E(method, errors.Internal, "error getting owner", err)
}
if ownerID == u.ID {
return true, nil
}
return false, nil
}
func (s itemService) GetItemLocations(i *quartermaster.Item, u *quartermaster.User) (map[int]int, error) {
const method errors.Method = "itemService/GetItemLocations"
if ok, err := s.IsOwner(i, u); !ok {
return nil, errors.E(method, err)
}
queryString := "SELECT location_id, count FROM x_items_locations WHERE item_id = $1"
rows, err := s.db.Query(queryString, i.ID)
if err != nil {
return nil, errors.E(method, errors.Internal, "error getting locations", err)
}
out := make(map[int]int)
for rows.Next() {
var lID int
var count int
err := rows.Scan(&lID, &count)
if err != nil {
log.Println(errors.E(method, errors.Internal, err))
}
out[lID] = count
}
return out, nil
}

@ -106,6 +106,17 @@ func (s locationService) GetItems(l *quartermaster.Location, user *quartermaster
return items, nil
}
func (s locationService) GetItemCount(l *quartermaster.Location, i *quartermaster.Item, user *quartermaster.User) (int, error) {
var count int
queryString := "SELECT count FROM x_items_locations WHERE location_id = $1 AND item_id = $2"
row := s.db.QueryRow(queryString, l.ID, i.ID)
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s locationService) GetTopLocations(user *quartermaster.User) ([]*quartermaster.Location, error) {
var locations []*quartermaster.Location
queryString := "SELECT id FROM locations WHERE parent_id IS NULL AND owner_id = $1"

@ -1,113 +1,27 @@
package utils
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(
strings.TrimSuffix(path, "/"),
handler,
),
)
}
type RootHandler func(http.ResponseWriter, *http.Request) error
func (rh RootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := rh(w, r)
if err == nil {
return
}
log.Println(err)
e, ok := err.(*errors.Error)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if errors.Is(errors.Permission, e) {
body := getErrorBody("Permission Denied")
w.WriteHeader(http.StatusForbidden)
w.Write(body)
}
if errors.Is(errors.Incorrect, e) && strings.Contains(string(e.Method), "login") {
body := getErrorBody("Invalid Login Credentials")
w.WriteHeader(http.StatusUnauthorized)
w.Write(body)
}
if errors.Is(errors.Incorrect, e) && strings.Contains(string(e.Method), "refresh") {
body := getErrorBody("Missing or Invalid Cookie")
w.WriteHeader(http.StatusUnauthorized)
w.Write(body)
}
if errors.Is(errors.Malformed, e) {
body := getErrorBody("Bad Request")
w.WriteHeader(http.StatusBadRequest)
w.Write(body)
}
if errors.Is(errors.Internal, e) {
body := getErrorBody("Internal Server Error")
w.WriteHeader(http.StatusInternalServerError)
w.Write(body)
}
if errors.Is(errors.Conflict, e) && strings.Contains(string(e.Method), "register") {
body := getErrorBody("User already exists")
w.WriteHeader(http.StatusConflict)
w.Write(body)
}
}
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)
func UnitFromString(s string) (quartermaster.Unit, error) {
switch s {
case "Teaspoon":
return 0, nil
case "Tablespoon":
return 1, nil
case "Cup":
return 2, nil
case "Ounce":
return 3, nil
case "Gram":
return 4, nil
case "Pound":
return 5, nil
case "Individual":
return 6, nil
default:
return -1, errors.E(errors.Method("utils/UnitFromString"), errors.Malformed, "not a valid unit")
}
return tkn, nil
}

@ -1,14 +1,12 @@
package quartermaster
import "database/sql"
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Size float32 `json:"size"`
Unit Unit `json:"unit"`
Barcode sql.NullString `json:"barcode"`
Barcode string `json:"barcode"`
Nutrition *Nutrition `json:"nutrition,omitempty"`
}
@ -17,11 +15,13 @@ type ItemService interface {
AddItem(*Item, *Location, *User) (*Item, error)
UpdateItem(*Item, *User) error
MoveItem(item *Item, old *Location, new *Location, user *User) error
RemoveItem(*Item, *Location) error
RemoveItem(*Item, *Location, *User) error
DeleteItem(*Item, *User) error
GetItemByBarcode(barcode string, user *User) (*Item, error)
AddGroup(*Item, *Group) error
AddCategory(*Item, *Category) error
RemoveGroup(*Item, *Group) error
RemoveCategory(*Item, *Category) error
GetItemLocations(*Item, *User) (map[int]int, error)
IsOwner(*Item, *User) (bool, error)
}

@ -14,5 +14,6 @@ type LocationService interface {
DeleteLocation(*Location, *User) error
GetChildren(*Location, *User) ([]*Location, error)
GetItems(*Location, *User) (map[*Item]int, error)
GetItemCount(*Location, *Item, *User) (int, error)
GetTopLocations(*User) ([]*Location, error)
}

@ -1,9 +1,10 @@
package api
import (
"github.com/dustinpianalto/quartermaster/internal/utils"
"github.com/dustinpianalto/quartermaster/pkg/api/items"
"github.com/dustinpianalto/quartermaster/pkg/api/locations"
"github.com/dustinpianalto/quartermaster/pkg/api/users"
"github.com/dustinpianalto/quartermaster/pkg/utils"
"github.com/gorilla/mux"
)
@ -12,5 +13,6 @@ func GetRouter() *mux.Router {
router.HandleFunc("/healthcheck", healthcheck).Methods("GET")
utils.Mount(router, "/users", users.GetRouter())
utils.Mount(router, "/locations", locations.GetRouter())
utils.Mount(router, "/items", items.GetRouter())
return router
}

@ -0,0 +1,16 @@
package items
import (
"github.com/dustinpianalto/quartermaster/pkg/utils"
"github.com/gorilla/mux"
)
func GetRouter() *mux.Router {
router := mux.NewRouter()
router.Handle("/", utils.AuthenticationMiddleware(items))
router.Handle("/getItemByBarcode/{barcode}", utils.AuthenticationMiddleware(getItemByBarcode))
router.Handle("/{id}/moveItem", utils.AuthenticationMiddleware(moveItem))
router.Handle("/{id}/removeItem", utils.AuthenticationMiddleware(removeItem))
router.Handle("/{id}/getLocationCount", utils.AuthenticationMiddleware(getLocationCount))
return router
}

@ -0,0 +1,203 @@
package items
import (
"encoding/json"
"net/http"
"strconv"
"github.com/dustinpianalto/errors"
"github.com/dustinpianalto/quartermaster"
"github.com/dustinpianalto/quartermaster/pkg/services"
"github.com/gorilla/mux"
)
type addItemRequest struct {
Location quartermaster.Location `json:"location"`
quartermaster.Item
}
type itemsResponse struct {
Count int `json:"count"`
*quartermaster.Item
}
func items(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items"
if r.Method == "POST" {
err := addItem(w, r, user)
if err != nil {
return errors.E(method, "there was a problem adding the item", err)
}
} else if r.Method == "GET" {
err := getItems(w, r, user)
if err != nil {
return errors.E(method, "there was a problem getting items", err)
}
} else {
return errors.E(method, errors.Malformed, "http method not allowed")
}
return nil
}
func addItem(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/addItem"
var ir addItemRequest
err := json.NewDecoder(r.Body).Decode(&ir)
if err != nil {
return errors.E(method, errors.Malformed, "failed to decode item request", err)
}
if ir.Name == "" || ir.Description == "" || ir.Size == 0.0 || ir.Barcode == "" {
return errors.E(method, errors.Malformed, "name, description, and size, and barcode are requred")
}
i, err := services.ItemService.AddItem(&ir.Item, &ir.Location, user)
if err != nil {
return errors.E(method, errors.Internal, "error adding item to location", err)
}
iJson, err := json.Marshal(i)
if err != nil {
return errors.E(method, errors.Internal, "error marshalling item", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusCreated)
(*w).Write(iJson)
return nil
}
func getItems(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/getItems"
var l *quartermaster.Location
err := json.NewDecoder(r.Body).Decode(&l)
if err != nil {
return errors.E(method, errors.Malformed, "failed to decode location", err)
}
if l.ID == 0 {
return errors.E(method, errors.Malformed, "id is required")
}
items, err := services.LocationService.GetItems(l, user)
if err != nil {
return errors.E(method, "problem getting items", err)
}
var itemsResp []itemsResponse
for i, c := range items {
itemsResp = append(itemsResp, itemsResponse{Count: c, Item: i})
}
itemsJson, err := json.Marshal(itemsResp)
if err != nil {
return errors.E(method, errors.Internal, "error marshalling items", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(itemsJson)
return nil
}
func getItemByBarcode(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/getItemByBarcode"
params := mux.Vars(r)
barcodeString := params["barcode"]
item, err := services.ItemService.GetItemByBarcode(barcodeString, user)
if err != nil {
return errors.E(method, "problem getting items", err)
}
if item != nil {
itemJson, err := json.Marshal(item)
if err != nil {
return errors.E(method, errors.Internal, "error marshalling items", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(itemJson)
} else {
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusNotFound)
}
return nil
}
func moveItem(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/moveItem"
params := mux.Vars(r)
itemID, err := strconv.Atoi(params["id"])
if err != nil {
return errors.E(method, errors.Malformed, "id must be a valid number")
}
var req map[string]int
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return errors.E(method, errors.Malformed, "failed to decode request", err)
}
if _, ok := req["old_id"]; !ok {
return errors.E(method, errors.Malformed, "old_id is required")
}
if _, ok := req["new_id"]; !ok {
return errors.E(method, errors.Malformed, "new_id is required")
}
item := &quartermaster.Item{
ID: itemID,
}
old := &quartermaster.Location{
ID: req["old_id"],
}
new := &quartermaster.Location{
ID: req["new_id"],
}
err = services.ItemService.MoveItem(item, old, new, user)
if err != nil {
return errors.E(method, errors.Internal, "error moving item", err)
}
(*w).WriteHeader(http.StatusOK)
return nil
}
func removeItem(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/removeItem"
params := mux.Vars(r)
itemID, err := strconv.Atoi(params["id"])
if err != nil {
return errors.E(method, errors.Malformed, "id must be a valid number")
}
var req map[string]int
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return errors.E(method, errors.Malformed, "failed to decode request", err)
}
if _, ok := req["location_id"]; !ok {
return errors.E(method, errors.Malformed, "location_id is required")
}
item := &quartermaster.Item{
ID: itemID,
}
location := &quartermaster.Location{
ID: req["location_id"],
}
err = services.ItemService.RemoveItem(item, location, user)
if err != nil {
return errors.E(method, errors.Internal, "error removing item", err)
}
(*w).WriteHeader(http.StatusOK)
return nil
}
func getLocationCount(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/getLocationCount"
params := mux.Vars(r)
itemID, err := strconv.Atoi(params["id"])
if err != nil {
return errors.E(method, errors.Malformed, "id must be a valid number", err)
}
i := &quartermaster.Item{
ID: itemID,
}
locations, err := services.ItemService.GetItemLocations(i, user)
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
}

@ -1,12 +1,18 @@
package locations
import (
"github.com/dustinpianalto/quartermaster/internal/utils"
"github.com/dustinpianalto/quartermaster/pkg/utils"
"github.com/gorilla/mux"
)
func GetRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
router.Handle("/", utils.RootHandler(locations))
router := mux.NewRouter()
router.Handle("/", utils.AuthenticationMiddleware(locations))
router.Handle("/{id}/getChildren", utils.AuthenticationMiddleware(getChildren))
router.Handle("/{id}/items", utils.AuthenticationMiddleware(getItems))
router.Handle("/{id}/addItem/{item_id}", utils.AuthenticationMiddleware(addItem))
router.Handle("/{id}/removeItem/{item_id}", utils.AuthenticationMiddleware(removeItem))
router.Handle("/{id}/getItemCount/{item_id}", utils.AuthenticationMiddleware(getItemCount))
router.Handle("/{id}", utils.AuthenticationMiddleware(getLocation))
return router
}

@ -1,35 +1,31 @@
package locations
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
"github.com/dustinpianalto/errors"
"github.com/dustinpianalto/quartermaster"
"github.com/dustinpianalto/quartermaster/internal/utils"
"github.com/dustinpianalto/quartermaster/pkg/services"
"github.com/gorilla/mux"
)
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)
}
type itemsResponse struct {
Count int `json:"count"`
*quartermaster.Item
}
func locations(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "locations"
if r.Method == "POST" {
err = addLocation(w, r, user)
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)
err := getTopLocations(w, r, user)
if err != nil {
return errors.E(method, "there was a problem getting locations", err)
}
@ -39,7 +35,7 @@ func locations(w http.ResponseWriter, r *http.Request) error {
return nil
}
func addLocation(w http.ResponseWriter, r *http.Request, u *quartermaster.User) error {
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)
@ -63,14 +59,14 @@ func addLocation(w http.ResponseWriter, r *http.Request, u *quartermaster.User)
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)
(*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"
func getTopLocations(w *http.ResponseWriter, r *http.Request, u *quartermaster.User) error {
const method errors.Method = "locations/getTopLocations"
locations, err := services.LocationService.GetTopLocations(u)
if err != nil {
return errors.E(method, errors.Internal, "error getting locations", err)
@ -79,8 +75,207 @@ func getTopLocations(w http.ResponseWriter, r *http.Request, u *quartermaster.Us
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)
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(locationsJson)
return nil
}
func getChildren(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "locations/getChildren"
params := mux.Vars(r)
idString := params["id"]
id, err := strconv.Atoi(idString)
if err != nil {
return errors.E(method, errors.Malformed, "id must be a valid number")
}
location, err := services.LocationService.Location(id, user)
if err != nil {
if err == sql.ErrNoRows {
return errors.E(method, errors.Malformed, "location not found", err)
}
return errors.E(method, errors.Internal, "probem retrieving location", err)
}
children, err := services.LocationService.GetChildren(location, user)
if err != nil {
return errors.E(method, errors.Internal, "problem retrieving children", err)
}
var childrenJson []byte
if children == nil {
childrenJson = []byte("[]")
} else {
childrenJson, err = json.Marshal(children)
}
if err != nil {
return errors.E(method, errors.Internal, "problem marshalling children", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(childrenJson)
return nil
}
func getLocation(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "locations/getLocation"
params := mux.Vars(r)
idString := params["id"]
id, err := strconv.Atoi(idString)
if err != nil {
return errors.E(method, errors.Malformed, "id must be a valid number")
}
location, err := services.LocationService.Location(id, user)
if err != nil {
if err == sql.ErrNoRows {
return errors.E(method, errors.Malformed, "location not found", err)
}
return errors.E(method, errors.Internal, "robem retrieving location", err)
}
locationJson, err := json.Marshal(location)
if err != nil {
return errors.E(method, errors.Internal, "problem marshalling location", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(locationJson)
return nil
}
func getItems(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "locations/getItems"
params := mux.Vars(r)
idString := params["id"]
id, err := strconv.Atoi(idString)
if err != nil {
return errors.E(method, errors.Malformed, "id must be a valid number")
}
location, err := services.LocationService.Location(id, user)
if err != nil {
return errors.E(method, errors.Malformed, "location not found", err)
}
items, err := services.LocationService.GetItems(location, user)
if err != nil {
return errors.E(method, "problem getting items", err)
}
var itemsResp []itemsResponse
for i, c := range items {
itemsResp = append(itemsResp, itemsResponse{Count: c, Item: i})
}
var itemsJson []byte
if itemsResp == nil {
itemsJson = []byte("[]")
} else {
itemsJson, err = json.Marshal(itemsResp)
}
if err != nil {
return errors.E(method, errors.Internal, "error marshalling items", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(itemsJson)
return nil
}
func addItem(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/addItem"
params := mux.Vars(r)
idString := params["id"]
id, err := strconv.Atoi(idString)
if err != nil {
return errors.E(method, errors.Malformed, "location id must be a valid number")
}
location, err := services.LocationService.Location(id, user)
if err != nil {
return errors.E(method, errors.Malformed, "location not found", err)
}
itemIDString := params["item_id"]
itemID, err := strconv.Atoi(itemIDString)
if err != nil {
return errors.E(method, errors.Malformed, "item id must be a valid number")
}
item, err := services.ItemService.Item(itemID, user)
if err != nil {
return errors.E(method, errors.Malformed, "item not found", err)
}
i, err := services.ItemService.AddItem(item, location, user)
if err != nil {
return errors.E(method, errors.Internal, "error adding item to location", err)
}
iJson, err := json.Marshal(i)
if err != nil {
return errors.E(method, errors.Internal, "error marshalling item", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusCreated)
(*w).Write(iJson)
return nil
}
func removeItem(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/removeItem"
params := mux.Vars(r)
idString := params["id"]
id, err := strconv.Atoi(idString)
if err != nil {
return errors.E(method, errors.Malformed, "location id must be a valid number")
}
location, err := services.LocationService.Location(id, user)
if err != nil {
return errors.E(method, errors.Malformed, "location not found", err)
}
itemIDString := params["item_id"]
itemID, err := strconv.Atoi(itemIDString)
if err != nil {
return errors.E(method, errors.Malformed, "item id must be a valid number")
}
item, err := services.ItemService.Item(itemID, user)
if err != nil {
return errors.E(method, errors.Malformed, "item not found", err)
}
err = services.ItemService.RemoveItem(item, location, user)
if err != nil {
return errors.E(method, errors.Internal, "error removing item from location", err)
}
iJson, err := json.Marshal(struct{ Status string }{Status: "ok"})
if err != nil {
return errors.E(method, errors.Internal, "error marshalling item", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusCreated)
(*w).Write(iJson)
return nil
}
func getItemCount(w *http.ResponseWriter, r *http.Request, user *quartermaster.User) error {
const method errors.Method = "items/getItemCount"
params := mux.Vars(r)
idString := params["id"]
id, err := strconv.Atoi(idString)
if err != nil {
return errors.E(method, errors.Malformed, "location id must be a valid number")
}
location, err := services.LocationService.Location(id, user)
if err != nil {
return errors.E(method, errors.Malformed, "location not found", err)
}
itemIDString := params["item_id"]
itemID, err := strconv.Atoi(itemIDString)
if err != nil {
return errors.E(method, errors.Malformed, "item id must be a valid number")
}
item, err := services.ItemService.Item(itemID, user)
if err != nil {
return errors.E(method, errors.Malformed, "item not found", err)
}
c, err := services.LocationService.GetItemCount(location, item, user)
if err != nil {
return errors.E(method, errors.Internal, "error getting count of items in location", err)
}
iJson, err := json.Marshal(struct{ Count int }{Count: c})
if err != nil {
return errors.E(method, errors.Internal, "error marshalling item", err)
}
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusCreated)
(*w).Write(iJson)
return nil
}

@ -1,7 +1,7 @@
package users
import (
"github.com/dustinpianalto/quartermaster/internal/utils"
"github.com/dustinpianalto/quartermaster/pkg/utils"
"github.com/gorilla/mux"
)

@ -3,20 +3,17 @@ package users
import (
"encoding/json"
"net/http"
"os"
"time"
"github.com/dustinpianalto/errors"
"github.com/dustinpianalto/quartermaster"
ijwt "github.com/dustinpianalto/quartermaster/internal/jwt"
"github.com/dustinpianalto/quartermaster/pkg/services"
"github.com/golang-jwt/jwt"
"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 {
func loginHandler(w *http.ResponseWriter, r *http.Request) error {
const method errors.Method = "users/login"
var userReq quartermaster.User
err := json.NewDecoder(r.Body).Decode(&userReq)
@ -34,7 +31,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) error {
}
expires := time.Now().Add(10 * time.Minute)
claims := &quartermaster.Claims{
claims := quartermaster.Claims{
ID: user.ID,
Username: user.Username,
StandardClaims: jwt.StandardClaims{
@ -42,26 +39,27 @@ func loginHandler(w http.ResponseWriter, r *http.Request) error {
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
tokenString, err := token.SignedString(jwtKey)
tokenString, err := ijwt.CreateJWTToken(claims)
if err != nil {
return errors.E(method, errors.Username(user.Username), errors.Internal, "error creating token", err)
}
t := quartermaster.TokenResponse{
Token: tokenString,
}
tJson, err := json.Marshal(t)
if err != nil {
return errors.E(method, errors.Username(user.Username), errors.Internal, "error signing token", err)
return errors.E(method, errors.Internal, "error marshalling token", err)
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenString,
Expires: expires,
Path: "/",
HttpOnly: false,
Secure: false,
SameSite: http.SameSiteLaxMode,
})
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(tJson)
return nil
}
func registerHandler(w http.ResponseWriter, r *http.Request) error {
func registerHandler(w *http.ResponseWriter, r *http.Request) error {
const method errors.Method = "users/register"
var user *quartermaster.User
err := json.NewDecoder(r.Body).Decode(&user)
@ -83,34 +81,11 @@ func registerHandler(w http.ResponseWriter, r *http.Request) error {
return nil
}
func refreshHandler(w http.ResponseWriter, r *http.Request) error {
func refreshHandler(w *http.ResponseWriter, r *http.Request) error {
const method errors.Method = "users/refresh"
c, err := r.Cookie("token")
if err != nil {
if err == http.ErrNoCookie {
return errors.E(method, errors.Incorrect, "cookie not found", err)
}
return 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
})
claims, err := ijwt.AuthenticateJWTToken(r)
if err != nil {
if err == jwt.ErrSignatureInvalid {
return errors.E(method, errors.Incorrect, "cookie is invalid", err)
}
e, _ := err.(*jwt.ValidationError)
if e.Inner == jwt.ErrInvalidKeyType {
return errors.E(method, errors.Internal, err)
} else if e.Inner == jwt.ErrHashUnavailable {
return errors.E(method, errors.Internal, err)
}
return errors.E(method, errors.Malformed, "failed to parse cookie", err)
}
if !tkn.Valid {
return errors.E(method, errors.Incorrect, "cookie is invalid", err)
return errors.E(method, errors.Malformed, "error with authentication header", err)
}
// We ensure that a new token is not issued until enough time has elapsed
@ -123,22 +98,22 @@ func refreshHandler(w http.ResponseWriter, r *http.Request) error {
// Now, create a new token for the current use, with a renewed expiration time
expirationTime := time.Now().Add(10 * time.Minute)
claims.StandardClaims.ExpiresAt = expirationTime.Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
tokenString, err := ijwt.CreateJWTToken(*claims)
if err != nil {
return errors.E(method, errors.Internal, "error creating token", err)
}
t := quartermaster.TokenResponse{
Token: tokenString,
}
tJson, err := json.Marshal(t)
if err != nil {
return errors.E(method, errors.Internal, "error signing token", err)
return errors.E(method, errors.Internal, "error marshalling token", err)
}
// Set the new token as the users `token` cookie
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenString,
Expires: expirationTime,
Path: "/",
HttpOnly: false,
Secure: false,
SameSite: http.SameSiteLaxMode,
})
(*w).Header().Set("Content-Type", "application/json")
(*w).WriteHeader(http.StatusOK)
(*w).Write(tJson)
return nil
}

File diff suppressed because it is too large Load Diff

@ -12,10 +12,14 @@
"core-js": "^3.6.5",
"jwt-decode": "^3.1.2",
"material-design-icons-iconfont": "^6.1.1",
"qrious": "^4.0.2",
"sass": "^1.43.4",
"sass-loader": "^10.2.0",
"vue": "^2.6.11",
"vue-barcode-reader": "^0.0.3",
"vue-barcode-reader-plus": "^1.2.2",
"vue-cookies": "^1.7.4",
"vue-html-to-paper": "^1.4.4",
"vue-router": "^3.5.3",
"vuetify": "^2.6.0",
"vuex": "^3.6.2"

@ -14,7 +14,8 @@
import AppBar from "@/components/AppBar.vue";
import Drawer from "@/components/Drawer.vue";
import StaticFab from "@/components/StaticFab.vue";
import axios from "axios";
import { API } from "./utils/api_common";
import jwt_decode from "jwt-decode";
export default {
name: "App",
@ -25,28 +26,43 @@ export default {
},
data: () => ({
timer: null
timer: null,
}),
mounted() {
if (
localStorage.getItem("token") &&
jwt_decode(localStorage.getItem("token")).exp < Date.now() / 1000
) {
localStorage.removeItem("token");
this.$router.push({ name: "Login" });
}
this.$store.dispatch("loadCurrentUser");
this.timer = setInterval(this.refreshUser.bind(this), 10000);
},
methods: {
refreshUser() {
let token = localStorage.getItem("token");
if (
this.$store.state.currentUser &&
this.$store.state.currentUser.exp - Date.now() / 1000 < 120
token &&
jwt_decode(token).exp >= Date.now() / 1000 &&
jwt_decode(token).exp - Date.now() / 1000 < 120
) {
axios
.get("http://localhost:8000/api/users/refresh")
API
.get("api/v1/users/refresh", {
headers: { Authorization: localStorage.getItem("token") },
})
.then((response) => {
if (response.status == 200) {
if (response.status == 200 && response.data.token) {
localStorage.setItem("token", response.data.token);
this.$store.dispatch("loadCurrentUser");
}
});
}
} else if (token && jwt_decode(token).exp < Date.now() / 1000) {
localStorage.removeItem("token");
this.$router.push({ name: "Login" });
} else {
this.$store.dispatch("loadCurrentUser");
}
},
cancelAutoUpdate() {
clearInterval(this.timer);

@ -0,0 +1,234 @@
<template>
<v-card class="pa-5" width="600px">
<v-form dark v-model="valid" ref="form">
<StreamBarcodeReader v-if="barcodeReader != 'none'"
@decode="(a, b, c) => onDecode(a, b, c)"
@loaded="() => onLoad()"
></StreamBarcodeReader>
<v-text-field
v-model="barcode"
label="Barcode"
outlined
filled
dense
dark
counter="100"
:rules="[required('Barcode'), minLength('Barcode', 2), maxLength('Barcode', 100)]"
v-on:keyup.enter="submitFunc()"
append-icon="mdi-barcode-scan"
@click:append="barcodeReader = 'barcode'"
></v-text-field>
<v-text-field
v-model="name"
label="Name"
outlined
filled
dense
dark
counter="30"
:rules="[required('Name'), minLength('Name', 2), maxLength('Name', 30)]"
v-on:keyup.enter="submitFunc()"
></v-text-field>
<v-textarea
v-model="description"
label="Description"
outlined
dense
dark
filled
auto-grow
counter="true"
:rules="[required('Description')]"
v-on:keyup.enter="submitFunc()"
></v-textarea>
<v-text-field
v-model.number="size"
type="number"
label="Size"
outlined
filled
dense
dark
v-on:keyup.enter="submitFunc()"
></v-text-field>
<v-select
v-model="unit"
:items="units"
label="Units"
persistent-hint
outlined
dense
filled
return-object
single-line
></v-select>
<v-text-field
v-model="locationName"
label="Location"
outlined
filled
dense
dark
counter="100"
:readonly="true"
:rules="[required('Location'), minLength('Location', 2), maxLength('Location', 100)]"
v-on:keyup.enter="submitFunc()"
append-icon="mdi-barcode-scan"
@click:append="barcodeReader = 'location'"
></v-text-field>
<div class="text-center">
<v-btn
class="signin-btn"
rounded
color--text="white"
dark
:disabled="!valid"
@click="submitFunc()"
>
Add Item
</v-btn>
</div>
</v-form>
<v-container class="pt-50">
<v-alert
v-model="success"
prominent
dense
type="success"
transition="slide-y-transition"
>
{{ successMessage }}
</v-alert>
<v-alert
v-model="error"
prominent
dense
type="error"
transition="slide-y-transition"
>
{{ errorMessage }}
</v-alert>
</v-container>
</v-card>
</template>
<script>
import validations from "@/utils/validations";
import { StreamBarcodeReader } from "vue-barcode-reader";
import { API } from "../utils/api_common";
import { mapState } from "vuex";
export default {
name: "AddItemForm",
data: () => ({
valid: false,
name: "",
description: "",
size: null,
unit: "",
units: [
{ value: 0, text: "Teaspoon" },
{ value: 1, text: "Tablespoon" },
{ value: 2, text: "Cup" },
{ value: 3, text: "Ounce" },
{ value: 4, text: "Gram" },
{ value: 5, text: "Pound" },
{ value: 6, text: "Individual" },
],
barcode: "",
barcodeReader: "none",
location: null,
locationName: "",
successMessage: null,
errorMessage: null,
success: false,
error: false,
open: [],
...validations,
}),
components: {
StreamBarcodeReader
},
props: ["defaultLocation", "closeFunc"],
mounted() {
//TODO: Process defaultLocation
},
computed: {
items() {
return [];
},
},
methods: {
onDecode(a, b, c) {
if (this.barcodeReader == 'barcode') {
this.barcode = a
API.get("api/v1/items/getItemByBarcode/" + a,
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
).then((response) => {
if (response.status == 200) {
this.name = response.data.name
this.description = response.data.description
this.size = response.data.size
this.unit = response.data.unit
console.log(this.unit)
}
})
} else if (this.barcodeReader == 'location') {
var loc = JSON.parse(a);
this.location = loc.id
this.locationName = loc.name
}
this.barcodeReader = 'none'
},
onLoad() {
console.log("barcode reader loaded")
},
submitFunc() {
let data = {
name: this.name,
description: this.description,
size: this.size,
unit: this.unit.id,
location: {"id": this.location},
barcode: this.barcode,
};
console.log(data)
API
.post("api/v1/items/", data, {
headers: {
Authorization: localStorage.getItem("token"),
},
})
.then((response) => {
if (response.status == 201) {
this.successMessage = "Item Added";
this.success = true;
this.$refs.form.reset();
console.log(response.data)
return
setTimeout(() => {
this.$router.push({
name: "Item",
params: { id: response.data.id },
});
this.success = false;
this.successMessage = "";
this.closeFunc("addItem");
}, 2000);
}
})
.catch((error) => {
this.errorMessage = error.response.data.error;
this.error = true;
setTimeout(() => {
this.errorMessage = null;
this.error = false;
}, 3000);
});
},
},
};
</script>

@ -0,0 +1,185 @@
<template>
<v-card class="pa-5" width="600px">
<v-form dark v-model="valid" ref="form">
<v-text-field
v-model="name"
label="Name"
outlined
filled
dense
dark
counter="30"
:rules="[required('Name'), minLength('Name', 2), maxLength('Name', 30)]"
v-on:keyup.enter="submitFunc()"
></v-text-field>
<v-textarea
v-model="description"
label="Description"
outlined
dense
dark
filled
auto-grow
counter="true"
:rules="[required('Description')]"
v-on:keyup.enter="submitFunc()"
></v-textarea>
<v-treeview
:active.sync="active"
:items="items"
:load-children="fetchChildren"
:open.sync="open"
activatable
selection-type="independent"
dark
color="warning"
transition
>
<template v-slot:prepend="{ item }">
<v-icon v-if="!item.children"> mdi-map-marker </v-icon>
</template>
</v-treeview>
<div class="text-center">
<v-btn
class="signin-btn"
rounded
color--text="white"
dark
:disabled="!valid"
@click="submitFunc()"
>
Add Location
</v-btn>
</div>
</v-form>
<v-container class="pt-50">
<v-alert
v-model="success"
prominent
dense
type="success"
transition="slide-y-transition"
>
{{ successMessage }}
</v-alert>
<v-alert
v-model="error"
prominent
dense
type="error"
transition="slide-y-transition"
>
{{ errorMessage }}
</v-alert>
</v-container>
</v-card>
</template>
<script>
import validations from "@/utils/validations";
import { API } from "../utils/api_common";
import { mapState } from "vuex";
export default {
name: "AddLocationForm",
data: () => ({
valid: false,
name: "",
description: "",
parent: null,
successMessage: null,
errorMessage: null,
success: false,
error: false,
active: [],
open: [],
...validations,
...mapState(["topLocations"]),
}),
props: ["defaultParent", "closeFunc"],
computed: {
items() {
let locations = [];
if (this.topLocations() == null) {
this.$store.dispatch("loadTopLocations");
}
for (let l of this.topLocations()) {
locations.push(l);
}
return locations;
},
},
mounted() {
this.$refs.form.reset();
this.$store.dispatch("loadTopLocations");
},
methods: {
async fetchChildren(item) {
await API
.get(
"api/v1/locations/" + item.id + "/getChildren",
{
headers: {
Authorization: localStorage.getItem("token"),
},
}
)
.then((response) => {
if (response.status == 200 && response.data) {
for (let l of response.data) {
l.children = [];
}
item.children = response.data;
} else {
item.children = null;
}
})
.catch((error) => {
console.log(error.response.data);
});
},
submitFunc() {
let data = {
name: this.name,
description: this.description,
parent: null,
};
if (this.active.length == 1) {
data.parent = {
id: this.active[0],
};
}
API
.post("api/v1/locations/", data, {
headers: {
Authorization: localStorage.getItem("token"),
},
})
.then((response) => {
if (response.status == 201) {
this.successMessage = "Location Added";
this.success = true;
this.$refs.form.reset();
setTimeout(() => {
this.$router.push({
name: "Location",
params: { id: response.data.id },
});
this.success = false;
this.successMessage = "";
this.closeFunc("addLocation");
this.$store.dispatch("loadTopLocations");
}, 2000);
}
})
.catch((error) => {
this.errorMessage = error.response.data.error;
this.error = true;
setTimeout(() => {
this.errorMessage = null;
this.error = false;
}, 3000);
});
},
},
};
</script>

@ -2,11 +2,11 @@
<v-app-bar app dark flat>
<v-app-bar-nav-icon @click="toggleDrawer()"></v-app-bar-nav-icon>
<v-toolbar-title>Application</v-toolbar-title>
<v-toolbar-title>Quartermaster</v-toolbar-title>
<v-spacer />
<v-btn v-if="!currentUser" text class="mx-2" :to="{ name: 'Register' }">Register</v-btn>
<v-btn v-if="!currentUser" text class="mx-2" :to="{ name: 'Login' }">Login</v-btn>
<v-btn v-if="currentUser" text class="mx-2" @click="logout">Logout</v-btn>
<v-btn v-if="!currentUser" text class="mx-1" :to="{ name: 'Register' }">Register</v-btn>
<v-btn v-if="!currentUser" text class="mx-1" :to="{ name: 'Login' }">Login</v-btn>
<v-btn v-if="currentUser" text class="mx-1" @click="logout">Logout</v-btn>
</v-app-bar>
</template>
@ -19,7 +19,7 @@ export default {
this.$store.commit("toggleDrawer");
},
logout() {
this.$cookies.remove('token')
localStorage.removeItem('token')
this.$router.push({ name: "Login" });
},
},

@ -0,0 +1,73 @@
<template>
<v-container>
<v-card class="pa-5" width="600px">
<StreamBarcodeReader v-if="barcodeReader"
@decode="(a, b, c) => onDecode(a, b, c)"
@loaded="() => onLoad()"
></StreamBarcodeReader>
</v-card>
<v-dialog dark transition="dialog-bottom-transition" v-model="itemDetailsDialog" width="auto">
<ItemDetailsCard :closeFunc="closeDialog" :item="item"/>
</v-dialog>
</v-container>
</template>
<script>
import { StreamBarcodeReader } from "vue-barcode-reader";
import { API } from "../utils/api_common";
import ItemDetailsCard from "./ItemDetailsCard.vue"
export default {
name: "BarcodeDialog",
data: () => ({
item: null,
barcodeReader: true,
itemDetailsDialog: false,
}),
components: {
StreamBarcodeReader,
ItemDetailsCard,
},
props: ["closeFunc"],
mounted() {
this.barcodeReader = true
},
methods: {
onDecode(a, b, c) {
try{
let json = JSON.parse(a);
if (json.uri) {
this.barcodeReader = false;
this.closeFunc("barcode")
this.barcodeReader = true
window.location.href = json.uri
}
}catch{
API.get("api/v1/items/getItemByBarcode/" + a,
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
).then((response) => {
if (response.status == 200) {
this.item = response.data
this.itemDetailsDialog = true
}
})
this.barcodeReader = false
this.closeFunc("barcode")
this.barcodeReader = true
}
},
onLoad() {
console.log("barcode reader loaded")
},
closeDialog() {
this.itemDetailsDialog = false;
},
},
};
</script>

@ -1,151 +0,0 @@
<template>
<v-container>
<v-row class="text-center">
<v-col cols="12">
<v-img
:src="require('../assets/logo.svg')"
class="my-3"
contain
height="200"
/>
</v-col>
<v-col class="mb-4">
<h1 class="display-2 font-weight-bold mb-3">
Welcome to Vuetify
</h1>
<p class="subheading font-weight-regular">
For help and collaboration with other Vuetify developers,
<br>please join our online
<a
href="https://community.vuetifyjs.com"
target="_blank"
>Discord Community</a>
</p>
</v-col>
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
What's next?
</h2>
<v-row justify="center">
<a
v-for="(next, i) in whatsNext"
:key="i"
:href="next.href"
class="subheading mx-3"
target="_blank"
>
{{ next.text }}
</a>
</v-row>
</v-col>
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Important Links
</h2>
<v-row justify="center">
<a
v-for="(link, i) in importantLinks"
:key="i"
:href="link.href"
class="subheading mx-3"
target="_blank"
>
{{ link.text }}
</a>
</v-row>
</v-col>
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Ecosystem
</h2>
<v-row justify="center">
<a
v-for="(eco, i) in ecosystem"
:key="i"
:href="eco.href"
class="subheading mx-3"
target="_blank"
>
{{ eco.text }}
</a>
</v-row>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
name: 'HelloWorld',
data: () => ({
ecosystem: [
{
text: 'vuetify-loader',
href: 'https://github.com/vuetifyjs/vuetify-loader',
},
{
text: 'github',
href: 'https://github.com/vuetifyjs/vuetify',
},
{
text: 'awesome-vuetify',
href: 'https://github.com/vuetifyjs/awesome-vuetify',
},
],
importantLinks: [
{
text: 'Documentation',
href: 'https://vuetifyjs.com',
},
{
text: 'Chat',
href: 'https://community.vuetifyjs.com',
},
{
text: 'Made with Vuetify',
href: 'https://madewithvuejs.com/vuetify',
},
{
text: 'Twitter',
href: 'https://twitter.com/vuetifyjs',
},
{
text: 'Articles',
href: 'https://medium.com/vuetify',
},
],
whatsNext: [
{
text: 'Explore components',
href: 'https://vuetifyjs.com/components/api-explorer',
},
{
text: 'Select a layout',
href: 'https://vuetifyjs.com/getting-started/pre-made-layouts',
},
{
text: 'Frequently Asked Questions',
href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions',
},
],
}),
}
</script>

@ -0,0 +1,51 @@
<template>
<v-container>
<v-card dark class="pa-5" @click="itemDetailsDialog = true">
<v-list-item three-line>
<v-list-item-content>
<div class="text-right">
{{this.item.count}}
</div>
<v-list-item-title class="text-h5 mb-1">
{{this.item.name}}
</v-list-item-title>
<v-list-item-subtitle>
{{this.item.description}}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-card-actions>
<v-spacer/>
<v-btn fab dark small @click="addFunc(item.id)">
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-btn fab dark small @click="removeFunc(item.id)">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-actions>
</v-card>
<v-dialog dark transition="dialog-bottom-transition" v-model="itemDetailsDialog" width="auto">
<ItemDetailsCard :closeFunc="closeDialog" :item="item"/>
</v-dialog>
</v-container>
</template>
<script>
import ItemDetailsCard from "./ItemDetailsCard.vue"
export default {
name: "ItemCard",
props: ["item", "addFunc", "removeFunc"],
data: () => ({
itemDetailsDialog: false,
}),
methods: {
closeDialog() {
this.itemDetailsDialog = false;
}
},
components: {
ItemDetailsCard
}
}
</script>

@ -0,0 +1,134 @@
<template>
<v-card dark class="pa-5" max-width="600px">
<v-card-title class="text-h3 mb-1">
{{this.item.name}}
</v-card-title>
<v-card-subtitle>
Size: {{this.item.size}}
<br/>
Unit: {{this.units[this.item.unit]}}
</v-card-subtitle>
<v-card-text>
<p>
{{this.item.description}}
</p>
<p>
Barcode: {{this.item.barcode}}
</p>
</v-card-text>
<v-list>
<v-subheader>LOCATIONS</v-subheader>
<v-list-item
v-for="location in locations"
:key="location.name + location.count"
>
<v-list-item-content>
<v-list-item-title>{{location.name}} - {{location.count}}</v-list-item-title>
</v-list-item-content>
<v-btn fab dark small @click="addItem(item.id, location.id)">
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-btn fab dark small @click="removeItem(item.id, location.id)">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-list-item>
</v-list>
</v-card>
</template>
<script>
import { API } from '../utils/api_common';
export default {
name: "ItemCard",
props: ["item"],
data: () => ({
units: [
"Teaspoon",
"Tablespoon",
"Cup",
"Ounce",
"Gram",
"Pound",
"Individual",
],
locations: [],
}),
created() {
this.getLocations()
},
methods: {
async getLocations() {
let response = await API.get("api/v1/items/" + this.item.id + "/getLocationCount",
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
if (response.status == 200) {
let locs = []
for (const locID in response.data) {
let count = response.data[locID]
let locResp = await API.get("api/v1/locations/" + locID,
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
if (locResp.status == 200) {
locs.push({
id: locResp.data.id,
name: locResp.data.name,
count: count,
})
}
}
locs.sort(function(a,b) {
if (a.name < b.name) {
return -1
} else if (a.name > b.name) {
return 1
}
return 0
})
this.locations = locs
}
},
addItem(id, loc) {
API
.post("api/v1/locations/" + loc + "/addItem/" + id, {},
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
.then((response) => {
if (response.status == 201) {
this.getLocations()
}
})
},
removeItem(id, loc) {
API
.post("api/v1/locations/" + loc + "/removeItem/" + id, {},
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
.then((response) => {
if (response.status == 201) {
this.getLocations()
}
})
}
}
}
</script>

@ -0,0 +1,21 @@
<template>
<v-card dark class="pa-5" :to="{ name: 'Location', params: {id: location.id} }">
<v-list-item three-line>
<v-list-item-content>
<v-list-item-title class="text-h5 mb-1">
{{this.location.name}}
</v-list-item-title>
<v-list-item-subtitle>
{{this.location.description}}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-card>
</template>
<script>
export default {
name: "LocationCard",
props: ["location"],
}
</script>

@ -0,0 +1,29 @@
<template>
<v-card class="pa-5" width="600px">
<v-form dark v-model="valid" ref="form">
</v-form>
</v-card>
</template>
<script>
import validations from "@/utils/validations";
import { API } from "../utils/api_common";
import { mapState } from "vuex";
export default {
name: "SearchForm",
data: () => ({
}),
components: {
},
props: ["closeFunc"],
mounted() {
//TODO: Process defaultLocation
},
computed: {
},
methods: {
submitFunc() {
},
},
};
</script>

@ -1,4 +1,5 @@
<template>
<v-container>
<div id="static-fab">
<v-speed-dial
v-model="fab"
@ -16,7 +17,7 @@
</template>
<v-tooltip left>
<template v-slot:activator="{ on, attrs }">
<v-btn fab dark small color="blue" v-bind="attrs" v-on="on">
<v-btn fab dark small color="blue" v-bind="attrs" v-on="on" @click="addItemDialog = true">
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
@ -24,15 +25,23 @@
</v-tooltip>
<v-tooltip left>
<template v-slot:activator="{ on, attrs }">
<v-btn fab dark small color="red" v-bind="attrs" v-on="on">
<v-icon>mdi-close</v-icon>
<v-btn fab dark small color="purple" v-bind="attrs" v-on="on" @click="barcodeDialog = true">
<v-icon>mdi-barcode-scan</v-icon>
</v-btn>
</template>
<span>Remove Item</span>
<span>Scan Barcode</span>
</v-tooltip>
<!-- <v-tooltip left>
<template v-slot:activator="{ on, attrs }">
<v-btn fab dark small color="orange" v-bind="attrs" v-on="on">
<v-icon>mdi-magnify</v-icon>
</v-btn>
</template>
<span>Search</span>
</v-tooltip> -->
<v-tooltip left>
<template v-slot:activator="{ on, attrs }">
<v-btn fab dark small color="green" v-bind="attrs" v-on="on">
<v-btn fab dark small color="green" v-bind="attrs" v-on="on" @click="addLocationDialog = true">
<v-icon>mdi-map-marker</v-icon>
</v-btn>
</template>
@ -40,13 +49,53 @@
</v-tooltip>
</v-speed-dial>
</div>
<v-dialog dark transition="dialog-bottom-transition" v-model="addLocationDialog" width="auto">
<AddLocationForm :closeFunc="closeDialog"/>
</v-dialog>
<v-dialog dark transition="dialog-bottom-transition" v-model="addItemDialog" width="auto">
<AddItemForm :closeFunc="closeDialog"/>
</v-dialog>
<v-dialog dark transition="dialog-bottom-transition" v-model="barcodeDialog" width="auto">
<BarcodeDialog :closeFunc="closeDialog"/>
</v-dialog>
<v-dialog dark transition="dialog-bottom-transition" v-model="searchDialog" width="auto">
<SearchForm :closeFunc="closeDialog"/>
</v-dialog>
</v-container>
</template>
<script>
import AddLocationForm from "./AddLocationForm";
import AddItemForm from "./AddItemForm";
import BarcodeDialog from "./BarcodeDialog";
import SearchForm from "./SearchForm";
export default {
name: "StaticFab",
data: () => ({
fab: false,
addLocationDialog: null,
addItemDialog: null,
barcodeDialog: null,
searchDialog: null,
}),
components: {
AddLocationForm,
AddItemForm,
BarcodeDialog,
SearchForm,
},
methods: {
closeDialog(name) {
if (name == "addLocation") {
this.addLocationDialog = false
} else if (name == "addItem") {
this.addItemDialog = false
} else if (name == "barcode") {
this.barcodeDialog = false
} else if (name == "search") {
this.searchDialog = false
}
}
}
};
</script>

@ -0,0 +1,4 @@
module.exports = {
NODE_ENV: '"development"',
VUE_APP_BASE_URL: "http://quartermaster.djpianalto.com/",
}

@ -5,7 +5,11 @@ import router from './router'
import vuetify from './plugins/vuetify'
import 'material-design-icons-iconfont/dist/material-design-icons.css'
import store from './store'
import VueHtmlToPaper from 'vue-html-to-paper';
Vue.use(require('vue-cookies'))
Vue.use(VueHtmlToPaper)
import titleMixin from "./mixins/titleMixin"
Vue.mixin(titleMixin)
Vue.config.productionTip = false

@ -0,0 +1,16 @@
function getTitle (vm) {
const { title } = vm.$options
if (title) {
return typeof title === 'function'
? title.call(vm)
: title
}
}
export default {
mounted () {
const title = getTitle(this)
if (title) {
document.title = "Quartermaster - " + title
}
}
}

@ -1,8 +1,10 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import jwt_decode from 'jwt-decode'
import Dashboard from '../views/Dashboard.vue'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'
import Location from '../views/Location.vue'
import store from '../store'
Vue.use(VueRouter)
@ -34,6 +36,14 @@ const routes = [
name: 'Register',
component: Register,
},
{
path: '/location/:id',
name: 'Location',
component: Location,
meta: {
requiresAuth: true
}
}
]
const router = new VueRouter({
@ -46,11 +56,20 @@ router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!Vue.$cookies.get('token')) {
let token = localStorage.getItem('token')
if (!token) {
next({ name: 'Login' })
} else {
let claims = jwt_decode(token)
console.log(claims.exp)
console.log(Date.now()/1000)
if (claims.exp < Date.now() / 1000) {
localStorage.removeItem('token')
next({ name: "Login" })
} else {
next() // go to wherever I'm going
}
}
} else {
next() // does not require auth, make sure to always call next()!
}

@ -1,6 +1,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
import jwt_decode from 'jwt-decode'
import { API } from '../utils/api_common'
Vue.use(Vuex)
@ -8,6 +9,8 @@ export default new Vuex.Store({
state: {
drawer: null,
currentUser: null,
topLocations: [],
currentLocation: null,
},
getters: {
},
@ -23,17 +26,72 @@ export default new Vuex.Store({
},
SET_CURRENT_USER (state, user) {
state.currentUser = user
},
SET_TOP_LOCATIONS (state, locations) {
state.topLocations = locations
},
SET_CURRENT_LOCATION (state, location) {
state.currentLocation = location
}
},
actions: {
loadCurrentUser ({commit}) {
if (Vue.$cookies.isKey('token')) {
var user = jwt_decode(Vue.$cookies.get('token'));
let token = localStorage.getItem('token')
if (token) {
var user = jwt_decode(token);
commit('SET_CURRENT_USER', user)
} else {
commit('SET_CURRENT_USER', null)
}
},
loadTopLocations({commit}) {
API
.get("api/v1/locations/", {
headers: {
"Authorization": localStorage.getItem('token')
}
})
.then((response) => {
for (let l of response.data) {
l.children = []
API.get("api/v1/locations/" + l.id + "/getChildren", {
headers: {
"Authorization": localStorage.getItem('token')
}
})
.then((childrenResponse) => {
if (childrenResponse.status == 200) {
l.children = childrenResponse.data
} else {
l.children = []
}
})
.catch((error) => {
console.log(error)
l.children = []
})
}
commit('SET_TOP_LOCATIONS', response.data)
})
.catch((error) => {
commit('SET_TOP_LOCATIONS', null)
console.log(error.response.data.error)
})
},
async loadCurrentLocation({commit}, params) {
try {
let response = await API.get("api/v1/locations/" + params.id,
{
headers: {
"Authorization": localStorage.getItem('token')
}
})
commit('SET_CURRENT_LOCATION', response.data)
} catch(error) {
commit('SET_CURRENT_LOCATION', null)
console.log(error.response.data.error)
}
},
},
modules: {
}

@ -0,0 +1,5 @@
import axios from 'axios';
export const API = axios.create({
baseURL: 'https://quartermaster.djpianalto.com'
})

@ -3,3 +3,10 @@
<h1 class="grey--text lighten-4">This is an about page</h1>
</div>
</template>
<script>
export default {
name: "About",
title: "About",
}
</script>

@ -1,35 +1,33 @@
<template>
<v-container>
<v-row>
<template v-for="n in 4">
<v-col
:key="n"
class="mt-2"
cols="12"
>
<strong class="grey--text lighten-4">Category {{ n }}</strong>
<v-row :key="location.id + location.description + location.children.length" v-for="location in topLocations">
<v-col :key="location.id" class="mt-2" cols="12">
<v-btn text class="grey--text lighten-4" :to="{ name: 'Location', params: {id: location.id} }">{{
location.name
}}</v-btn>
</v-col>
<v-col
v-for="j in 6"
:key="`${n}${j}`"
cols="6"
md="2"
>
<v-sheet height="150"></v-sheet>
<v-col v-for="child in location.children" v-bind:key="`${child.id}`">
<LocationCard :location="child"/>
</v-col>
</template>
</v-row>
</v-container>
</template>
<script>
import { mapState } from "vuex";
import LocationCard from "@/components/LocationCard.vue"
export default {
name: 'Dashboard',
export default {
name: "Dashboard",
title: "Dashboard",
components: {
LocationCard,
},
computed: {
...mapState(["topLocations"]),
},
created() {
this.$store.dispatch("loadTopLocations");
},
}
};
</script>

@ -1,15 +0,0 @@
<template>
<hello-world />
</template>
<script>
import HelloWorld from '../components/HelloWorld'
export default {
name: 'Home',
components: {
HelloWorld,
},
}
</script>

@ -0,0 +1,197 @@
<template>
<v-container>
<strong class="grey--text text-h3 lighten-4" v-if="currentLocation">
{{currentLocation.name}}
</strong>
<br>
<v-btn dark class="ma-2" @click="displayQR = !displayQR">QR</v-btn>
<v-btn dark class="ma-2" @click="print">Print</v-btn>
<br>
<div id="qrcode-wrapper">
<img id="qrcode" v-show="this.displayQR"/>
</div>
<br>
<v-card
class="mx-auto grey darken-4"
max-width="800"
flat
>
<v-container fluid v-if="this.items.length > 0">
<strong class="grey--text text-h5 lighten-4">
Items
</strong>
<v-row dense class="mt-5">
<v-col
v-for="item in this.items"
:key="item.name + item.description + item.count"
:cols=6
>
<ItemCard :item="item" :addFunc="addItem" :removeFunc="removeItem"/>
</v-col>
</v-row>
</v-container>
<v-container fluid v-if="this.children.length > 0">
<strong class="grey--text text-h5 lighten-4">
Child Locations
</strong>
<v-row dense class="mt-5">
<v-col
v-for="location in this.children"
:key="location.name + location.description"
:cols=6
>
<LocationCard :location="location"/>
</v-col>
</v-row>
</v-container>
</v-card>
</v-container>
</template>
<script>
import { mapState } from "vuex";
import QRious from "qrious";
import ItemCard from "@/components/ItemCard.vue"
import LocationCard from "@/components/LocationCard.vue"
import { API } from '../utils/api_common';
export default {
name: "Location",
title() {
return "Location"
},
data: () => ({
items: [],
children: [],
displayQR: false,
}),
components: {
ItemCard,
LocationCard,
},
computed: {
...mapState(["currentLocation"]),
},
async created() {
await this.$store.dispatch("loadCurrentLocation", {id: this.$route.params.id});
this.getItems();
this.getChildren();
this.generateQR();
this.$watch(
() => this.$route.params,
(toParams, fromParams) => {
if (toParams !== fromParams) {
this.$store.dispatch("loadCurrentLocation", {id: this.$route.params.id});
this.getItems();
this.getChildren();
this.generateQR();
}
}
)
},
methods: {
generateQR() {
var qr = {
"name": this.currentLocation.name,
"id": this.currentLocation.id,
"uri": "https://quartermaster.djpianalto.com/#/location/" + this.currentLocation.id
}
var qr = new QRious({
element: document.getElementById('qrcode'),
level: "M",
value: JSON.stringify(qr),
size: 150
});
},
print() {
this.$htmlToPaper('qrcode-wrapper')
},
getItems() {
API
.get("api/v1/locations/" + this.$route.params.id + "/items",
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
.then((response) => {
if (response.status == 200) {
this.items = response.data
this.items.sort(function(a,b) {
if (a.name < b.name) {
return -1
} else if (a.name > b.name) {
return 1
}
return 0
})
}
})
},
getChildren() {
API
.get("api/v1/locations/" + this.$route.params.id + "/getChildren",
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
.then((response) => {
if (response.status == 200) {
this.children = response.data
this.children.sort(function(a,b) {
if (a.name < b.name) {
return -1
} else if (a.name > b.name) {
return 1
}
return 0
})
}
})
},
addItem(id) {
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].id == id) {
API
.post("api/v1/locations/" + this.$route.params.id + "/addItem/" + id, {},
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
.then((response) => {
if (response.status == 201) {
this.getItems()
}
})
}
}
},
removeItem(id) {
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].id == id) {
API
.post("api/v1/locations/" + this.$route.params.id + "/removeItem/" + id, {},
{
headers: {
Authorization: localStorage.getItem("token"),
}
}
)
.then((response) => {
if (response.status == 201) {
this.getItems()
}
})
}
}
}
},
};
</script>

@ -46,10 +46,11 @@
<script>
import UserAuthForm from "@/components/UserAuthForm.vue";
import axios from 'axios';
import { API } from '../utils/api_common';
export default {
name: "Login",
title: "Login",
components: {
UserAuthForm,
},
@ -61,10 +62,11 @@ export default {
}),
methods: {
async login(payload) {
axios
.post("http://localhost:8000/api/users/login", payload)
API
.post("api/v1/users/login", payload, { withCredentials: true })
.then((response) => {
if (response.status == 200) {
if (response.status == 200 && response.data.token) {
localStorage.setItem('token', response.data.token)
this.successMessage = "Successful Login";
this.success = true;
setTimeout(() => {

@ -41,10 +41,11 @@
<script>
import UserAuthForm from "@/components/UserAuthForm.vue"
import axios from 'axios'
import { API } from '../utils/api_common'
export default {
name: "Register",
title: "Register",
components: {
UserAuthForm,
},
@ -56,12 +57,12 @@ export default {
}),
methods: {
async register(payload) {
axios
.post("http://localhost:8000/api/users/register", payload)
API
.post("api/v1/users/register", payload)
.then((response) => {
if (response.status == 200) {
axios
.post("http://localhost:8000/api/users/login", payload)
API
.post("api/v1/users/login", payload)
.then((response) => {
if (response.status == 200) {
this.successMessage = "Registration Successful";

@ -1553,6 +1553,29 @@
resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
"@zxing/library@^0.15.1":
version "0.15.2"
resolved "https://registry.npmjs.org/@zxing/library/-/library-0.15.2.tgz"
integrity sha512-J+N88Eyg6eI2SKIk2YIkjjNICbMSqmLZnB3oD1S21Bi3k+Ddg2eKe/nW+Hce4NKAFAZtY1mdDM08Bj9eu87HSg==
dependencies:
ts-custom-error "^3.0.0"
optionalDependencies:
text-encoding "^0.7.0"
"@zxing/library@^0.19.1":
version "0.19.1"
resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.19.1.tgz#68932436a9cf860e2148a6d6ae38fe1e27ea23a8"
integrity sha512-rKwvl3Uuqs8yf364iU9l3HDDaIx8yPv+CH6DbtQaQr67VdKLG22G1ukEp9fOdDefE6tpLtRAdMnTrgtpiaKAZw==
dependencies:
ts-custom-error "^3.0.0"
optionalDependencies:
"@zxing/text-encoding" "~0.9.0"
"@zxing/text-encoding@~0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b"
integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz"
@ -1812,7 +1835,7 @@ aws4@^1.8.0:
axios@^0.24.0:
version "0.24.0"
resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
dependencies:
follow-redirects "^1.14.4"
@ -2207,7 +2230,7 @@ caller-path@^2.0.0:
callsite@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
resolved "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz"
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
callsites@^2.0.0:
@ -2937,7 +2960,7 @@ debug@^4.1.0, debug@^4.1.1:
decache@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/decache/-/decache-4.6.0.tgz#87026bc6e696759e82d57a3841c4e251a30356e8"
resolved "https://registry.npmjs.org/decache/-/decache-4.6.0.tgz"
integrity sha512-PppOuLiz+DFeaUvFXEYZjLxAkKiMYH/do/b/MxpDe/8AgKBi5GhZxridoVIbBq72GDbL36e4p0Ce2jTGUwwU+w==
dependencies:
callsite "^1.0.0"
@ -3586,7 +3609,7 @@ file-loader@^4.2.0:
file-loader@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz"
integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
dependencies:
loader-utils "^2.0.0"
@ -3673,11 +3696,16 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@^1.0.0, follow-redirects@^1.14.4:
follow-redirects@^1.0.0:
version "1.14.5"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz"
integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
follow-redirects@^1.14.4:
version "1.14.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz"
@ -4244,6 +4272,11 @@ ignore@^4.0.3:
resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz"
@ -4331,7 +4364,7 @@ internal-slot@^1.0.3:
interpret@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
ip-regex@^2.1.0:
@ -5453,7 +5486,7 @@ nth-check@^2.0.0:
null-loader@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a"
resolved "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz"
integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==
dependencies:
loader-utils "^2.0.0"
@ -6344,6 +6377,11 @@ q@^1.1.2:
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qrious@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/qrious/-/qrious-4.0.2.tgz#09c4d4079d2b961617f62c69cff3b9bb66a39693"
integrity sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g==
qs@6.7.0:
version "6.7.0"
resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz"
@ -6457,7 +6495,7 @@ readdirp@~3.6.0:
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz"
integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=
dependencies:
resolve "^1.1.6"
@ -6696,9 +6734,9 @@ safe-regex@^1.1.0:
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass-loader@^10.2.0:
version "10.2.0"
resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-10.2.0.tgz"
integrity sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw==
version "10.2.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.1.tgz#17e51df313f1a7a203889ce8ff91be362651276e"
integrity sha512-RRvWl+3K2LSMezIsd008ErK4rk6CulIMSwrcc2aZvjymUgKo/vjXGp1rSWmfTUX7bblEOz8tst4wBwWtCGBqKA==
dependencies:
klona "^2.0.4"
loader-utils "^2.0.0"
@ -6707,11 +6745,13 @@ sass-loader@^10.2.0:
semver "^7.3.2"
sass@^1.43.4:
version "1.43.4"
resolved "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz"
integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==
version "1.49.8"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.8.tgz#9bbbc5d43d14862db07f1c04b786c9da9b641828"
integrity sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@~1.2.4:
version "1.2.4"
@ -6897,7 +6937,7 @@ shell-quote@^1.6.1:
shelljs@^0.8.3:
version "0.8.4"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz"
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
dependencies:
glob "^7.0.0"
@ -6998,6 +7038,11 @@ source-list-map@^2.0.0:
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-resolve@^0.5.0:
version "0.5.3"
resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz"
@ -7349,6 +7394,11 @@ terser@^4.1.2:
source-map "~0.6.1"
source-map-support "~0.5.12"
text-encoding@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz"
integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz"
@ -7462,6 +7512,11 @@ tryer@^1.0.1:
resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-custom-error@^3.0.0:
version "3.2.0"
resolved "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz"
integrity sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==
ts-pnp@^1.1.6:
version "1.2.0"
resolved "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz"
@ -7736,15 +7791,29 @@ vm-browserify@^1.0.1:
resolved "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-barcode-reader-plus@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/vue-barcode-reader-plus/-/vue-barcode-reader-plus-1.2.2.tgz#940586044cfcc2de61472e9f1e17ff99972055bc"
integrity sha512-ObiOHx30WrYpuD+Ns49uxSAwiv34GYqeWjOeVlbh8MZ4WxrZc4V+EvIQOC3s7bG/ynRFPZdSnMh3JWYsBHgd3Q==
dependencies:
"@zxing/library" "^0.19.1"
vue-barcode-reader@^0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/vue-barcode-reader/-/vue-barcode-reader-0.0.3.tgz"
integrity sha512-3hzsMQ6otBRJp55Nchva/Z+Tob81BrWJqNS50myoDmGAbO5ufiulT+ix6EKi5bl0iUN6cepMg0jWIn082WD1QA==
dependencies:
"@zxing/library" "^0.15.1"
vue-cli-plugin-axios@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vue-cli-plugin-axios/-/vue-cli-plugin-axios-0.0.4.tgz#29d4eb48275c7fe15b92e1fd5d95fbe2a966436f"
resolved "https://registry.npmjs.org/vue-cli-plugin-axios/-/vue-cli-plugin-axios-0.0.4.tgz"
integrity sha512-p2b/fvPJuPBnvU8027PAAuU5DiOzUn2lku8XLG/f6c8FU0N+/MXWZAlOuHhqd9e7+KIZitwe/c8qlmv7TglbTg==
vue-cli-plugin-vuetify@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.3.tgz#68a893642b354ed8d534e4a9bc63d0728a8987ac"
integrity sha512-dT9KpH1rXT6UWzBDFLMB69sgrNCoWFcxWiIyDDZ4vikv85JDweMHh2dT5n6QaAt7qsGlvL4IMOopjcyUTiPW9g==
version "2.4.4"
resolved "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.4.tgz"
integrity sha512-asc6G7+yN7NCk2ha0zaaeVZ+hSUP++Akao99SeW8rPO/nCxN2AIeCZg7PHPHDvcDYL9+KWhTsw5YbqYCk8x4LA==
dependencies:
null-loader "^4.0.1"
semver "^7.1.2"
@ -7760,6 +7829,11 @@ vue-hot-reload-api@^2.3.0:
resolved "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz"
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
vue-html-to-paper@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/vue-html-to-paper/-/vue-html-to-paper-1.4.4.tgz#98af39a81e5dc426fbf3e1575f220c76d233709d"
integrity sha512-5Stkm0jJDsC7A/WJWroxqxiASR1+9fcgVWy7AXv30uxdxTPOr7k1Z4KUklZJm7dkHR45tExVCMSOHuxrC22TEw==
"vue-loader-v16@npm:vue-loader@^16.1.0":
version "16.8.3"
resolved "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz"
@ -7813,7 +7887,7 @@ vue@^2.6.11:
vuetify-loader@^1.7.0:
version "1.7.3"
resolved "https://registry.yarnpkg.com/vuetify-loader/-/vuetify-loader-1.7.3.tgz#404657f4925c828f400fe3269003421d586835c6"
resolved "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.7.3.tgz"
integrity sha512-1Kt6Rfvuw3i9BBlxC9WTMnU3WEU7IBWQmDX+fYGAVGpzWCX7oHythUIwPCZGShHSYcPMKSDbXTPP8UvT5RNw8Q==
dependencies:
decache "^4.6.0"

@ -0,0 +1,105 @@
package utils
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/dustinpianalto/errors"
"github.com/dustinpianalto/quartermaster"
"github.com/dustinpianalto/quartermaster/internal/jwt"
"github.com/dustinpianalto/quartermaster/pkg/services"
"github.com/gorilla/mux"
)
func Mount(r *mux.Router, path string, handler http.Handler) {
r.PathPrefix(path).Handler(
http.StripPrefix(
strings.TrimSuffix(path, "/"),
handler,
),
)
}
type RootHandler func(*http.ResponseWriter, *http.Request) error
func (rh RootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := rh(&w, r)
if err == nil {
return
}
log.Println(err)
e, ok := err.(*errors.Error)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if errors.Is(errors.Permission, e) {
body := getErrorBody("Permission Denied")
w.WriteHeader(http.StatusForbidden)
w.Write(body)
}
if errors.Is(errors.Incorrect, e) && strings.Contains(string(e.Method), "login") {
body := getErrorBody("Invalid Login Credentials")
w.WriteHeader(http.StatusUnauthorized)
w.Write(body)
}
if errors.Is(errors.Incorrect, e) && strings.Contains(string(e.Method), "refresh") {
body := getErrorBody("Missing or Invalid Cookie")
w.WriteHeader(http.StatusUnauthorized)
w.Write(body)
}
if errors.Is(errors.Malformed, e) {
body := getErrorBody("Bad Request")
w.WriteHeader(http.StatusBadRequest)
w.Write(body)
}
if errors.Is(errors.Internal, e) {
body := getErrorBody("Internal Server Error")
w.WriteHeader(http.StatusInternalServerError)
w.Write(body)
}
if errors.Is(errors.Conflict, e) && strings.Contains(string(e.Method), "register") {
body := getErrorBody("User already exists")
w.WriteHeader(http.StatusConflict)
w.Write(body)
}
}
func getErrorBody(s string) []byte {
return []byte(fmt.Sprintf("{\"error\": \"%s\"}", s))
}
type authenticatedHandler func(*http.ResponseWriter, *http.Request, *quartermaster.User) error
func AuthenticationMiddleware(next authenticatedHandler) RootHandler {
const method = "utils/AuthenticationMiddleware"
return RootHandler(func(w *http.ResponseWriter, r *http.Request) error {
claims, err := jwt.AuthenticateJWTToken(r)
if err != nil {
return errors.E(method, errors.Permission, "invalid token", err)
}
u, err := services.UserService.User(claims.Username)
if err != nil {
return errors.E(method, errors.Permission, "user not found", err)
}
err = next(w, r, u)
if err != nil {
return errors.E(method, "error in handler", err)
}
return nil
})
}

@ -20,3 +20,7 @@ type Claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
type TokenResponse struct {
Token string `json:"token"`
}

Loading…
Cancel
Save