commit d25c30a7b2904c5dc26cf033a2e26b3a8340779c
Author: Akhil Gupta
+
+
+ Current Version - 2021.05.07
+ A self-hosted vehicle expense tracking system with support for multiple users.
+
+Hammond
+
+ Explore the docs »
+
+
+
+ Report Bug
+ ·
+ Request Feature
+ ·
+ Screenshots
+
+
+
+
+
+[contributors-shield]: https://img.shields.io/github/contributors/akhilrex/hammond.svg?style=flat-square
+[contributors-url]: https://github.com/akhilrex/hammond/graphs/contributors
+[forks-shield]: https://img.shields.io/github/forks/akhilrex/hammond.svg?style=flat-square
+[forks-url]: https://github.com/akhilrex/hammond/network/members
+[stars-shield]: https://img.shields.io/github/stars/akhilrex/hammond.svg?style=flat-square
+[stars-url]: https://github.com/akhilrex/hammond/stargazers
+[issues-shield]: https://img.shields.io/github/issues/akhilrex/hammond.svg?style=flat-square
+[issues-url]: https://github.com/akhilrex/hammond/issues
+[license-shield]: https://img.shields.io/github/license/akhilrex/hammond.svg?style=flat-square
+[license-url]: https://github.com/akhilrex/hammond/blob/master/LICENSE.txt
+[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
+[linkedin-url]: https://linkedin.com/in/akhilrex
+[product-screenshot]: images/screenshot.jpg
diff --git a/Shreenshots.md b/Shreenshots.md
new file mode 100644
index 0000000..0bdb21d
--- /dev/null
+++ b/Shreenshots.md
@@ -0,0 +1,26 @@
+## Home Page / Summary
+
+![Product Name Screen Shot][product-screenshot]
+
+## Add Podcast
+
+
+
+## All Episodes Chronologically
+
+
+
+## Podcast Episodes
+
+
+
+
+## Player
+
+
+
+## Settings
+
+
+
+[product-screenshot]: images/screenshot.jpg
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c029931
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,13 @@
+version: "2.1"
+services:
+ hammond:
+ image: akhilrex/hammond
+ container_name: hammond
+ environment:
+ - CHECK_FREQUENCY=240
+ volumes:
+ - /path/to/config:/config
+ - /path/to/data:/assets
+ ports:
+ - 8080:8080
+ restart: unless-stopped
diff --git a/images/create_expense.jpg b/images/create_expense.jpg
new file mode 100644
index 0000000..0320468
Binary files /dev/null and b/images/create_expense.jpg differ
diff --git a/images/create_fillup.jpg b/images/create_fillup.jpg
new file mode 100644
index 0000000..76d1a09
Binary files /dev/null and b/images/create_fillup.jpg differ
diff --git a/images/screenshot.jpg b/images/screenshot.jpg
new file mode 100644
index 0000000..2edb1ac
Binary files /dev/null and b/images/screenshot.jpg differ
diff --git a/images/settings.jpg b/images/settings.jpg
new file mode 100644
index 0000000..7ff59e6
Binary files /dev/null and b/images/settings.jpg differ
diff --git a/images/users.jpg b/images/users.jpg
new file mode 100644
index 0000000..5805dff
Binary files /dev/null and b/images/users.jpg differ
diff --git a/images/vehicle_detail.jpg b/images/vehicle_detail.jpg
new file mode 100644
index 0000000..45d79bd
Binary files /dev/null and b/images/vehicle_detail.jpg differ
diff --git a/images/vehicles_add.jpg b/images/vehicles_add.jpg
new file mode 100644
index 0000000..de37f9b
Binary files /dev/null and b/images/vehicles_add.jpg differ
diff --git a/server/.env b/server/.env
new file mode 100644
index 0000000..be09dc8
--- /dev/null
+++ b/server/.env
@@ -0,0 +1,3 @@
+CONFIG=.
+DATA=./assets
+JWT_SECRET="A super strong secret that needs to be changed"
diff --git a/server/.gitignore b/server/.gitignore
new file mode 100644
index 0000000..7f17b4d
--- /dev/null
+++ b/server/.gitignore
@@ -0,0 +1,21 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+*.db
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+assets/*
+keys/*
+backups/*
+nodemon.json
+dist/*
\ No newline at end of file
diff --git a/server/Dockerfile b/server/Dockerfile
new file mode 100644
index 0000000..f2fba9c
--- /dev/null
+++ b/server/Dockerfile
@@ -0,0 +1,41 @@
+ARG GO_VERSION=1.15.2
+
+FROM golang:${GO_VERSION}-alpine AS builder
+
+RUN apk update && apk add alpine-sdk git && rm -rf /var/cache/apk/*
+
+RUN mkdir -p /api
+WORKDIR /api
+
+COPY go.mod .
+COPY go.sum .
+RUN go mod download
+
+COPY . .
+RUN go build -o ./app ./main.go
+
+FROM alpine:latest
+
+LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
+
+ENV CONFIG=/config
+ENV DATA=/assets
+ENV UID=998
+ENV PID=100
+ENV GIN_MODE=release
+VOLUME ["/config", "/assets"]
+RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
+RUN mkdir -p /config; \
+ mkdir -p /assets; \
+ mkdir -p /api
+
+RUN chmod 777 /config; \
+ chmod 777 /assets
+
+WORKDIR /api
+COPY --from=builder /api/app .
+COPY dist ./dist
+
+EXPOSE 3000
+
+ENTRYPOINT ["./app"]
\ No newline at end of file
diff --git a/server/common/utils.go b/server/common/utils.go
new file mode 100644
index 0000000..2be238d
--- /dev/null
+++ b/server/common/utils.go
@@ -0,0 +1,89 @@
+// Common tools and helper functions
+package common
+
+import (
+ "fmt"
+ "math/rand"
+ "os"
+ "time"
+
+ "github.com/akhilrex/hammond/db"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+ "github.com/go-playground/validator/v10"
+)
+
+var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+// A helper function to generate random string
+func RandString(n int) string {
+ b := make([]rune, n)
+ for i := range b {
+ b[i] = letters[rand.Intn(len(letters))]
+ }
+ return string(b)
+}
+
+// A Util function to generate jwt_token which can be used in the request header
+func GenToken(id string, role db.Role) (string, string) {
+ jwt_token := jwt.New(jwt.GetSigningMethod("HS256"))
+ // Set some claims
+ jwt_token.Claims = jwt.MapClaims{
+ "id": id,
+ "role": role,
+ "exp": time.Now().Add(time.Hour * 24 * 3).Unix(),
+ }
+ // Sign and get the complete encoded token as a string
+ token, _ := jwt_token.SignedString([]byte(os.Getenv("JWT_SECRET")))
+
+ refreshToken := jwt.New(jwt.SigningMethodHS256)
+ rtClaims := refreshToken.Claims.(jwt.MapClaims)
+ rtClaims["id"] = id
+ rtClaims["exp"] = time.Now().Add(time.Hour * 24 * 20).Unix()
+
+ rt, _ := refreshToken.SignedString([]byte(os.Getenv("JWT_SECRET")))
+
+ return token, rt
+}
+
+// My own Error type that will help return my customized Error info
+// {"database": {"hello":"no such table", error: "not_exists"}}
+type CommonError struct {
+ Errors map[string]interface{} `json:"errors"`
+}
+
+// To handle the error returned by c.Bind in gin framework
+// https://github.com/go-playground/validator/blob/v9/_examples/translations/main.go
+func NewValidatorError(err error) CommonError {
+ res := CommonError{}
+ res.Errors = make(map[string]interface{})
+ errs := err.(validator.ValidationErrors)
+ for _, v := range errs {
+ // can translate each error one at a time.
+ //fmt.Println("gg",v.NameNamespace)
+ if v.Param() != "" {
+ res.Errors[v.Field()] = fmt.Sprintf("{%v: %v}", v.Tag(), v.Param())
+ } else {
+ res.Errors[v.Field()] = fmt.Sprintf("{key: %v}", v.Tag())
+ }
+
+ }
+ return res
+}
+
+// Warp the error info in a object
+func NewError(key string, err error) CommonError {
+ res := CommonError{}
+ res.Errors = make(map[string]interface{})
+ res.Errors[key] = err.Error()
+ return res
+}
+
+// Changed the c.MustBindWith() -> c.ShouldBindWith().
+// I don't want to auto return 400 when error happened.
+// origin function is here: https://github.com/gin-gonic/gin/blob/master/context.go
+func Bind(c *gin.Context, obj interface{}) error {
+ b := binding.Default(c.Request.Method, c.ContentType())
+ return c.ShouldBindWith(obj, b)
+}
diff --git a/server/controllers/auth.go b/server/controllers/auth.go
new file mode 100644
index 0000000..be7a01a
--- /dev/null
+++ b/server/controllers/auth.go
@@ -0,0 +1,179 @@
+package controllers
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/akhilrex/hammond/common"
+ "github.com/akhilrex/hammond/db"
+ "github.com/akhilrex/hammond/models"
+ "github.com/akhilrex/hammond/service"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/gin-gonic/gin"
+)
+
+func RegisterAnonController(router *gin.RouterGroup) {
+ router.POST("/login", userLogin)
+ router.POST("/auth/initialize", initializeSystem)
+
+}
+func RegisterAuthController(router *gin.RouterGroup) {
+
+ router.POST("/refresh", refresh)
+ router.GET("/me", me)
+ router.POST("/register", ShouldBeAdmin(), userRegister)
+ router.POST("/changePassword", changePassword)
+
+}
+
+func ShouldBeAdmin() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ model := c.MustGet("userModel").(db.User)
+ if model.Role != db.ADMIN {
+ c.JSON(http.StatusUnauthorized, gin.H{})
+ } else {
+ c.Next()
+ }
+ }
+}
+
+func me(c *gin.Context) {
+ user, err := service.GetUserById(c.MustGet("userId").(string))
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{})
+ }
+ c.JSON(http.StatusOK, user)
+}
+
+func userRegister(c *gin.Context) {
+ var registerRequest models.RegisterRequest
+ if err := c.ShouldBind(®isterRequest); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+
+ if err := service.CreateUser(®isterRequest, *registerRequest.Role); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
+ return
+ }
+
+ c.JSON(http.StatusCreated, gin.H{"success": true})
+}
+func initializeSystem(c *gin.Context) {
+
+ canInitialize, err := service.CanInitializeSystem()
+ if !canInitialize {
+ c.JSON(http.StatusUnprocessableEntity, err)
+ }
+
+ var registerRequest models.RegisterRequest
+ if err := c.ShouldBind(®isterRequest); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+
+ if err := service.CreateUser(®isterRequest, db.ADMIN); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("initializeSystem", err))
+ return
+ }
+
+ service.UpdateSettings(registerRequest.Currency, *registerRequest.DistanceUnit)
+
+ c.JSON(http.StatusCreated, gin.H{"success": true})
+}
+
+func userLogin(c *gin.Context) {
+ var loginRequest models.LoginRequest
+ if err := c.ShouldBind(&loginRequest); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+ user, err := db.FindOneUser(&db.User{Email: loginRequest.Email})
+
+ if err != nil {
+ c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
+ return
+ }
+
+ if user.CheckPassword(loginRequest.Password) != nil {
+ c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
+ return
+ }
+ UpdateContextUserModel(c, user.ID)
+ token, refreshToken := common.GenToken(user.ID, user.Role)
+ response := models.LoginResponse{
+ Name: user.Name,
+ Email: user.Email,
+ Token: token,
+ RefreshToken: refreshToken,
+ Role: user.RoleDetail().Long,
+ }
+ c.JSON(http.StatusOK, response)
+}
+
+func refresh(c *gin.Context) {
+ type tokenReqBody struct {
+ RefreshToken string `json:"refreshToken"`
+ }
+ tokenReq := tokenReqBody{}
+ c.Bind(&tokenReq)
+
+ token, _ := jwt.Parse(tokenReq.RefreshToken, func(token *jwt.Token) (interface{}, error) {
+ // Don't forget to validate the alg is what you expect:
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+ }
+
+ // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
+ return []byte(os.Getenv("JWT_SECRET")), nil
+ })
+ if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
+ // Get the user record from database or
+ // run through your business logic to verify if the user can log in
+ user, err := service.GetUserById(claims["id"].(string))
+ if err == nil {
+
+ token, refreshToken := common.GenToken(user.ID, user.Role)
+
+ response := models.LoginResponse{
+ Name: user.Name,
+ Email: user.Email,
+ Token: token,
+ RefreshToken: refreshToken,
+ Role: user.RoleDetail().Long,
+ }
+ c.JSON(http.StatusOK, response)
+ } else {
+
+ c.JSON(http.StatusUnauthorized, gin.H{})
+ }
+ } else {
+
+ c.JSON(http.StatusUnauthorized, gin.H{})
+ }
+}
+
+func changePassword(c *gin.Context) {
+ var request models.ChangePasswordRequest
+ if err := c.ShouldBind(&request); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+ user, err := service.GetUserById(c.GetString("userId"))
+
+ if err != nil {
+ c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Not Registered email or invalid password")))
+ return
+ }
+
+ if user.CheckPassword(request.OldPassword) != nil {
+ c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Not Registered email or invalid password")))
+ return
+ }
+
+ user.SetPassword(request.NewPassword)
+ success, err := service.UpdatePassword(user.ID, request.NewPassword)
+ c.JSON(http.StatusOK, success)
+}
diff --git a/server/controllers/files.go b/server/controllers/files.go
new file mode 100644
index 0000000..7d6ef3e
--- /dev/null
+++ b/server/controllers/files.go
@@ -0,0 +1,157 @@
+package controllers
+
+import (
+ "net/http"
+ "os"
+
+ "github.com/akhilrex/hammond/common"
+ "github.com/akhilrex/hammond/db"
+ "github.com/akhilrex/hammond/models"
+ "github.com/akhilrex/hammond/service"
+ "github.com/gin-gonic/gin"
+)
+
+func RegisterFilesController(router *gin.RouterGroup) {
+ router.POST("/upload", uploadFile)
+ router.POST("/quickEntries", createQuickEntry)
+ router.GET("/quickEntries", getAllQuickEntries)
+ router.GET("/me/quickEntries", getMyQuickEntries)
+ router.GET("/quickEntries/:id", getQuickEntryById)
+ router.POST("/quickEntries/:id/process", setQuickEntryAsProcessed)
+ router.GET("/attachments/:id/file", getAttachmentFile)
+}
+
+func createQuickEntry(c *gin.Context) {
+ var request models.CreateQuickEntryModel
+ if err := c.ShouldBind(&request); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+ attachment, err := saveUploadedFile(c, "file")
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, err)
+ return
+ }
+ if err := c.ShouldBind(&request); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, err)
+ return
+ }
+ quickEntry, err := service.CreateQuickEntry(request, attachment.ID, c.MustGet("userId").(string))
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createQuickEntry", err))
+ return
+ }
+ c.JSON(http.StatusCreated, quickEntry)
+}
+
+func getAllQuickEntries(c *gin.Context) {
+ quickEntries, err := service.GetAllQuickEntries("")
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getAllQuickEntries", err))
+ return
+ }
+ c.JSON(http.StatusOK, quickEntries)
+}
+func getMyQuickEntries(c *gin.Context) {
+ quickEntries, err := service.GetQuickEntriesForUser(c.MustGet("userId").(string), "")
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getMyQuickEntries", err))
+ return
+ }
+ c.JSON(http.StatusOK, quickEntries)
+}
+
+func getQuickEntryById(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+
+ if c.ShouldBindUri(&searchByIdQuery) == nil {
+ quickEntry, err := service.GetQuickEntryById(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleById", err))
+ return
+ }
+ c.JSON(http.StatusOK, quickEntry)
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
+ }
+}
+func setQuickEntryAsProcessed(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+
+ if c.ShouldBindUri(&searchByIdQuery) == nil {
+ err := service.SetQuickEntryAsProcessed(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleById", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
+ }
+}
+
+func uploadFile(c *gin.Context) {
+ attachment, err := saveMultipleUploadedFile(c, "file")
+ if err != nil {
+ c.JSON(http.StatusBadRequest, err)
+ } else {
+ c.JSON(http.StatusOK, attachment)
+ }
+}
+func getAttachmentFile(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+
+ if c.ShouldBindUri(&searchByIdQuery) == nil {
+
+ attachment, err := db.GetAttachmentById(searchByIdQuery.Id)
+ if err == nil {
+ if _, err = os.Stat(attachment.Path); os.IsNotExist(err) {
+ c.Status(404)
+ } else {
+ c.File(attachment.Path)
+ }
+ }
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
+ }
+}
+
+func saveUploadedFile(c *gin.Context, fileVariable string) (*db.Attachment, error) {
+ if fileVariable == "" {
+ fileVariable = "file"
+ }
+ file, err := c.FormFile(fileVariable)
+ if err != nil {
+ return nil, err
+ }
+ filePath := service.GetFilePath(file.Filename)
+ if err := c.SaveUploadedFile(file, filePath); err != nil {
+ return nil, err
+ }
+
+ return service.CreateAttachment(filePath, file.Filename, file.Size, file.Header.Get("Content-Type"), c.MustGet("userId").(string))
+}
+func saveMultipleUploadedFile(c *gin.Context, fileVariable string) ([]*db.Attachment, error) {
+ if fileVariable == "" {
+ fileVariable = "files"
+ }
+ form, err := c.MultipartForm()
+ if err != nil {
+ return nil, err
+ }
+ files := form.File[fileVariable]
+ var toReturn []*db.Attachment
+ for _, file := range files {
+ filePath := service.GetFilePath(file.Filename)
+ if err := c.SaveUploadedFile(file, filePath); err != nil {
+ return nil, err
+ }
+ attachment, err := service.CreateAttachment(filePath, file.Filename, file.Size, file.Header.Get("Content-Type"), c.MustGet("userId").(string))
+ if err != nil {
+ return nil, err
+ }
+
+ toReturn = append(toReturn, attachment)
+ }
+ return toReturn, nil
+}
diff --git a/server/controllers/masters.go b/server/controllers/masters.go
new file mode 100644
index 0000000..e1785c6
--- /dev/null
+++ b/server/controllers/masters.go
@@ -0,0 +1,64 @@
+package controllers
+
+import (
+ "net/http"
+
+ "github.com/akhilrex/hammond/common"
+ "github.com/akhilrex/hammond/db"
+ "github.com/akhilrex/hammond/models"
+ "github.com/akhilrex/hammond/service"
+ "github.com/gin-gonic/gin"
+)
+
+func RegisterAnonMasterConroller(router *gin.RouterGroup) {
+ router.GET("/masters", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "fuelUnits": db.FuelUnitDetails,
+ "fuelTypes": db.FuelTypeDetails,
+ "distanceUnits": db.DistanceUnitDetails,
+ "roles": db.RoleDetails,
+ "currencies": models.GetCurrencyMasterList(),
+ })
+ })
+}
+func RegisterMastersController(router *gin.RouterGroup) {
+
+ router.GET("/settings", getSettings)
+ router.POST("/settings", udpateSettings)
+ router.POST("/me/settings", udpateMySettings)
+
+}
+
+func getSettings(c *gin.Context) {
+
+ c.JSON(http.StatusOK, service.GetSettings())
+}
+func udpateSettings(c *gin.Context) {
+ var model models.UpdateSettingModel
+ if err := c.ShouldBind(&model); err == nil {
+ err := service.UpdateSettings(model.Currency, *model.DistanceUnit)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("udpateSettings", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+
+}
+
+func udpateMySettings(c *gin.Context) {
+ var model models.UpdateSettingModel
+ if err := c.ShouldBind(&model); err == nil {
+ err := service.UpdateUserSettings(c.MustGet("userId").(string), model.Currency, *model.DistanceUnit)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("udpateMySettings", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+
+}
diff --git a/server/controllers/middlewares.go b/server/controllers/middlewares.go
new file mode 100644
index 0000000..3c12c54
--- /dev/null
+++ b/server/controllers/middlewares.go
@@ -0,0 +1,71 @@
+package controllers
+
+import (
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/akhilrex/hammond/db"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/dgrijalva/jwt-go/request"
+ "github.com/gin-gonic/gin"
+)
+
+// Strips 'BEARER ' prefix from token string
+func stripBearerPrefixFromTokenString(tok string) (string, error) {
+ // Should be a bearer token
+ if len(tok) > 6 && strings.ToUpper(tok[0:6]) == "BEARER " {
+ return tok[7:], nil
+ }
+ return tok, nil
+}
+
+// Extract token from Authorization header
+// Uses PostExtractionFilter to strip "TOKEN " prefix from header
+var AuthorizationHeaderExtractor = &request.PostExtractionFilter{
+ request.HeaderExtractor{"Authorization"},
+ stripBearerPrefixFromTokenString,
+}
+
+// Extractor for OAuth2 access tokens. Looks in 'Authorization'
+// header then 'access_token' argument for a token.
+var MyAuth2Extractor = &request.MultiExtractor{
+ AuthorizationHeaderExtractor,
+ request.ArgumentExtractor{"access_token"},
+}
+
+// A helper to write user_id and user_model to the context
+func UpdateContextUserModel(c *gin.Context, my_user_id string) {
+ var myUserModel db.User
+ if my_user_id != "" {
+
+ db.DB.First(&myUserModel, map[string]string{
+ "ID": my_user_id,
+ })
+ }
+ c.Set("userId", my_user_id)
+ c.Set("userModel", myUserModel)
+}
+
+// You can custom middlewares yourself as the doc: https://github.com/gin-gonic/gin#custom-middleware
+// r.Use(AuthMiddleware(true))
+func AuthMiddleware(auto401 bool) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ UpdateContextUserModel(c, "")
+ token, err := request.ParseFromRequest(c.Request, MyAuth2Extractor, func(token *jwt.Token) (interface{}, error) {
+ b := ([]byte(os.Getenv("JWT_SECRET")))
+ return b, nil
+ })
+ if err != nil {
+ if auto401 {
+ c.AbortWithError(http.StatusUnauthorized, err)
+ }
+ return
+ }
+ if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
+ my_user_id := claims["id"].(string)
+ //fmt.Println(my_user_id,claims["id"])
+ UpdateContextUserModel(c, my_user_id)
+ }
+ }
+}
diff --git a/server/controllers/setup.go b/server/controllers/setup.go
new file mode 100644
index 0000000..b8378f3
--- /dev/null
+++ b/server/controllers/setup.go
@@ -0,0 +1,70 @@
+package controllers
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/akhilrex/hammond/common"
+ "github.com/akhilrex/hammond/db"
+ "github.com/akhilrex/hammond/models"
+ "github.com/akhilrex/hammond/service"
+ "github.com/gin-gonic/gin"
+)
+
+func RegisterSetupController(router *gin.RouterGroup) {
+ router.POST("/clarkson/check", canMigrate)
+ router.POST("/clarkson/migrate", migrate)
+ router.GET("/system/status", appInitialized)
+}
+
+func appInitialized(c *gin.Context) {
+ canInitialize, err := service.CanInitializeSystem()
+ message := ""
+ if err != nil {
+ message = err.Error()
+ }
+ c.JSON(http.StatusOK, gin.H{"initialized": !canInitialize, "message": message})
+}
+
+func canMigrate(c *gin.Context) {
+ var request models.ClarksonMigrationModel
+ if err := c.ShouldBind(&request); err == nil {
+ canMigrate, data, errr := db.CanMigrate(request.Url)
+ errorMessage := ""
+ if errr != nil {
+ errorMessage = errr.Error()
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "canMigrate": canMigrate,
+ "data": data,
+ "message": errorMessage,
+ })
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func migrate(c *gin.Context) {
+ var request models.ClarksonMigrationModel
+ if err := c.ShouldBind(&request); err == nil {
+ canMigrate, _, _ := db.CanMigrate(request.Url)
+
+ if !canMigrate {
+ c.JSON(http.StatusBadRequest, fmt.Errorf("cannot migrate database. please check connection string."))
+ return
+ }
+
+ success, err := db.MigrateClarkson(request.Url)
+ if !success {
+ c.JSON(http.StatusBadRequest, err)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "success": success,
+ })
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
diff --git a/server/controllers/users.go b/server/controllers/users.go
new file mode 100644
index 0000000..1ea48c4
--- /dev/null
+++ b/server/controllers/users.go
@@ -0,0 +1,22 @@
+package controllers
+
+import (
+ "net/http"
+
+ "github.com/akhilrex/hammond/db"
+ "github.com/gin-gonic/gin"
+)
+
+func RegisterUserController(router *gin.RouterGroup) {
+ router.GET("/users", allUsers)
+}
+
+func allUsers(c *gin.Context) {
+ users, err := db.GetAllUsers()
+ if err != nil {
+ c.JSON(http.StatusBadRequest, err)
+ return
+ }
+ c.JSON(http.StatusOK, users)
+
+}
diff --git a/server/controllers/vehicle.go b/server/controllers/vehicle.go
new file mode 100644
index 0000000..94f78f6
--- /dev/null
+++ b/server/controllers/vehicle.go
@@ -0,0 +1,436 @@
+package controllers
+
+import (
+ "net/http"
+
+ "github.com/akhilrex/hammond/common"
+ "github.com/akhilrex/hammond/models"
+ "github.com/akhilrex/hammond/service"
+ "github.com/gin-gonic/gin"
+)
+
+func RegisterVehicleController(router *gin.RouterGroup) {
+ router.POST("/vehicles", createVehicle)
+ router.GET("/vehicles", getAllVehicles)
+ router.GET("/vehicles/:id", getVehicleById)
+ router.PUT("/vehicles/:id", updateVehicle)
+ router.GET("/vehicles/:id/stats", getVehicleStats)
+ router.GET("/vehicles/:id/users", getVehicleUsers)
+ router.POST("/vehicles/:id/users/:subId", shareVehicle)
+ router.DELETE("/vehicles/:id/users/:subId", unshareVehicle)
+
+ router.GET("/me/vehicles", getMyVehicles)
+ router.GET("/me/stats", getMystats)
+
+ router.GET("/vehicles/:id/fillups", getFillupsByVehicleId)
+ router.POST("/vehicles/:id/fillups", createFillup)
+ router.GET("/vehicles/:id/fillups/:subId", getFillupById)
+ router.PUT("/vehicles/:id/fillups/:subId", updateFillup)
+ router.DELETE("/vehicles/:id/fillups/:subId", deleteFillup)
+
+ router.GET("/vehicles/:id/expenses", getExpensesByVehicleId)
+ router.POST("/vehicles/:id/expenses", createExpense)
+ router.GET("/vehicles/:id/expenses/:subId", getExpenseById)
+ router.PUT("/vehicles/:id/expenses/:subId", updateExpense)
+ router.DELETE("/vehicles/:id/expenses/:subId", deleteExpense)
+
+ router.POST("/vehicles/:id/attachments", createVehicleAttachment)
+ router.GET("/vehicles/:id/attachments", getVehicleAttachments)
+}
+
+func createVehicle(c *gin.Context) {
+ var request models.CreateVehicleRequest
+ if err := c.ShouldBind(&request); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+ vehicle, err := service.CreateVehicle(request, c.MustGet("userId").(string))
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createVehicle", err))
+ return
+ }
+ c.JSON(http.StatusCreated, vehicle)
+}
+func getVehicleById(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+
+ if c.ShouldBindUri(&searchByIdQuery) == nil {
+ vehicle, err := service.GetVehicleById(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleById", err))
+ return
+ }
+ c.JSON(http.StatusOK, vehicle)
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
+ }
+}
+func updateVehicle(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+ var updateVehicleModel models.UpdateVehicleRequest
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+ if err := c.ShouldBind(&updateVehicleModel); err == nil {
+ err := service.UpdateVehicle(searchByIdQuery.Id, updateVehicleModel)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleById", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+func getAllVehicles(c *gin.Context) {
+ vehicles, err := service.GetAllVehicles()
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleById", err))
+ return
+ }
+ c.JSON(200, vehicles)
+
+}
+func getMyVehicles(c *gin.Context) {
+ vehicles, err := service.GetUserVehicles(c.MustGet("userId").(string))
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getMyVehicles", err))
+ return
+ }
+ c.JSON(200, vehicles)
+
+}
+
+func getFillupsByVehicleId(c *gin.Context) {
+
+ var searchByIdQuery models.SearchByIdQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ fillups, err := service.GetFillupsByVehicleId(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getFillupsByVehicleId", err))
+ return
+ }
+ c.JSON(http.StatusOK, fillups)
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func getExpensesByVehicleId(c *gin.Context) {
+
+ var searchByIdQuery models.SearchByIdQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ data, err := service.GetExpensesByVehicleId(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getExpensesByVehicleId", err))
+ return
+ }
+ c.JSON(http.StatusOK, data)
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func createFillup(c *gin.Context) {
+ var request models.CreateFillupRequest
+ var searchByIdQuery models.SearchByIdQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+ if err := c.ShouldBind(&request); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+ fillup, err := service.CreateFillup(request)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createFillup", err))
+ return
+ }
+ c.JSON(http.StatusCreated, fillup)
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func createExpense(c *gin.Context) {
+ var request models.CreateExpenseRequest
+ var searchByIdQuery models.SearchByIdQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+ if err := c.ShouldBind(&request); err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ return
+ }
+ expense, err := service.CreateExpense(request)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createExpense", err))
+ return
+ }
+ c.JSON(http.StatusCreated, expense)
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func updateExpense(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+ var updateExpenseModel models.UpdateExpenseRequest
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+ if err := c.ShouldBind(&updateExpenseModel); err == nil {
+ err := service.UpdateExpense(searchByIdQuery.SubId, updateExpenseModel)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getExpenseById", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+func updateFillup(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+ var updateFillupModel models.UpdateFillupRequest
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+ if err := c.ShouldBind(&updateFillupModel); err == nil {
+ err := service.UpdateFillup(searchByIdQuery.SubId, updateFillupModel)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getFillupById", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func deleteExpense(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ err := service.DeleteExpenseById(searchByIdQuery.SubId)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getExpenseById", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+func deleteFillup(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ err := service.DeleteFillupById(searchByIdQuery.SubId)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getFillupById", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func getExpenseById(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ obj, err := service.GetExpenseById(searchByIdQuery.SubId)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getExpenseById", err))
+ return
+ }
+ c.JSON(http.StatusOK, obj)
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+func getFillupById(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ obj, err := service.GetFillupById(searchByIdQuery.SubId)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getFillupById", err))
+ return
+ }
+ c.JSON(http.StatusOK, obj)
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func createVehicleAttachment(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+ var dataModel models.CreateVehicleAttachmentModel
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+ if err := c.ShouldBind(&dataModel); err == nil {
+ vehicle, err := service.GetVehicleById(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createVehicleAttachment", err))
+ return
+ }
+ attachment, err := saveUploadedFile(c, "file")
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createVehicleAttachment", err))
+ return
+ }
+ err = service.CreateVehicleAttachment(vehicle.ID, attachment.ID, dataModel.Title)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createVehicleAttachment", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func getVehicleAttachments(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ vehicle, err := service.GetVehicleById(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createVehicleAttachment", err))
+ return
+ }
+
+ attachments, err := service.GetVehicleAttachments(vehicle.ID)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("createVehicleAttachment", err))
+ return
+ }
+ c.JSON(http.StatusOK, attachments)
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func getVehicleUsers(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ vehicle, err := service.GetVehicleById(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleUsers", err))
+ return
+ }
+ data, err := service.GetVehicleUsers(vehicle.ID)
+
+ var model []models.UserVehicleSimpleModel
+
+ for _, item := range *data {
+ model = append(model, models.UserVehicleSimpleModel{
+ ID: item.ID,
+ UserID: item.UserID,
+ VehicleID: item.VehicleID,
+ IsOwner: item.IsOwner,
+ Name: item.User.Name,
+ })
+ }
+
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleUsers", err))
+ return
+ }
+
+ c.JSON(http.StatusOK, model)
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+func shareVehicle(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ err := service.ShareVehicle(searchByIdQuery.Id, searchByIdQuery.SubId)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("shareVehicle", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+func unshareVehicle(c *gin.Context) {
+ var searchByIdQuery models.SubItemQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ err := service.UnshareVehicle(searchByIdQuery.Id, searchByIdQuery.SubId)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("shareVehicle", err))
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func getVehicleStats(c *gin.Context) {
+ var searchByIdQuery models.SearchByIdQuery
+
+ if err := c.ShouldBindUri(&searchByIdQuery); err == nil {
+
+ vehicle, err := service.GetVehicleById(searchByIdQuery.Id)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getVehicleState", err))
+ return
+ }
+
+ model := models.VehicleStatsModel{}
+
+ c.JSON(http.StatusOK, model.SetStats(&vehicle.Fillups, &vehicle.Expenses))
+
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+}
+
+func getMystats(c *gin.Context) {
+ var model models.UserStatsQueryModel
+ if err := c.ShouldBind(&model); err == nil {
+
+ stats, err := service.GetUserStats(c.MustGet("userId").(string), model)
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, common.NewError("getMyVehicles", err))
+ return
+ }
+ c.JSON(200, stats)
+ } else {
+ c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
+ }
+
+}
diff --git a/server/db/base.go b/server/db/base.go
new file mode 100644
index 0000000..8e4af26
--- /dev/null
+++ b/server/db/base.go
@@ -0,0 +1,22 @@
+package db
+
+import (
+ "time"
+
+ uuid "github.com/satori/go.uuid"
+ "gorm.io/gorm"
+)
+
+//Base is
+type Base struct {
+ ID string `sql:"type:uuid;primary_key" json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ DeletedAt *time.Time `gorm:"index" json:"deletedAt"`
+}
+
+//BeforeCreate
+func (base *Base) BeforeCreate(tx *gorm.DB) error {
+ tx.Statement.SetColumn("ID", uuid.NewV4().String())
+ return nil
+}
diff --git a/server/db/clarkson.go b/server/db/clarkson.go
new file mode 100644
index 0000000..a4a25c7
--- /dev/null
+++ b/server/db/clarkson.go
@@ -0,0 +1,223 @@
+package db
+
+import (
+ "time"
+
+ "gorm.io/driver/mysql"
+
+ "gorm.io/gorm"
+)
+
+func CanMigrate(connectionString string) (bool, interface{}, error) {
+
+ canInitialize, err := CanInitializeSystem()
+ if !canInitialize {
+ return canInitialize, nil, err
+ }
+
+ cdb, err := gorm.Open(mysql.Open(connectionString), &gorm.Config{})
+ if err != nil {
+ return false, nil, err
+ }
+
+ var usersCount, vehiclesCount, fuelCount int64
+ tx := cdb.Table("Users").Count(&usersCount)
+ if tx.Error != nil {
+ return false, nil, tx.Error
+ }
+ tx = cdb.Table("Vehicles").Count(&vehiclesCount)
+ if tx.Error != nil {
+ return false, nil, tx.Error
+ }
+ tx = cdb.Table("Fuel").Count(&fuelCount)
+ if tx.Error != nil {
+ return false, nil, tx.Error
+ }
+ data := struct {
+ Users int64 `json:"users"`
+ Vehicles int64 `json:"vehicles"`
+ Fillups int64 `json:"fillups"`
+ }{
+ Vehicles: vehiclesCount,
+ Users: usersCount,
+ Fillups: fuelCount,
+ }
+
+ return true, data, nil
+}
+
+func MigrateClarkson(connectionString string) (bool, error) {
+ canInitialize, err := CanInitializeSystem()
+ if !canInitialize {
+ return canInitialize, err
+ }
+
+ //dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
+ cdb, err := gorm.Open(mysql.Open(connectionString), &gorm.Config{})
+ if err != nil {
+ return false, nil
+ }
+
+ /////Models
+ type CUser struct {
+ ID string `gorm:"column:id"`
+ Email string `gorm:"column:email"`
+ Username string `gorm:"column:username"`
+ Password string `gorm:"column:password"`
+ Admin bool `gorm:"column:admin"`
+ FuelUnit int `gorm:"column:fuelUnit"`
+ DistanceUnit int `gorm:"column:distanceUnit"`
+ FuelConsumptionUnit int `gorm:"column:fuelConsumptionUnit"`
+ CurrencyUnit int `gorm:"column:currencyUnit"`
+ }
+
+ type CVehicle struct {
+ ID string `gorm:"column:id"`
+ User string
+ Name string
+ Registration string
+ Make string
+ Model string
+ YearOfManufacture int `gorm:"column:yearOfManufacture"`
+ Vin string
+ EngineSizeCC int `gorm:"column:engineSizeCC"`
+ FuelType int `gorm:"column:fuelType"`
+ }
+
+ type CFuel struct {
+ ID string `gorm:"column:id"`
+ Vehicle string `gorm:"column:vehicle"`
+ Date time.Time `gorm:"column:date"`
+ FuelAmount float32 `gorm:"column:fuelAmount"`
+ TotalCost float32 `gorm:"column:totalCost"`
+ FuelUnitCost float32 `gorm:"column:fuelUnitCost"`
+ OdometerReading int `gorm:"column:odometerReading"`
+ Notes string `gorm:"column:notes"`
+ FullTank bool `gorm:"column:fullTank"`
+ MissedFillup bool `gorm:"column:missedFillUp"`
+ }
+
+ distanceUnitMap := map[int]DistanceUnit{
+ 1: MILES,
+ 2: KILOMETERS,
+ }
+
+ fuelTypeMap := map[int]FuelType{
+ 1: PETROL,
+ 2: DIESEL,
+ 3: ETHANOL,
+ 4: LPG,
+ }
+
+ fuelUnitsMap := map[int]FuelUnit{
+ 1: LITRE,
+ 2: GALLON,
+ 3: US_GALLON,
+ }
+ currencyMap := map[int]string{
+ 1: "GBP",
+ 2: "USD",
+ 3: "EUR",
+ 4: "AUD",
+ 5: "CAD",
+ }
+
+ newUserIdsMap := make(map[string]User)
+ oldUserIdsMap := make(map[string]CUser)
+
+ var allUsers []CUser
+ cdb.Table("Users").Find(&allUsers)
+ for _, v := range allUsers {
+ role := USER
+ if v.Admin {
+ role = ADMIN
+ }
+ user := User{
+ Email: v.Email,
+ Currency: currencyMap[v.CurrencyUnit],
+ DistanceUnit: distanceUnitMap[v.DistanceUnit],
+ Role: role,
+ Name: v.Username,
+ }
+ user.SetPassword("hammond")
+ err = CreateUser(&user)
+ if err != nil {
+ return false, err
+ }
+
+ newUserIdsMap[v.ID] = user
+ oldUserIdsMap[v.ID] = v
+
+ if v.Admin {
+ setting := GetOrCreateSetting()
+ setting.Currency = user.Currency
+ setting.DistanceUnit = user.DistanceUnit
+ UpdateSettings(setting)
+ }
+ }
+
+ newVehicleIdsMap := make(map[string]Vehicle)
+ oldVehicleIdsMap := make(map[string]CVehicle)
+ vehicleUserMap := make(map[string]User)
+ var allVehicles []CVehicle
+ cdb.Table("Vehicles").Find(&allVehicles)
+ for _, model := range allVehicles {
+ vehicle := Vehicle{
+ Nickname: model.Name,
+ Registration: model.Registration,
+ Model: model.Model,
+ Make: model.Make,
+ YearOfManufacture: model.YearOfManufacture,
+ EngineSize: float32(model.EngineSizeCC),
+ FuelUnit: fuelUnitsMap[oldUserIdsMap[model.User].FuelUnit],
+ FuelType: fuelTypeMap[model.FuelType],
+ }
+
+ tx := DB.Create(&vehicle)
+ if tx.Error != nil {
+ return false, tx.Error
+ }
+ association := UserVehicle{
+ UserID: newUserIdsMap[model.User].ID,
+ VehicleID: vehicle.ID,
+ IsOwner: true,
+ }
+ vehicleUserMap[vehicle.ID] = newUserIdsMap[model.User]
+ tx = DB.Create(&association)
+
+ if tx.Error != nil {
+ return false, err
+ }
+
+ newVehicleIdsMap[model.ID] = vehicle
+ oldVehicleIdsMap[model.ID] = model
+ }
+
+ var allFillups []CFuel
+ cdb.Table("Fuel").Find(&allFillups)
+ for _, model := range allFillups {
+ fillup := Fillup{
+ VehicleID: newVehicleIdsMap[model.Vehicle].ID,
+ FuelUnit: newVehicleIdsMap[model.Vehicle].FuelUnit,
+ FuelQuantity: model.FuelAmount,
+ PerUnitPrice: model.FuelUnitCost,
+ TotalAmount: model.TotalCost,
+ OdoReading: model.OdometerReading,
+ IsTankFull: &model.FullTank,
+ HasMissedFillup: &model.MissedFillup,
+ Comments: model.Notes,
+ UserID: vehicleUserMap[newVehicleIdsMap[model.Vehicle].ID].ID,
+ Date: model.Date,
+ Currency: vehicleUserMap[newVehicleIdsMap[model.Vehicle].ID].Currency,
+ DistanceUnit: vehicleUserMap[newVehicleIdsMap[model.Vehicle].ID].DistanceUnit,
+ }
+
+ tx := DB.Create(&fillup)
+ if tx.Error != nil {
+ return false, tx.Error
+ }
+
+ }
+
+ return true, nil
+}
diff --git a/server/db/db.go b/server/db/db.go
new file mode 100644
index 0000000..51dd7e1
--- /dev/null
+++ b/server/db/db.go
@@ -0,0 +1,58 @@
+package db
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path"
+
+ "gorm.io/driver/sqlite"
+
+ "gorm.io/gorm"
+)
+
+//DB is
+var DB *gorm.DB
+
+//Init is used to Initialize Database
+func Init() (*gorm.DB, error) {
+ // github.com/mattn/go-sqlite3
+ configPath := os.Getenv("CONFIG")
+ dbPath := path.Join(configPath, "hammond.db")
+ log.Println(dbPath)
+ db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
+ DisableForeignKeyConstraintWhenMigrating: true,
+ })
+ if err != nil {
+ fmt.Println("db err: ", err)
+ return nil, err
+ }
+
+ localDB, _ := db.DB()
+ localDB.SetMaxIdleConns(10)
+ //db.LogMode(true)
+ DB = db
+ return DB, nil
+}
+
+//Migrate Database
+func Migrate() {
+ err := DB.AutoMigrate(&Attachment{}, &QuickEntry{}, &User{}, &Vehicle{}, &UserVehicle{}, &VehicleAttachment{}, &Fillup{}, &Expense{}, &Setting{}, &JobLock{}, &Migration{})
+ if err != nil {
+ fmt.Println("1 " + err.Error())
+ }
+ err = DB.SetupJoinTable(&User{}, "Vehicles", &UserVehicle{})
+ if err != nil {
+ fmt.Println(err.Error())
+ }
+ err = DB.SetupJoinTable(&Vehicle{}, "Attachments", &VehicleAttachment{})
+ if err != nil {
+ fmt.Println(err.Error())
+ }
+ RunMigrations()
+}
+
+// Using this function to get a connection, you can create your connection pool here.
+func GetDB() *gorm.DB {
+ return DB
+}
diff --git a/server/db/dbModels.go b/server/db/dbModels.go
new file mode 100644
index 0000000..72188d0
--- /dev/null
+++ b/server/db/dbModels.go
@@ -0,0 +1,192 @@
+package db
+
+import (
+ "encoding/json"
+ "errors"
+ "time"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+type User struct {
+ Base
+ Email string `gorm:"unique" json:"email"`
+ Password string `json:"-"`
+ Currency string `json:"currency"`
+ DistanceUnit DistanceUnit `json:"distanceUnit"`
+ Role Role `json:"role"`
+ Name string `json:"name"`
+ Vehicles []Vehicle `gorm:"many2many:user_vehicles;" json:"vehicles"`
+}
+
+func (b *User) MarshalJSON() ([]byte, error) {
+ return json.Marshal(struct {
+ User
+ RoleDetail EnumDetail `json:"roleDetail"`
+ DistanceUnitDetail EnumDetail `json:"distanceUnitDetail"`
+ }{
+ User: *b,
+ RoleDetail: b.RoleDetail(),
+ DistanceUnitDetail: b.DistanceUnitDetail(),
+ })
+}
+func (v *User) RoleDetail() EnumDetail {
+ return RoleDetails[v.Role]
+}
+func (v *User) DistanceUnitDetail() EnumDetail {
+ return DistanceUnitDetails[v.DistanceUnit]
+}
+
+func (u *User) SetPassword(password string) error {
+ if len(password) == 0 {
+ return errors.New("password should not be empty")
+ }
+ bytePassword := []byte(password)
+ // Make sure the second param `bcrypt generator cost` between [4, 32)
+ passwordHash, _ := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost)
+ u.Password = string(passwordHash)
+ return nil
+}
+
+func (u *User) CheckPassword(password string) error {
+ bytePassword := []byte(password)
+ byteHashedPassword := []byte(u.Password)
+ return bcrypt.CompareHashAndPassword(byteHashedPassword, bytePassword)
+}
+
+type Vehicle struct {
+ Base
+ Nickname string `json:"nickname"`
+ Registration string `json:"registration"`
+ Make string `json:"make"`
+ Model string `json:"model"`
+ YearOfManufacture int `json:"yearOfManufacture"`
+ EngineSize float32 `json:"engineSize"`
+ FuelUnit FuelUnit `json:"fuelUnit"`
+ FuelType FuelType `json:"fuelType"`
+ Users []User `gorm:"many2many:user_vehicles;" json:"users"`
+ Fillups []Fillup `json:"fillups"`
+ Expenses []Expense `json:"expenses"`
+ Attachments []Attachment `gorm:"many2many:vehicle_attachments;" json:"attachments"`
+ IsOwner bool `gorm:"->" json:"isOwner"`
+}
+
+func (b *Vehicle) MarshalJSON() ([]byte, error) {
+ return json.Marshal(struct {
+ Vehicle
+ FuelTypeDetail EnumDetail `json:"fuelTypeDetail"`
+ FuelUnitDetail EnumDetail `json:"fuelUnitDetail"`
+ }{
+ Vehicle: *b,
+ FuelTypeDetail: b.FuelTypeDetail(),
+ FuelUnitDetail: b.FuelUnitDetail(),
+ })
+}
+func (v *Vehicle) FuelTypeDetail() EnumDetail {
+ return FuelTypeDetails[v.FuelType]
+}
+
+func (v *Vehicle) FuelUnitDetail() EnumDetail {
+ return FuelUnitDetails[v.FuelUnit]
+}
+
+type UserVehicle struct {
+ Base
+ UserID string `gorm:"primaryKey"`
+ User User `json:"user"`
+ VehicleID string `gorm:"primaryKey"`
+ IsOwner bool `json:"isOwner"`
+}
+
+type Fillup struct {
+ Base
+ VehicleID string `json:"vehicleId"`
+ Vehicle Vehicle `json:"-"`
+ FuelUnit FuelUnit `json:"fuelUnit"`
+ FuelQuantity float32 `json:"fuelQuantity"`
+ PerUnitPrice float32 `json:"perUnitPrice"`
+ TotalAmount float32 `json:"totalAmount"`
+ OdoReading int `json:"odoReading"`
+ IsTankFull *bool `json:"isTankFull"`
+ HasMissedFillup *bool `json:"hasMissedFillup"`
+ Comments string `json:"comments"`
+ FillingStation string `json:"fillingStation"`
+ UserID string `json:"userId"`
+ User User `json:"user"`
+ Date time.Time `json:"date"`
+ Currency string `json:"currency"`
+ DistanceUnit DistanceUnit `json:"distanceUnit"`
+}
+
+func (v *Fillup) FuelUnitDetail() EnumDetail {
+ return FuelUnitDetails[v.FuelUnit]
+}
+func (b *Fillup) MarshalJSON() ([]byte, error) {
+ return json.Marshal(struct {
+ Fillup
+ FuelUnitDetail EnumDetail `json:"fuelUnitDetail"`
+ }{
+ Fillup: *b,
+ FuelUnitDetail: b.FuelUnitDetail(),
+ })
+}
+
+type Expense struct {
+ Base
+ VehicleID string `json:"vehicleId"`
+ Vehicle Vehicle `json:"-"`
+ Amount float32 `json:"amount"`
+ OdoReading int `json:"odoReading"`
+ Comments string `json:"comments"`
+ ExpenseType string `json:"expenseType"`
+ UserID string `json:"userId"`
+ User User `json:"user"`
+ Date time.Time `json:"date"`
+ Currency string `json:"currency"`
+ DistanceUnit DistanceUnit `json:"distanceUnit"`
+}
+
+type Setting struct {
+ Base
+ Currency string `json:"currency" gorm:"default:INR"`
+ DistanceUnit DistanceUnit `json:"distanceUnit" gorm:"default:1"`
+}
+type Migration struct {
+ Base
+ Date time.Time
+ Name string
+}
+type JobLock struct {
+ Base
+ Date time.Time
+ Name string
+ Duration int
+}
+
+type Attachment struct {
+ Base
+ Path string `json:"path"`
+ OriginalName string `json:"originalName"`
+ Size int64 `json:"size"`
+ ContentType string `json:"contentType"`
+ Title string `gorm:"->" json:"title"`
+ UserID string `json:"userId"`
+ User User `json:"user"`
+}
+
+type QuickEntry struct {
+ Base
+ AttachmentID string `json:"attachmentId"`
+ Attachment Attachment `json:"attachment"`
+ ProcessDate *time.Time `json:"processDate"`
+ UserID string `json:"userId"`
+ User User `json:"user"`
+ Comments string `json:"comments"`
+}
+
+type VehicleAttachment struct {
+ Base
+ AttachmentID string `gorm:"primaryKey" json:"attachmentId"`
+ VehicleID string `gorm:"primaryKey" json:"vehicleId"`
+ Title string `json:"title"`
+}
diff --git a/server/db/dbfunctions.go b/server/db/dbfunctions.go
new file mode 100644
index 0000000..8e5506f
--- /dev/null
+++ b/server/db/dbfunctions.go
@@ -0,0 +1,304 @@
+package db
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+func CanInitializeSystem() (bool, error) {
+ users, _ := GetAllUsers()
+ if len(*users) != 0 {
+ // db.MigrateClarkson("root:password@tcp(192.168.0.117:3306)/clarkson?charset=utf8mb4&parseTime=True&loc=Local")
+ return false,
+ fmt.Errorf("there are already users in the database. Migration can only be done on an empty database")
+ }
+ return true, nil
+}
+
+func CreateUser(user *User) error {
+ tx := DB.Create(&user)
+ return tx.Error
+}
+func UpdateUser(user *User) error {
+ tx := DB.Omit(clause.Associations).Save(&user)
+ return tx.Error
+}
+func FindOneUser(condition interface{}) (User, error) {
+
+ var model User
+ err := DB.Where(condition).First(&model).Error
+ return model, err
+}
+func GetAllUsers() (*[]User, error) {
+
+ sorting := "created_at desc"
+ var users []User
+ result := DB.Order(sorting).Find(&users)
+ return &users, result.Error
+}
+
+func GetAllVehicles(sorting string) (*[]Vehicle, error) {
+ if sorting == "" {
+ sorting = "created_at desc"
+ }
+ var vehicles []Vehicle
+ result := DB.Preload("Fillups", func(db *gorm.DB) *gorm.DB {
+ return db.Order("fillups.date DESC")
+ }).Preload("Expenses", func(db *gorm.DB) *gorm.DB {
+ return db.Order("expenses.date DESC")
+ }).Order(sorting).Find(&vehicles)
+ return &vehicles, result.Error
+}
+
+func GetVehicleOwner(vehicleId string) (string, error) {
+ var mapping UserVehicle
+
+ tx := DB.Where("vehicle_id = ? AND is_owner = 1", vehicleId).First(&mapping)
+
+ if tx.Error != nil {
+ return "", tx.Error
+ }
+ return mapping.ID, nil
+}
+
+func GetVehicleUsers(vehicleId string) (*[]UserVehicle, error) {
+ var mapping []UserVehicle
+
+ tx := DB.Debug().Preload("User").Where("vehicle_id = ?", vehicleId).Find(&mapping)
+
+ if tx.Error != nil {
+ return nil, tx.Error
+ }
+ return &mapping, nil
+}
+
+func ShareVehicle(vehicleId, userId string) error {
+ var mapping UserVehicle
+
+ tx := DB.Where("vehicle_id = ? AND user_id = ?", vehicleId, userId).First(&mapping)
+
+ if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
+ newMapping := UserVehicle{
+ UserID: userId,
+ VehicleID: vehicleId,
+ IsOwner: false,
+ }
+ tx = DB.Create(&newMapping)
+ return tx.Error
+ }
+ return nil
+}
+
+func UnshareVehicle(vehicleId, userId string) error {
+ var mapping UserVehicle
+
+ tx := DB.Where("vehicle_id = ? AND user_id = ?", vehicleId, userId).First(&mapping)
+
+ if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
+ return nil
+ }
+ if mapping.IsOwner {
+ return fmt.Errorf("Cannot unshare owner")
+ }
+ result := DB.Where("id=?", mapping.ID).Delete(&UserVehicle{})
+ return result.Error
+}
+
+func GetUserVehicles(id string) (*[]Vehicle, error) {
+ var toReturn []Vehicle
+ user, err := GetUserById(id)
+ if err != nil {
+ return nil, err
+ }
+ err = DB.Preload("Fillups", func(db *gorm.DB) *gorm.DB {
+ return db.Order("fillups.date DESC")
+ }).Preload("Expenses", func(db *gorm.DB) *gorm.DB {
+ return db.Order("expenses.date DESC")
+ }).Model(user).Select("vehicles.*,user_vehicles.is_owner").Association("Vehicles").Find(&toReturn)
+ if err != nil {
+ return nil, err
+ }
+ return &toReturn, nil
+}
+func GetUserById(id string) (*User, error) {
+ var data User
+ result := DB.Preload(clause.Associations).First(&data, "id=?", id)
+ return &data, result.Error
+}
+func GetVehicleById(id string) (*Vehicle, error) {
+ var vehicle Vehicle
+ result := DB.Preload(clause.Associations).First(&vehicle, "id=?", id)
+ return &vehicle, result.Error
+}
+func GetFillupById(id string) (*Fillup, error) {
+ var obj Fillup
+ result := DB.Preload(clause.Associations).First(&obj, "id=?", id)
+ return &obj, result.Error
+}
+
+func GetFillupsByVehicleId(id string) (*[]Fillup, error) {
+ var obj []Fillup
+ result := DB.Preload(clause.Associations).Order("date desc").Find(&obj, &Fillup{VehicleID: id})
+ return &obj, result.Error
+}
+func FindFillups(condition interface{}) (*[]Fillup, error) {
+
+ var model []Fillup
+ err := DB.Where(condition).Find(&model).Error
+ return &model, err
+}
+
+func FindFillupsForDateRange(vehicleIds []string, start, end time.Time) (*[]Fillup, error) {
+
+ var model []Fillup
+ err := DB.Where("date <= ? AND date >= ? AND vehicle_id in ?", end, start, vehicleIds).Find(&model).Error
+ return &model, err
+}
+func FindExpensesForDateRange(vehicleIds []string, start, end time.Time) (*[]Expense, error) {
+
+ var model []Expense
+ err := DB.Where("date <= ? AND date >= ? AND vehicle_id in ?", end, start, vehicleIds).Find(&model).Error
+ return &model, err
+}
+
+func GetExpensesByVehicleId(id string) (*[]Expense, error) {
+ var obj []Expense
+ result := DB.Preload(clause.Associations).Order("date desc").Find(&obj, &Expense{VehicleID: id})
+ return &obj, result.Error
+}
+func GetExpenseById(id string) (*Expense, error) {
+ var obj Expense
+ result := DB.Preload(clause.Associations).First(&obj, "id=?", id)
+ return &obj, result.Error
+}
+
+func DeleteFillupById(id string) error {
+
+ result := DB.Where("id=?", id).Delete(&Fillup{})
+ return result.Error
+}
+func DeleteExpenseById(id string) error {
+ result := DB.Where("id=?", id).Delete(&Expense{})
+ return result.Error
+}
+
+func GetAllQuickEntries(sorting string) (*[]QuickEntry, error) {
+ if sorting == "" {
+ sorting = "created_at desc"
+ }
+ var quickEntries []QuickEntry
+ result := DB.Preload(clause.Associations).Order(sorting).Find(&quickEntries)
+ return &quickEntries, result.Error
+}
+func GetQuickEntriesForUser(userId, sorting string) (*[]QuickEntry, error) {
+ if sorting == "" {
+ sorting = "created_at desc"
+ }
+ var quickEntries []QuickEntry
+ result := DB.Preload(clause.Associations).Where("user_id = ?", userId).Order(sorting).Find(&quickEntries)
+ return &quickEntries, result.Error
+}
+func GetQuickEntryById(id string) (*QuickEntry, error) {
+ var quickEntry QuickEntry
+ result := DB.Preload(clause.Associations).First(&quickEntry, "id=?", id)
+ return &quickEntry, result.Error
+}
+func UpdateQuickEntry(entry *QuickEntry) error {
+ return DB.Save(entry).Error
+}
+func SetQuickEntryAsProcessed(id string, processDate time.Time) error {
+ result := DB.Model(QuickEntry{}).Where("id=?", id).Update("process_date", processDate)
+ return result.Error
+}
+
+func GetAttachmentById(id string) (*Attachment, error) {
+ var entry Attachment
+ result := DB.Preload(clause.Associations).First(&entry, "id=?", id)
+ return &entry, result.Error
+}
+func GetVehicleAttachments(vehicleId string) (*[]Attachment, error) {
+ var attachments []Attachment
+ vehicle, err := GetVehicleById(vehicleId)
+ if err != nil {
+ return nil, err
+ }
+ err = DB.Debug().Model(vehicle).Select("attachments.*,vehicle_attachments.title").Preload("User").Association("Attachments").Find(&attachments)
+ if err != nil {
+ return nil, err
+ }
+ return &attachments, nil
+}
+
+func UpdateSettings(setting *Setting) error {
+ tx := DB.Save(&setting)
+ return tx.Error
+}
+func GetOrCreateSetting() *Setting {
+ var setting Setting
+ result := DB.First(&setting)
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ DB.Save(&Setting{})
+ DB.First(&setting)
+ }
+ return &setting
+}
+
+func GetLock(name string) *JobLock {
+ var jobLock JobLock
+ result := DB.Where("name = ?", name).First(&jobLock)
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ return &JobLock{
+ Name: name,
+ }
+ }
+ return &jobLock
+}
+func Lock(name string, duration int) {
+ jobLock := GetLock(name)
+ if jobLock == nil {
+ jobLock = &JobLock{
+ Name: name,
+ }
+ }
+ jobLock.Duration = duration
+ jobLock.Date = time.Now()
+ if jobLock.ID == "" {
+ DB.Create(&jobLock)
+ } else {
+ DB.Save(&jobLock)
+ }
+}
+func Unlock(name string) {
+ jobLock := GetLock(name)
+ if jobLock == nil {
+ return
+ }
+ jobLock.Duration = 0
+ jobLock.Date = time.Time{}
+ DB.Save(&jobLock)
+}
+
+func UnlockMissedJobs() {
+ var jobLocks []JobLock
+
+ result := DB.Find(&jobLocks)
+ if result.Error != nil {
+ return
+ }
+ for _, job := range jobLocks {
+ if (job.Date == time.Time{}) {
+ continue
+ }
+ var duration time.Duration
+ duration = time.Duration(job.Duration)
+ d := job.Date.Add(time.Minute * duration)
+ if d.Before(time.Now()) {
+ fmt.Println(job.Name + " is unlocked")
+ Unlock(job.Name)
+ }
+ }
+}
diff --git a/server/db/enums.go b/server/db/enums.go
new file mode 100644
index 0000000..eb6cdf0
--- /dev/null
+++ b/server/db/enums.go
@@ -0,0 +1,111 @@
+package db
+
+type FuelUnit int
+
+const (
+ LITRE FuelUnit = iota
+ GALLON
+ US_GALLON
+ KILOGRAM
+ KILOWATT_HOUR
+ MINUTE
+)
+
+type FuelType int
+
+const (
+ PETROL FuelType = iota
+ DIESEL
+ ETHANOL
+ CNG
+ ELECTRIC
+ LPG
+)
+
+type DistanceUnit int
+
+const (
+ MILES DistanceUnit = iota
+ KILOMETERS
+)
+
+type Role int
+
+const (
+ ADMIN Role = iota
+ USER
+)
+
+type EnumDetail struct {
+ Short string `json:"short"`
+ Long string `json:"long"`
+}
+
+var FuelUnitDetails map[FuelUnit]EnumDetail = map[FuelUnit]EnumDetail{
+ LITRE: {
+ Short: "Lt",
+ Long: "Litre",
+ },
+ GALLON: {
+ Short: "Gal",
+ Long: "Gallon",
+ }, KILOGRAM: {
+ Short: "Kg",
+ Long: "Kilogram",
+ }, KILOWATT_HOUR: {
+ Short: "KwH",
+ Long: "Kilowatt Hour",
+ }, US_GALLON: {
+ Short: "US Gal",
+ Long: "US Gallon",
+ },
+ MINUTE: {
+ Short: "Mins",
+ Long: "Minutes",
+ },
+}
+
+var FuelTypeDetails map[FuelType]EnumDetail = map[FuelType]EnumDetail{
+ PETROL: {
+ Short: "Petrol",
+ Long: "Petrol",
+ },
+ DIESEL: {
+ Short: "Diesel",
+ Long: "Diesel",
+ }, CNG: {
+ Short: "CNG",
+ Long: "CNG",
+ }, LPG: {
+ Short: "LPG",
+ Long: "LPG",
+ }, ELECTRIC: {
+ Short: "Electric",
+ Long: "Electric",
+ }, ETHANOL: {
+ Short: "Ethanol",
+ Long: "Ethanol",
+ },
+}
+
+var DistanceUnitDetails map[DistanceUnit]EnumDetail = map[DistanceUnit]EnumDetail{
+ KILOMETERS: {
+ Short: "Km",
+ Long: "Kilometers",
+ },
+ MILES: {
+ Short: "Mi",
+ Long: "Miles",
+ },
+}
+
+var RoleDetails map[Role]EnumDetail = map[Role]EnumDetail{
+ ADMIN: {
+ Short: "Admin",
+ Long: "ADMIN",
+ },
+ USER: {
+ Short: "User",
+ Long: "USER",
+ },
+}
diff --git a/server/db/migrations.go b/server/db/migrations.go
new file mode 100644
index 0000000..6bb9d83
--- /dev/null
+++ b/server/db/migrations.go
@@ -0,0 +1,43 @@
+package db
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "gorm.io/gorm"
+)
+
+type localMigration struct {
+ Name string
+ Query string
+}
+
+var migrations = []localMigration{
+ // {
+ // Name: "2020_11_03_04_42_SetDefaultDownloadStatus",
+ // Query: "update podcast_items set download_status=2 where download_path!='' and download_status=0",
+ // },
+}
+
+func RunMigrations() {
+ for _, mig := range migrations {
+ ExecuteAndSaveMigration(mig.Name, mig.Query)
+ }
+}
+func ExecuteAndSaveMigration(name string, query string) error {
+ var migration Migration
+ result := DB.Where("name=?", name).First(&migration)
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ fmt.Println(query)
+ result = DB.Debug().Exec(query)
+ if result.Error == nil {
+ DB.Save(&Migration{
+ Date: time.Now(),
+ Name: name,
+ })
+ }
+ return result.Error
+ }
+ return nil
+}
diff --git a/server/go.mod b/server/go.mod
new file mode 100644
index 0000000..457bdc7
--- /dev/null
+++ b/server/go.mod
@@ -0,0 +1,20 @@
+module github.com/akhilrex/hammond
+
+go 1.15
+
+require (
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
+ github.com/gin-contrib/location v0.0.2
+ github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 // indirect
+ github.com/gin-gonic/gin v1.7.1
+ github.com/go-playground/validator/v10 v10.4.1
+ github.com/jasonlvhit/gocron v0.0.1
+ github.com/joho/godotenv v1.3.0
+ github.com/satori/go.uuid v1.2.0
+ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
+ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gorm.io/driver/mysql v1.0.5
+ gorm.io/driver/sqlite v1.1.4
+ gorm.io/gorm v1.21.3
+)
diff --git a/server/go.sum b/server/go.sum
new file mode 100644
index 0000000..6548b7c
--- /dev/null
+++ b/server/go.sum
@@ -0,0 +1,110 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4=
+github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 h1:J2LPEOcQmWaooBnBtUDV9KHFEnP5LYTZY03GiQ0oQBw=
+github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
+github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
+github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
+github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
+github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
+golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o=
+gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI=
+gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
+gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
+gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gorm.io/gorm v1.21.3 h1:qDFi55ZOsjZTwk5eN+uhAmHi8GysJ/qCTichM/yO7ME=
+gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
diff --git a/server/internal/sanitize/.gitignore b/server/internal/sanitize/.gitignore
new file mode 100644
index 0000000..0026861
--- /dev/null
+++ b/server/internal/sanitize/.gitignore
@@ -0,0 +1,22 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
diff --git a/server/internal/sanitize/LICENSE b/server/internal/sanitize/LICENSE
new file mode 100644
index 0000000..749ebb2
--- /dev/null
+++ b/server/internal/sanitize/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2017 Mechanism Design. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/server/internal/sanitize/README.md b/server/internal/sanitize/README.md
new file mode 100644
index 0000000..4401ef7
--- /dev/null
+++ b/server/internal/sanitize/README.md
@@ -0,0 +1,62 @@
+sanitize [](https://godoc.org/github.com/kennygrant/sanitize) [](https://goreportcard.com/report/github.com/kennygrant/sanitize) [](https://circleci.com/gh/kennygrant/sanitize)
+========
+
+Package sanitize provides functions to sanitize html and paths with go (golang).
+
+FUNCTIONS
+
+
+```go
+sanitize.Accents(s string) string
+```
+
+Accents replaces a set of accented characters with ascii equivalents.
+
+```go
+sanitize.BaseName(s string) string
+```
+
+BaseName makes a string safe to use in a file name, producing a sanitized basename replacing . or / with -. Unlike Name no attempt is made to normalise text as a path.
+
+```go
+sanitize.HTML(s string) string
+```
+
+HTML strips html tags with a very simple parser, replace common entities, and escape < and > in the result. The result is intended to be used as plain text.
+
+```go
+sanitize.HTMLAllowing(s string, args...[]string) (string, error)
+```
+
+HTMLAllowing parses html and allow certain tags and attributes from the lists optionally specified by args - args[0] is a list of allowed tags, args[1] is a list of allowed attributes. If either is missing default sets are used.
+
+```go
+sanitize.Name(s string) string
+```
+
+Name makes a string safe to use in a file name by first finding the path basename, then replacing non-ascii characters.
+
+```go
+sanitize.Path(s string) string
+```
+
+Path makes a string safe to use as an url path.
+
+
+Changes
+-------
+
+Version 1.2
+
+Adjusted HTML function to avoid linter warning
+Added more tests from https://githubengineering.com/githubs-post-csp-journey/
+Chnaged name of license file
+Added badges and change log to readme
+
+Version 1.1
+Fixed type in comments.
+Merge pull request from Povilas Balzaravicius Pawka
+ - replace br tags with newline even when they contain a space
+
+Version 1.0
+First release
\ No newline at end of file
diff --git a/server/internal/sanitize/sanitize.go b/server/internal/sanitize/sanitize.go
new file mode 100644
index 0000000..8854722
--- /dev/null
+++ b/server/internal/sanitize/sanitize.go
@@ -0,0 +1,388 @@
+// Package sanitize provides functions for sanitizing text.
+package sanitize
+
+import (
+ "bytes"
+ "html"
+ "html/template"
+ "io"
+ "path"
+ "regexp"
+ "strings"
+
+ parser "golang.org/x/net/html"
+)
+
+var (
+ ignoreTags = []string{"title", "script", "style", "iframe", "frame", "frameset", "noframes", "noembed", "embed", "applet", "object", "base"}
+
+ defaultTags = []string{"h1", "h2", "h3", "h4", "h5", "h6", "div", "span", "hr", "p", "br", "b", "i", "strong", "em", "ol", "ul", "li", "a", "img", "pre", "code", "blockquote", "article", "section"}
+
+ defaultAttributes = []string{"id", "class", "src", "href", "title", "alt", "name", "rel"}
+)
+
+// HTMLAllowing sanitizes html, allowing some tags.
+// Arrays of allowed tags and allowed attributes may optionally be passed as the second and third arguments.
+func HTMLAllowing(s string, args ...[]string) (string, error) {
+
+ allowedTags := defaultTags
+ if len(args) > 0 {
+ allowedTags = args[0]
+ }
+ allowedAttributes := defaultAttributes
+ if len(args) > 1 {
+ allowedAttributes = args[1]
+ }
+
+ // Parse the html
+ tokenizer := parser.NewTokenizer(strings.NewReader(s))
+
+ buffer := bytes.NewBufferString("")
+ ignore := ""
+
+ for {
+ tokenType := tokenizer.Next()
+ token := tokenizer.Token()
+
+ switch tokenType {
+
+ case parser.ErrorToken:
+ err := tokenizer.Err()
+ if err == io.EOF {
+ return buffer.String(), nil
+ }
+ return "", err
+
+ case parser.StartTagToken:
+
+ if len(ignore) == 0 && includes(allowedTags, token.Data) {
+ token.Attr = cleanAttributes(token.Attr, allowedAttributes)
+ buffer.WriteString(token.String())
+ } else if includes(ignoreTags, token.Data) {
+ ignore = token.Data
+ }
+
+ case parser.SelfClosingTagToken:
+
+ if len(ignore) == 0 && includes(allowedTags, token.Data) {
+ token.Attr = cleanAttributes(token.Attr, allowedAttributes)
+ buffer.WriteString(token.String())
+ } else if token.Data == ignore {
+ ignore = ""
+ }
+
+ case parser.EndTagToken:
+ if len(ignore) == 0 && includes(allowedTags, token.Data) {
+ token.Attr = []parser.Attribute{}
+ buffer.WriteString(token.String())
+ } else if token.Data == ignore {
+ ignore = ""
+ }
+
+ case parser.TextToken:
+ // We allow text content through, unless ignoring this entire tag and its contents (including other tags)
+ if ignore == "" {
+ buffer.WriteString(token.String())
+ }
+ case parser.CommentToken:
+ // We ignore comments by default
+ case parser.DoctypeToken:
+ // We ignore doctypes by default - html5 does not require them and this is intended for sanitizing snippets of text
+ default:
+ // We ignore unknown token types by default
+
+ }
+
+ }
+
+}
+
+// HTML strips html tags, replace common entities, and escapes <>&;'" in the result.
+// Note the returned text may contain entities as it is escaped by HTMLEscapeString, and most entities are not translated.
+func HTML(s string) (output string) {
+
+ // Shortcut strings with no tags in them
+ if !strings.ContainsAny(s, "<>") {
+ output = s
+ } else {
+
+ // First remove line breaks etc as these have no meaning outside html tags (except pre)
+ // this means pre sections will lose formatting... but will result in less unintentional paras.
+ s = strings.Replace(s, "\n", "", -1)
+
+ // Then replace line breaks with newlines, to preserve that formatting
+ s = strings.Replace(s, "", "\n", -1)
+ s = strings.Replace(s, "
+
{{ item.label }}
+{{ item.value }}
+Hello!
' + const { element } = shallowMount(MainLayout, { + slots: { + default: slotContent, + }, + }) + expect(element.innerHTML).toContain(slotContent) + }) +}) diff --git a/ui/src/router/layouts/main.vue b/ui/src/router/layouts/main.vue new file mode 100644 index 0000000..7677274 --- /dev/null +++ b/ui/src/router/layouts/main.vue @@ -0,0 +1,14 @@ + + ++ If you have an existing Clarkson deployment and you want to migrate your data from that, + press the following button. +
++ If you want a fresh install of Hammond, press the following button. +
+You need to make sure that this deployment of Hammond can access the MySQL database used by + Clarkson.
+If that is not directly possible, you can make a copy of that database somewhere accessible + from this instance.
+Once that is done, enter the connection string to the MySQL instance in the following + format.
+All the users imported from Clarkson will have their username as their email in Clarkson + database and pasword set to hammond
+
+ user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
+
+ {{ user }}
+ {{ entry.comments }}
No Quick Entries right now.
++ This project is under active development which means I release new updates very frequently. I will eventually build the version + management/update checking mechanism. Until then it is recommended that you use something like watchtower which will automatically update + your containers whenever I release a new version or periodically rebuild the container with the latest image manually. +
+| Current Version | +2021.05.07 | +
| Website | +https://github.com/akhilrex/hammond | +
| Found a bug | +Report here | +
| Feature Request | +Request here | +
| Support the developer | +Support here | +
{{ vehicle.nickname }} - {{ vehicle.registration }}
++ {{ [vehicle.make, vehicle.model, vehicle.fuelTypeDetail.long].join(' | ') }} + + + | Shared with : + {{ + users + .map((x) => { + if (x.userId === me.id) { + return 'You' + } else { + return x.name + } + }) + .join(', ') + }} + +
+{{ item.label }}
+{{ item.value }}
+{{ props.row.id }}
+ +