parent
35b2afba37
commit
5c8e734e81
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
@ -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,52 +1,101 @@
|
||||
<template>
|
||||
<div id="static-fab">
|
||||
<v-speed-dial
|
||||
v-model="fab"
|
||||
bottom
|
||||
right
|
||||
direction="top"
|
||||
fixed
|
||||
transition="scale-transition"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<v-btn v-model="fab" color="blue darken-2" dark fab>
|
||||
<v-icon v-if="fab"> mdi-close </v-icon>
|
||||
<v-icon v-else> mdi-plus </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-tooltip left>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn fab dark small color="blue" v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
<v-container>
|
||||
<div id="static-fab">
|
||||
<v-speed-dial
|
||||
v-model="fab"
|
||||
bottom
|
||||
right
|
||||
direction="top"
|
||||
fixed
|
||||
transition="scale-transition"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<v-btn v-model="fab" color="blue darken-2" dark fab>
|
||||
<v-icon v-if="fab"> mdi-close </v-icon>
|
||||
<v-icon v-else> mdi-plus </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Add Item</span>
|
||||
</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>
|
||||
</template>
|
||||
<span>Remove Item</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-icon>mdi-map-marker</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Add Location</span>
|
||||
</v-tooltip>
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
<v-tooltip left>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<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>
|
||||
<span>Add Item</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip left>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<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>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" @click="addLocationDialog = true">
|
||||
<v-icon>mdi-map-marker</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Add Location</span>
|
||||
</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/",
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const API = axios.create({
|
||||
baseURL: 'https://quartermaster.djpianalto.com'
|
||||
})
|
||||
@ -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-col>
|
||||
|
||||
<v-col
|
||||
v-for="j in 6"
|
||||
:key="`${n}${j}`"
|
||||
cols="6"
|
||||
md="2"
|
||||
>
|
||||
<v-sheet height="150"></v-sheet>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
<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="child in location.children" v-bind:key="`${child.id}`">
|
||||
<LocationCard :location="child"/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import LocationCard from "@/components/LocationCard.vue"
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
|
||||
components: {
|
||||
},
|
||||
}
|
||||
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>
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
Loading…
Reference in new issue