first commit

This commit is contained in:
Akhil Gupta
2021-05-29 15:20:50 +05:30
commit d25c30a7b2
194 changed files with 49873 additions and 0 deletions

3
server/.env Normal file
View File

@@ -0,0 +1,3 @@
CONFIG=.
DATA=./assets
JWT_SECRET="A super strong secret that needs to be changed"

21
server/.gitignore vendored Normal file
View File

@@ -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/*

41
server/Dockerfile Normal file
View File

@@ -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"]

89
server/common/utils.go Normal file
View File

@@ -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)
}

179
server/controllers/auth.go Normal file
View File

@@ -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(&registerRequest); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
if err := service.CreateUser(&registerRequest, *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(&registerRequest); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
if err := service.CreateUser(&registerRequest, 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)
}

157
server/controllers/files.go Normal file
View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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)
}
}
}

View File

@@ -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))
}
}

View File

@@ -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)
}

View File

@@ -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))
}
}

22
server/db/base.go Normal file
View File

@@ -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
}

223
server/db/clarkson.go Normal file
View File

@@ -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
}

58
server/db/db.go Normal file
View File

@@ -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
}

192
server/db/dbModels.go Normal file
View File

@@ -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"`
}

304
server/db/dbfunctions.go Normal file
View File

@@ -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)
}
}
}

111
server/db/enums.go Normal file
View File

@@ -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",
},
}

43
server/db/migrations.go Normal file
View File

@@ -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
}

20
server/go.mod Normal file
View File

@@ -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
)

110
server/go.sum Normal file
View File

@@ -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=

22
server/internal/sanitize/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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.

View File

@@ -0,0 +1,62 @@
sanitize [![GoDoc](https://godoc.org/github.com/kennygrant/sanitize?status.svg)](https://godoc.org/github.com/kennygrant/sanitize) [![Go Report Card](https://goreportcard.com/badge/github.com/kennygrant/sanitize)](https://goreportcard.com/report/github.com/kennygrant/sanitize) [![CircleCI](https://circleci.com/gh/kennygrant/sanitize.svg?style=svg)](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

View File

@@ -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, "</p>", "\n", -1)
s = strings.Replace(s, "<br>", "\n", -1)
s = strings.Replace(s, "</br>", "\n", -1)
s = strings.Replace(s, "<br/>", "\n", -1)
s = strings.Replace(s, "<br />", "\n", -1)
// Walk through the string removing all tags
b := bytes.NewBufferString("")
inTag := false
for _, r := range s {
switch r {
case '<':
inTag = true
case '>':
inTag = false
default:
if !inTag {
b.WriteRune(r)
}
}
}
output = b.String()
}
// Remove a few common harmless entities, to arrive at something more like plain text
output = strings.Replace(output, "&#8216;", "'", -1)
output = strings.Replace(output, "&#8217;", "'", -1)
output = strings.Replace(output, "&#8220;", "\"", -1)
output = strings.Replace(output, "&#8221;", "\"", -1)
output = strings.Replace(output, "&nbsp;", " ", -1)
output = strings.Replace(output, "&quot;", "\"", -1)
output = strings.Replace(output, "&apos;", "'", -1)
// Translate some entities into their plain text equivalent (for example accents, if encoded as entities)
output = html.UnescapeString(output)
// In case we have missed any tags above, escape the text - removes <, >, &, ' and ".
output = template.HTMLEscapeString(output)
// After processing, remove some harmless entities &, ' and " which are encoded by HTMLEscapeString
output = strings.Replace(output, "&#34;", "\"", -1)
output = strings.Replace(output, "&#39;", "'", -1)
output = strings.Replace(output, "&amp; ", "& ", -1) // NB space after
output = strings.Replace(output, "&amp;amp; ", "& ", -1) // NB space after
return output
}
// We are very restrictive as this is intended for ascii url slugs
var illegalPath = regexp.MustCompile(`[^[:alnum:]\~\-\./]`)
// Path makes a string safe to use as a URL path,
// removing accents and replacing separators with -.
// The path may still start at / and is not intended
// for use as a file system path without prefix.
func Path(s string) string {
// Start with lowercase string
filePath := strings.ToLower(s)
filePath = strings.Replace(filePath, "..", "", -1)
filePath = path.Clean(filePath)
// Remove illegal characters for paths, flattening accents
// and replacing some common separators with -
filePath = cleanString(filePath, illegalPath)
// NB this may be of length 0, caller must check
return filePath
}
// Remove all other unrecognised characters apart from
var illegalName = regexp.MustCompile(`[^[:alnum:]-.]`)
// Name makes a string safe to use in a file name by first finding the path basename, then replacing non-ascii characters.
func Name(s string) string {
// Start with lowercase string
fileName := s
fileName = path.Clean(path.Base(fileName))
// Remove illegal characters for names, replacing some common separators with -
fileName = cleanString(fileName, illegalName)
// NB this may be of length 0, caller must check
return fileName
}
// Replace these separators with -
var baseNameSeparators = regexp.MustCompile(`[./]`)
// BaseName makes a string safe to use in a file name, producing a sanitized basename replacing . or / with -.
// No attempt is made to normalise a path or normalise case.
func BaseName(s string) string {
// Replace certain joining characters with a dash
baseName := baseNameSeparators.ReplaceAllString(s, "-")
// Remove illegal characters for names, replacing some common separators with -
baseName = cleanString(baseName, illegalName)
// NB this may be of length 0, caller must check
return baseName
}
// A very limited list of transliterations to catch common european names translated to urls.
// This set could be expanded with at least caps and many more characters.
var transliterations = map[rune]string{
'À': "A",
'Á': "A",
'Â': "A",
'Ã': "A",
'Ä': "A",
'Å': "AA",
'Æ': "AE",
'Ç': "C",
'È': "E",
'É': "E",
'Ê': "E",
'Ë': "E",
'Ì': "I",
'Í': "I",
'Î': "I",
'Ï': "I",
'Ð': "D",
'Ł': "L",
'Ñ': "N",
'Ò': "O",
'Ó': "O",
'Ô': "O",
'Õ': "O",
'Ö': "OE",
'Ø': "OE",
'Œ': "OE",
'Ù': "U",
'Ú': "U",
'Ü': "UE",
'Û': "U",
'Ý': "Y",
'Þ': "TH",
'ẞ': "SS",
'à': "a",
'á': "a",
'â': "a",
'ã': "a",
'ä': "ae",
'å': "aa",
'æ': "ae",
'ç': "c",
'è': "e",
'é': "e",
'ê': "e",
'ë': "e",
'ì': "i",
'í': "i",
'î': "i",
'ï': "i",
'ð': "d",
'ł': "l",
'ñ': "n",
'ń': "n",
'ò': "o",
'ó': "o",
'ô': "o",
'õ': "o",
'ō': "o",
'ö': "oe",
'ø': "oe",
'œ': "oe",
'ś': "s",
'ù': "u",
'ú': "u",
'û': "u",
'ū': "u",
'ü': "ue",
'ý': "y",
'ÿ': "y",
'ż': "z",
'þ': "th",
'ß': "ss",
}
// Accents replaces a set of accented characters with ascii equivalents.
func Accents(s string) string {
// Replace some common accent characters
b := bytes.NewBufferString("")
for _, c := range s {
// Check transliterations first
if val, ok := transliterations[c]; ok {
b.WriteString(val)
} else {
b.WriteRune(c)
}
}
return b.String()
}
var (
// If the attribute contains data: or javascript: anywhere, ignore it
// we don't allow this in attributes as it is so frequently used for xss
// NB we allow spaces in the value, and lowercase.
illegalAttr = regexp.MustCompile(`(d\s*a\s*t\s*a|j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*)\s*:`)
// We are far more restrictive with href attributes.
legalHrefAttr = regexp.MustCompile(`\A[/#][^/\\]?|mailto:|http://|https://`)
)
// cleanAttributes returns an array of attributes after removing malicious ones.
func cleanAttributes(a []parser.Attribute, allowed []string) []parser.Attribute {
if len(a) == 0 {
return a
}
var cleaned []parser.Attribute
for _, attr := range a {
if includes(allowed, attr.Key) {
val := strings.ToLower(attr.Val)
// Check for illegal attribute values
if illegalAttr.FindString(val) != "" {
attr.Val = ""
}
// Check for legal href values - / mailto:// http:// or https://
if attr.Key == "href" {
if legalHrefAttr.FindString(val) == "" {
attr.Val = ""
}
}
// If we still have an attribute, append it to the array
if attr.Val != "" {
cleaned = append(cleaned, attr)
}
}
}
return cleaned
}
// A list of characters we consider separators in normal strings and replace with our canonical separator - rather than removing.
var (
separators = regexp.MustCompile(`[!&_="#|+?:]`)
dashes = regexp.MustCompile(`[\-]+`)
)
// cleanString replaces separators with - and removes characters listed in the regexp provided from string.
// Accents, spaces, and all characters not in A-Za-z0-9 are replaced.
func cleanString(s string, r *regexp.Regexp) string {
// Remove any trailing space to avoid ending on -
s = strings.Trim(s, " ")
// Flatten accents first so that if we remove non-ascii we still get a legible name
s = Accents(s)
// Replace certain joining characters with a dash
s = separators.ReplaceAllString(s, "-")
// Remove all other unrecognised characters - NB we do allow any printable characters
//s = r.ReplaceAllString(s, "")
// Remove any multiple dashes caused by replacements above
s = dashes.ReplaceAllString(s, "-")
return s
}
// includes checks for inclusion of a string in a []string.
func includes(a []string, s string) bool {
for _, as := range a {
if as == s {
return true
}
}
return false
}

80
server/main.go Normal file
View File

@@ -0,0 +1,80 @@
package main
import (
"fmt"
"log"
"os"
"github.com/akhilrex/hammond/controllers"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/service"
"github.com/gin-contrib/location"
"github.com/gin-gonic/contrib/static"
"github.com/gin-gonic/gin"
"github.com/jasonlvhit/gocron"
_ "github.com/joho/godotenv/autoload"
)
func main() {
var err error
db.DB, err = db.Init()
if err != nil {
fmt.Println("status: ", err)
} else {
db.Migrate()
}
r := gin.Default()
r.Use(setupSettings())
r.Use(gin.Recovery())
r.Use(location.Default())
r.Use(static.Serve("/", static.LocalFile("./dist", true)))
r.NoRoute(func(c *gin.Context) {
//fmt.'Println(c.Request.URL.Path)
c.File("dist/index.html")
})
router := r.Group("/api")
dataPath := os.Getenv("DATA")
router.Static("/assets/", dataPath)
controllers.RegisterAnonController(router)
controllers.RegisterAnonMasterConroller(router)
controllers.RegisterSetupController(router)
router.Use(controllers.AuthMiddleware(true))
controllers.RegisterUserController(router)
controllers.RegisterMastersController(router)
controllers.RegisterAuthController(router)
controllers.RegisterVehicleController(router)
controllers.RegisterFilesController(router)
go assetEnv()
go intiCron()
r.Run(":3000") // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
func setupSettings() gin.HandlerFunc {
return func(c *gin.Context) {
setting := db.GetOrCreateSetting()
c.Set("setting", setting)
c.Writer.Header().Set("X-Clacks-Overhead", "GNU Terry Pratchett")
c.Next()
}
}
func intiCron() {
//gocron.Every(uint64(checkFrequency)).Minutes().Do(service.DownloadMissingEpisodes)
gocron.Every(2).Days().Do(service.CreateBackup)
<-gocron.Start()
}
func assetEnv() {
log.Println("Config Dir: ", os.Getenv("CONFIG"))
log.Println("Assets Dir: ", os.Getenv("DATA"))
}

30
server/models/auth.go Normal file
View File

@@ -0,0 +1,30 @@
package models
import "github.com/akhilrex/hammond/db"
type LoginResponse struct {
Name string `json:"name"`
Email string `json:"email"`
Token string `json:"token"`
RefreshToken string `json:"refreshToken"`
Role string `json:"role"`
}
type LoginRequest struct {
Email string `form:"email" json:"email" binding:"required,email"`
Password string `form:"password" json:"password" binding:"required,min=6,max=255"`
}
type RegisterRequest struct {
Name string `form:"name" json:"name"`
Email string `form:"email" json:"email" binding:"required,email"`
Password string `form:"password" json:"password" binding:"required,min=8,max=255"`
Currency string `json:"currency" form:"currency" query:"currency"`
DistanceUnit *db.DistanceUnit `json:"distanceUnit" form:"distanceUnit" query:"distanceUnit" `
Role *db.Role `json:"role" form:"role" query:"role" `
}
type ChangePasswordRequest struct {
OldPassword string `form:"oldPassword" json:"oldPassword" binding:"required,min=8,max=255"`
NewPassword string `form:"newPassword" json:"newPassword" binding:"required,min=8,max=255"`
}

849
server/models/currency.go Normal file
View File

@@ -0,0 +1,849 @@
package models
type CurrencyModel struct {
Symbol string `json:"symbol"`
SymbolNative string `json:"symbolNative"`
DecimalDigits string `json:"decimalDigits"`
Rounding string `json:"rounding"`
Code string `json:"code"`
NamePlural string `json:"namePlural"`
}
func GetCurrencyMasterList() []CurrencyModel {
return []CurrencyModel{
{
Symbol: "$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "USD",
NamePlural: "US dollars",
}, {
Symbol: "CA$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "CAD",
NamePlural: "Canadian dollars",
}, {
Symbol: "€",
SymbolNative: "€",
DecimalDigits: "2",
Rounding: "0",
Code: "EUR",
NamePlural: "euros",
}, {
Symbol: "AED",
SymbolNative: "د.إ.",
DecimalDigits: "2",
Rounding: "0",
Code: "AED",
NamePlural: "UAE dirhams",
}, {
Symbol: "Af",
SymbolNative: "؋",
DecimalDigits: "0",
Rounding: "0",
Code: "AFN",
NamePlural: "Afghan Afghanis",
}, {
Symbol: "ALL",
SymbolNative: "Lek",
DecimalDigits: "0",
Rounding: "0",
Code: "ALL",
NamePlural: "Albanian lekë",
}, {
Symbol: "AMD",
SymbolNative: "դր.",
DecimalDigits: "0",
Rounding: "0",
Code: "AMD",
NamePlural: "Armenian drams",
}, {
Symbol: "AR$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "ARS",
NamePlural: "Argentine pesos",
}, {
Symbol: "AU$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "AUD",
NamePlural: "Australian dollars",
}, {
Symbol: "man.",
SymbolNative: "ман.",
DecimalDigits: "2",
Rounding: "0",
Code: "AZN",
NamePlural: "Azerbaijani manats",
}, {
Symbol: "KM",
SymbolNative: "KM",
DecimalDigits: "2",
Rounding: "0",
Code: "BAM",
NamePlural: "Bosnia-Herzegovina convertible marks",
}, {
Symbol: "Tk",
SymbolNative: "৳",
DecimalDigits: "2",
Rounding: "0",
Code: "BDT",
NamePlural: "Bangladeshi takas",
}, {
Symbol: "BGN",
SymbolNative: "лв.",
DecimalDigits: "2",
Rounding: "0",
Code: "BGN",
NamePlural: "Bulgarian leva",
}, {
Symbol: "BD",
SymbolNative: "د.ب.",
DecimalDigits: "3",
Rounding: "0",
Code: "BHD",
NamePlural: "Bahraini dinars",
}, {
Symbol: "FBu",
SymbolNative: "FBu",
DecimalDigits: "0",
Rounding: "0",
Code: "BIF",
NamePlural: "Burundian francs",
}, {
Symbol: "BN$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "BND",
NamePlural: "Brunei dollars",
}, {
Symbol: "Bs",
SymbolNative: "Bs",
DecimalDigits: "2",
Rounding: "0",
Code: "BOB",
NamePlural: "Bolivian bolivianos",
}, {
Symbol: "R$",
SymbolNative: "R$",
DecimalDigits: "2",
Rounding: "0",
Code: "BRL",
NamePlural: "Brazilian reals",
}, {
Symbol: "BWP",
SymbolNative: "P",
DecimalDigits: "2",
Rounding: "0",
Code: "BWP",
NamePlural: "Botswanan pulas",
}, {
Symbol: "Br",
SymbolNative: "руб.",
DecimalDigits: "2",
Rounding: "0",
Code: "BYN",
NamePlural: "Belarusian rubles",
}, {
Symbol: "BZ$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "BZD",
NamePlural: "Belize dollars",
}, {
Symbol: "CDF",
SymbolNative: "FrCD",
DecimalDigits: "2",
Rounding: "0",
Code: "CDF",
NamePlural: "Congolese francs",
}, {
Symbol: "CHF",
SymbolNative: "CHF",
DecimalDigits: "2",
Rounding: "0.05",
Code: "CHF",
NamePlural: "Swiss francs",
}, {
Symbol: "CL$",
SymbolNative: "$",
DecimalDigits: "0",
Rounding: "0",
Code: "CLP",
NamePlural: "Chilean pesos",
}, {
Symbol: "CN¥",
SymbolNative: "CN¥",
DecimalDigits: "2",
Rounding: "0",
Code: "CNY",
NamePlural: "Chinese yuan",
}, {
Symbol: "CO$",
SymbolNative: "$",
DecimalDigits: "0",
Rounding: "0",
Code: "COP",
NamePlural: "Colombian pesos",
}, {
Symbol: "₡",
SymbolNative: "₡",
DecimalDigits: "0",
Rounding: "0",
Code: "CRC",
NamePlural: "Costa Rican colóns",
}, {
Symbol: "CV$",
SymbolNative: "CV$",
DecimalDigits: "2",
Rounding: "0",
Code: "CVE",
NamePlural: "Cape Verdean escudos",
}, {
Symbol: "Kč",
SymbolNative: "Kč",
DecimalDigits: "2",
Rounding: "0",
Code: "CZK",
NamePlural: "Czech Republic korunas",
}, {
Symbol: "Fdj",
SymbolNative: "Fdj",
DecimalDigits: "0",
Rounding: "0",
Code: "DJF",
NamePlural: "Djiboutian francs",
}, {
Symbol: "Dkr",
SymbolNative: "kr",
DecimalDigits: "2",
Rounding: "0",
Code: "DKK",
NamePlural: "Danish kroner",
}, {
Symbol: "RD$",
SymbolNative: "RD$",
DecimalDigits: "2",
Rounding: "0",
Code: "DOP",
NamePlural: "Dominican pesos",
}, {
Symbol: "DA",
SymbolNative: "د.ج.",
DecimalDigits: "2",
Rounding: "0",
Code: "DZD",
NamePlural: "Algerian dinars",
}, {
Symbol: "Ekr",
SymbolNative: "kr",
DecimalDigits: "2",
Rounding: "0",
Code: "EEK",
NamePlural: "Estonian kroons",
}, {
Symbol: "EGP",
SymbolNative: "ج.م.",
DecimalDigits: "2",
Rounding: "0",
Code: "EGP",
NamePlural: "Egyptian pounds",
}, {
Symbol: "Nfk",
SymbolNative: "Nfk",
DecimalDigits: "2",
Rounding: "0",
Code: "ERN",
NamePlural: "Eritrean nakfas",
}, {
Symbol: "Br",
SymbolNative: "Br",
DecimalDigits: "2",
Rounding: "0",
Code: "ETB",
NamePlural: "Ethiopian birrs",
}, {
Symbol: "£",
SymbolNative: "£",
DecimalDigits: "2",
Rounding: "0",
Code: "GBP",
NamePlural: "British pounds sterling",
}, {
Symbol: "GEL",
SymbolNative: "GEL",
DecimalDigits: "2",
Rounding: "0",
Code: "GEL",
NamePlural: "Georgian laris",
}, {
Symbol: "GH₵",
SymbolNative: "GH₵",
DecimalDigits: "2",
Rounding: "0",
Code: "GHS",
NamePlural: "Ghanaian cedis",
}, {
Symbol: "FG",
SymbolNative: "FG",
DecimalDigits: "0",
Rounding: "0",
Code: "GNF",
NamePlural: "Guinean francs",
}, {
Symbol: "GTQ",
SymbolNative: "Q",
DecimalDigits: "2",
Rounding: "0",
Code: "GTQ",
NamePlural: "Guatemalan quetzals",
}, {
Symbol: "HK$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "HKD",
NamePlural: "Hong Kong dollars",
}, {
Symbol: "HNL",
SymbolNative: "L",
DecimalDigits: "2",
Rounding: "0",
Code: "HNL",
NamePlural: "Honduran lempiras",
}, {
Symbol: "kn",
SymbolNative: "kn",
DecimalDigits: "2",
Rounding: "0",
Code: "HRK",
NamePlural: "Croatian kunas",
}, {
Symbol: "Ft",
SymbolNative: "Ft",
DecimalDigits: "0",
Rounding: "0",
Code: "HUF",
NamePlural: "Hungarian forints",
}, {
Symbol: "Rp",
SymbolNative: "Rp",
DecimalDigits: "0",
Rounding: "0",
Code: "IDR",
NamePlural: "Indonesian rupiahs",
}, {
Symbol: "₪",
SymbolNative: "₪",
DecimalDigits: "2",
Rounding: "0",
Code: "ILS",
NamePlural: "Israeli new sheqels",
}, {
Symbol: "Rs",
SymbolNative: "টকা",
DecimalDigits: "2",
Rounding: "0",
Code: "INR",
NamePlural: "Indian rupees",
}, {
Symbol: "IQD",
SymbolNative: "د.ع.",
DecimalDigits: "0",
Rounding: "0",
Code: "IQD",
NamePlural: "Iraqi dinars",
}, {
Symbol: "IRR",
SymbolNative: "﷼",
DecimalDigits: "0",
Rounding: "0",
Code: "IRR",
NamePlural: "Iranian rials",
}, {
Symbol: "Ikr",
SymbolNative: "kr",
DecimalDigits: "0",
Rounding: "0",
Code: "ISK",
NamePlural: "Icelandic krónur",
}, {
Symbol: "J$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "JMD",
NamePlural: "Jamaican dollars",
}, {
Symbol: "JD",
SymbolNative: "د.أ.",
DecimalDigits: "3",
Rounding: "0",
Code: "JOD",
NamePlural: "Jordanian dinars",
}, {
Symbol: "¥",
SymbolNative: "¥",
DecimalDigits: "0",
Rounding: "0",
Code: "JPY",
NamePlural: "Japanese yen",
}, {
Symbol: "Ksh",
SymbolNative: "Ksh",
DecimalDigits: "2",
Rounding: "0",
Code: "KES",
NamePlural: "Kenyan shillings",
}, {
Symbol: "KHR",
SymbolNative: "៛",
DecimalDigits: "2",
Rounding: "0",
Code: "KHR",
NamePlural: "Cambodian riels",
}, {
Symbol: "CF",
SymbolNative: "FC",
DecimalDigits: "0",
Rounding: "0",
Code: "KMF",
NamePlural: "Comorian francs",
}, {
Symbol: "₩",
SymbolNative: "₩",
DecimalDigits: "0",
Rounding: "0",
Code: "KRW",
NamePlural: "South Korean won",
}, {
Symbol: "KD",
SymbolNative: "د.ك.",
DecimalDigits: "3",
Rounding: "0",
Code: "KWD",
NamePlural: "Kuwaiti dinars",
}, {
Symbol: "KZT",
SymbolNative: "тңг.",
DecimalDigits: "2",
Rounding: "0",
Code: "KZT",
NamePlural: "Kazakhstani tenges",
}, {
Symbol: "LB£",
SymbolNative: "ل.ل.",
DecimalDigits: "0",
Rounding: "0",
Code: "LBP",
NamePlural: "Lebanese pounds",
}, {
Symbol: "SLRs",
SymbolNative: "SL Re",
DecimalDigits: "2",
Rounding: "0",
Code: "LKR",
NamePlural: "Sri Lankan rupees",
}, {
Symbol: "Lt",
SymbolNative: "Lt",
DecimalDigits: "2",
Rounding: "0",
Code: "LTL",
NamePlural: "Lithuanian litai",
}, {
Symbol: "Ls",
SymbolNative: "Ls",
DecimalDigits: "2",
Rounding: "0",
Code: "LVL",
NamePlural: "Latvian lati",
}, {
Symbol: "LD",
SymbolNative: "د.ل.",
DecimalDigits: "3",
Rounding: "0",
Code: "LYD",
NamePlural: "Libyan dinars",
}, {
Symbol: "MAD",
SymbolNative: "د.م.",
DecimalDigits: "2",
Rounding: "0",
Code: "MAD",
NamePlural: "Moroccan dirhams",
}, {
Symbol: "MDL",
SymbolNative: "MDL",
DecimalDigits: "2",
Rounding: "0",
Code: "MDL",
NamePlural: "Moldovan lei",
}, {
Symbol: "MGA",
SymbolNative: "MGA",
DecimalDigits: "0",
Rounding: "0",
Code: "MGA",
NamePlural: "Malagasy Ariaries",
}, {
Symbol: "MKD",
SymbolNative: "MKD",
DecimalDigits: "2",
Rounding: "0",
Code: "MKD",
NamePlural: "Macedonian denari",
}, {
Symbol: "MMK",
SymbolNative: "K",
DecimalDigits: "0",
Rounding: "0",
Code: "MMK",
NamePlural: "Myanma kyats",
}, {
Symbol: "MOP$",
SymbolNative: "MOP$",
DecimalDigits: "2",
Rounding: "0",
Code: "MOP",
NamePlural: "Macanese patacas",
}, {
Symbol: "MURs",
SymbolNative: "MURs",
DecimalDigits: "0",
Rounding: "0",
Code: "MUR",
NamePlural: "Mauritian rupees",
}, {
Symbol: "MX$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "MXN",
NamePlural: "Mexican pesos",
}, {
Symbol: "RM",
SymbolNative: "RM",
DecimalDigits: "2",
Rounding: "0",
Code: "MYR",
NamePlural: "Malaysian ringgits",
}, {
Symbol: "MTn",
SymbolNative: "MTn",
DecimalDigits: "2",
Rounding: "0",
Code: "MZN",
NamePlural: "Mozambican meticals",
}, {
Symbol: "N$",
SymbolNative: "N$",
DecimalDigits: "2",
Rounding: "0",
Code: "NAD",
NamePlural: "Namibian dollars",
}, {
Symbol: "₦",
SymbolNative: "₦",
DecimalDigits: "2",
Rounding: "0",
Code: "NGN",
NamePlural: "Nigerian nairas",
}, {
Symbol: "C$",
SymbolNative: "C$",
DecimalDigits: "2",
Rounding: "0",
Code: "NIO",
NamePlural: "Nicaraguan córdobas",
}, {
Symbol: "Nkr",
SymbolNative: "kr",
DecimalDigits: "2",
Rounding: "0",
Code: "NOK",
NamePlural: "Norwegian kroner",
}, {
Symbol: "NPRs",
SymbolNative: "नेरू",
DecimalDigits: "2",
Rounding: "0",
Code: "NPR",
NamePlural: "Nepalese rupees",
}, {
Symbol: "NZ$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "NZD",
NamePlural: "New Zealand dollars",
}, {
Symbol: "OMR",
SymbolNative: "ر.ع.",
DecimalDigits: "3",
Rounding: "0",
Code: "OMR",
NamePlural: "Omani rials",
}, {
Symbol: "B/.",
SymbolNative: "B/.",
DecimalDigits: "2",
Rounding: "0",
Code: "PAB",
NamePlural: "Panamanian balboas",
}, {
Symbol: "S/.",
SymbolNative: "S/.",
DecimalDigits: "2",
Rounding: "0",
Code: "PEN",
NamePlural: "Peruvian nuevos soles",
}, {
Symbol: "₱",
SymbolNative: "₱",
DecimalDigits: "2",
Rounding: "0",
Code: "PHP",
NamePlural: "Philippine pesos",
}, {
Symbol: "PKRs",
SymbolNative: "₨",
DecimalDigits: "0",
Rounding: "0",
Code: "PKR",
NamePlural: "Pakistani rupees",
}, {
Symbol: "zł",
SymbolNative: "zł",
DecimalDigits: "2",
Rounding: "0",
Code: "PLN",
NamePlural: "Polish zlotys",
}, {
Symbol: "₲",
SymbolNative: "₲",
DecimalDigits: "0",
Rounding: "0",
Code: "PYG",
NamePlural: "Paraguayan guaranis",
}, {
Symbol: "QR",
SymbolNative: "ر.ق.",
DecimalDigits: "2",
Rounding: "0",
Code: "QAR",
NamePlural: "Qatari rials",
}, {
Symbol: "RON",
SymbolNative: "RON",
DecimalDigits: "2",
Rounding: "0",
Code: "RON",
NamePlural: "Romanian lei",
}, {
Symbol: "din.",
SymbolNative: "дин.",
DecimalDigits: "0",
Rounding: "0",
Code: "RSD",
NamePlural: "Serbian dinars",
}, {
Symbol: "RUB",
SymbolNative: "₽.",
DecimalDigits: "2",
Rounding: "0",
Code: "RUB",
NamePlural: "Russian rubles",
}, {
Symbol: "RWF",
SymbolNative: "FR",
DecimalDigits: "0",
Rounding: "0",
Code: "RWF",
NamePlural: "Rwandan francs",
}, {
Symbol: "SR",
SymbolNative: "ر.س.",
DecimalDigits: "2",
Rounding: "0",
Code: "SAR",
NamePlural: "Saudi riyals",
}, {
Symbol: "SDG",
SymbolNative: "SDG",
DecimalDigits: "2",
Rounding: "0",
Code: "SDG",
NamePlural: "Sudanese pounds",
}, {
Symbol: "Skr",
SymbolNative: "kr",
DecimalDigits: "2",
Rounding: "0",
Code: "SEK",
NamePlural: "Swedish kronor",
}, {
Symbol: "S$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "SGD",
NamePlural: "Singapore dollars",
}, {
Symbol: "Ssh",
SymbolNative: "Ssh",
DecimalDigits: "0",
Rounding: "0",
Code: "SOS",
NamePlural: "Somali shillings",
}, {
Symbol: "SY£",
SymbolNative: "ل.س.",
DecimalDigits: "0",
Rounding: "0",
Code: "SYP",
NamePlural: "Syrian pounds",
}, {
Symbol: "฿",
SymbolNative: "฿",
DecimalDigits: "2",
Rounding: "0",
Code: "THB",
NamePlural: "Thai baht",
}, {
Symbol: "DT",
SymbolNative: "د.ت.",
DecimalDigits: "3",
Rounding: "0",
Code: "TND",
NamePlural: "Tunisian dinars",
}, {
Symbol: "T$",
SymbolNative: "T$",
DecimalDigits: "2",
Rounding: "0",
Code: "TOP",
NamePlural: "Tongan paʻanga",
}, {
Symbol: "TL",
SymbolNative: "TL",
DecimalDigits: "2",
Rounding: "0",
Code: "TRY",
NamePlural: "Turkish Lira",
}, {
Symbol: "TT$",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "TTD",
NamePlural: "Trinidad and Tobago dollars",
}, {
Symbol: "NT$",
SymbolNative: "NT$",
DecimalDigits: "2",
Rounding: "0",
Code: "TWD",
NamePlural: "New Taiwan dollars",
}, {
Symbol: "TSh",
SymbolNative: "TSh",
DecimalDigits: "0",
Rounding: "0",
Code: "TZS",
NamePlural: "Tanzanian shillings",
}, {
Symbol: "₴",
SymbolNative: "₴",
DecimalDigits: "2",
Rounding: "0",
Code: "UAH",
NamePlural: "Ukrainian hryvnias",
}, {
Symbol: "USh",
SymbolNative: "USh",
DecimalDigits: "0",
Rounding: "0",
Code: "UGX",
NamePlural: "Ugandan shillings",
}, {
Symbol: "$U",
SymbolNative: "$",
DecimalDigits: "2",
Rounding: "0",
Code: "UYU",
NamePlural: "Uruguayan pesos",
}, {
Symbol: "UZS",
SymbolNative: "UZS",
DecimalDigits: "0",
Rounding: "0",
Code: "UZS",
NamePlural: "Uzbekistan som",
}, {
Symbol: "Bs.F.",
SymbolNative: "Bs.F.",
DecimalDigits: "2",
Rounding: "0",
Code: "VEF",
NamePlural: "Venezuelan bolívars",
}, {
Symbol: "₫",
SymbolNative: "₫",
DecimalDigits: "0",
Rounding: "0",
Code: "VND",
NamePlural: "Vietnamese dong",
}, {
Symbol: "FCFA",
SymbolNative: "FCFA",
DecimalDigits: "0",
Rounding: "0",
Code: "XAF",
NamePlural: "CFA francs BEAC",
}, {
Symbol: "CFA",
SymbolNative: "CFA",
DecimalDigits: "0",
Rounding: "0",
Code: "XOF",
NamePlural: "CFA francs BCEAO",
}, {
Symbol: "YR",
SymbolNative: "ر.ي.",
DecimalDigits: "0",
Rounding: "0",
Code: "YER",
NamePlural: "Yemeni rials",
}, {
Symbol: "R",
SymbolNative: "R",
DecimalDigits: "2",
Rounding: "0",
Code: "ZAR",
NamePlural: "South African rand",
}, {
Symbol: "ZK",
SymbolNative: "ZK",
DecimalDigits: "0",
Rounding: "0",
Code: "ZMK",
NamePlural: "Zambian kwachas",
}, {
Symbol: "ZWL$",
SymbolNative: "ZWL$",
DecimalDigits: "0",
Rounding: "0",
Code: "ZWL",
NamePlural: "Zimbabwean Dollar",
},
}
}

11
server/models/errors.go Normal file
View File

@@ -0,0 +1,11 @@
package models
import "fmt"
type VehicleAlreadyExistsError struct {
Registration string
}
func (e *VehicleAlreadyExistsError) Error() string {
return fmt.Sprintf("Vehicle with this url already exists")
}

5
server/models/files.go Normal file
View File

@@ -0,0 +1,5 @@
package models
type CreateQuickEntryModel struct {
Comments string `json:"comments" form:"comments"`
}

12
server/models/misc.go Normal file
View File

@@ -0,0 +1,12 @@
package models
import "github.com/akhilrex/hammond/db"
type UpdateSettingModel struct {
Currency string `json:"currency" form:"currency" query:"currency"`
DistanceUnit *db.DistanceUnit `json:"distanceUnit" form:"distanceUnit" query:"distanceUnit" `
}
type ClarksonMigrationModel struct {
Url string `json:"url" form:"url" query:"url"`
}

150
server/models/vehicle.go Normal file
View File

@@ -0,0 +1,150 @@
package models
import (
"time"
"github.com/akhilrex/hammond/db"
_ "github.com/go-playground/validator/v10"
)
type SearchByIdQuery struct {
Id string `binding:"required" uri:"id" json:"id" form:"id"`
}
type SubItemQuery struct {
Id string `binding:"required" uri:"id" json:"id" form:"id"`
SubId string `binding:"required" uri:"subId" json:"subId" form:"subId"`
}
type CreateVehicleRequest struct {
Nickname string `form:"nickname" json:"nickname" binding:"required"`
Registration string `form:"registration" json:"registration" binding:"required"`
Make string `form:"make" json:"make" binding:"required"`
Model string `form:"model" json:"model" binding:"required"`
YearOfManufacture int `form:"yearOfManufacture" json:"yearOfManufacture"`
EngineSize float32 `form:"engineSize" json:"engineSize"`
FuelUnit *db.FuelUnit `form:"fuelUnit" json:"fuelUnit" binding:"required"`
FuelType *db.FuelType `form:"fuelType" json:"fuelType" binding:"required"`
}
type UpdateVehicleRequest struct {
CreateVehicleRequest
}
type UserVehicleSimpleModel struct {
ID string `json:"id"`
UserID string `json:"userId"`
VehicleID string `json:"vehicleId"`
IsOwner bool `json:"isOwner"`
Name string `json:"name"`
}
type CreateFillupRequest struct {
VehicleID string `form:"vehicleId" json:"vehicleId" binding:"required"`
FuelUnit *db.FuelUnit `form:"fuelUnit" json:"fuelUnit" binding:"required"`
FuelQuantity float32 `form:"fuelQuantity" json:"fuelQuantity" binding:"required"`
PerUnitPrice float32 `form:"perUnitPrice" json:"perUnitPrice" binding:"required"`
TotalAmount float32 `form:"totalAmount" json:"totalAmount" binding:"required"`
OdoReading int `form:"odoReading" json:"odoReading" binding:"required"`
IsTankFull *bool `form:"isTankFull" json:"isTankFull" binding:"required"`
HasMissedFillup *bool `form:"hasMissedFillup" json:"HasMissedFillup"`
Comments string `form:"comments" json:"comments" `
FillingStation string `form:"fillingStation" json:"fillingStation"`
UserID string `form:"userId" json:"userId" binding:"required"`
Date time.Time `form:"date" json:"date" binding:"required" time_format:"2006-01-02"`
}
type UpdateFillupRequest struct {
CreateFillupRequest
}
type UpdateExpenseRequest struct {
CreateExpenseRequest
}
type CreateExpenseRequest struct {
VehicleID string `form:"vehicleId" json:"vehicleId" binding:"required"`
Amount float32 `form:"amount" json:"amount" binding:"required"`
OdoReading int `form:"odoReading" json:"odoReading"`
Comments string `form:"comments" json:"comments" `
ExpenseType string `form:"expenseType" json:"expenseType"`
UserID string `form:"userId" json:"userId" binding:"required"`
Date time.Time `form:"date" json:"date" binding:"required" time_format:"2006-01-02"`
}
type CreateVehicleAttachmentModel struct {
Title string `form:"title" json:"title" binding:"required"`
}
type VehicleStatsModel struct {
CountFillups int `json:"countFillups"`
CountExpenses int `json:"countExpenses"`
ExpenditureFillups float32 `json:"expenditureFillups"`
ExpenditureExpenses float32 `json:"expenditureExpenses"`
ExpenditureTotal float32 `json:"expenditureTotal"`
AvgFillupCost float32 `json:"avgFillupCost"`
AvgExpenseCost float32 `json:"avgExpenseCost"`
AvgFuelQty float32 `json:"avgFuelQty"`
AvgFuelPrice float32 `json:"avgFuelPrice"`
Currency string `json:"currency"`
}
func (m *VehicleStatsModel) SetStats(fillups *[]db.Fillup, expenses *[]db.Expense) []VehicleStatsModel {
currencyMap := make(map[string]int)
for _, v := range *fillups {
currencyMap[v.Currency] = 1
}
for _, v := range *expenses {
currencyMap[v.Currency] = 1
}
var toReturn []VehicleStatsModel
for currency, _ := range currencyMap {
model := VehicleStatsModel{}
var totalExpenditure, fillupTotal, expenseTotal, totalFuel, averageFuelCost, averageFuelQty, averageFillup, averageExpense float32
var countFillup, countExpense int
for _, v := range *fillups {
if v.Currency == currency {
fillupTotal = fillupTotal + v.TotalAmount
totalFuel = totalFuel + v.FuelQuantity
countFillup++
}
}
for _, v := range *expenses {
if v.Currency == currency {
expenseTotal = expenseTotal + v.Amount
countExpense++
}
}
totalExpenditure = expenseTotal + fillupTotal
if countFillup > 0 {
averageFillup = fillupTotal / float32(countFillup)
averageFuelCost = fillupTotal / totalFuel
averageFuelQty = totalFuel / float32(countFillup)
}
if countExpense > 0 {
averageExpense = expenseTotal / float32(countExpense)
}
model.CountFillups = countFillup
model.CountExpenses = countExpense
model.ExpenditureFillups = fillupTotal
model.ExpenditureExpenses = expenseTotal
model.ExpenditureTotal = totalExpenditure
model.AvgFillupCost = averageFillup
model.AvgExpenseCost = averageExpense
model.AvgFuelPrice = averageFuelCost
model.AvgFuelQty = averageFuelQty
model.Currency = currency
toReturn = append(toReturn, model)
}
return toReturn
}
type UserStatsQueryModel struct {
Start time.Time `json:"start" query:"start" form:"start"`
End time.Time `json:"end" query:"end" form:"end"`
}

View File

@@ -0,0 +1,236 @@
package service
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"time"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/internal/sanitize"
"github.com/akhilrex/hammond/models"
uuid "github.com/satori/go.uuid"
)
func CreateAttachment(path, originalName string, size int64, contentType, userId string) (*db.Attachment, error) {
model := &db.Attachment{
Path: path,
OriginalName: originalName,
Size: size,
ContentType: contentType,
UserID: userId,
}
tx := db.DB.Create(&model)
if tx.Error != nil {
return nil, tx.Error
}
return model, nil
}
func CreateQuickEntry(model models.CreateQuickEntryModel, attachmentId, userId string) (*db.QuickEntry, error) {
toCreate := &db.QuickEntry{
AttachmentID: attachmentId,
UserID: userId,
Comments: model.Comments,
}
tx := db.DB.Create(&toCreate)
if tx.Error != nil {
return nil, tx.Error
}
return toCreate, nil
}
func GetAllQuickEntries(sorting string) (*[]db.QuickEntry, error) {
return db.GetAllQuickEntries(sorting)
}
func GetQuickEntriesForUser(userId, sorting string) (*[]db.QuickEntry, error) {
return db.GetQuickEntriesForUser(userId, sorting)
}
func GetQuickEntryById(id string) (*db.QuickEntry, error) {
return db.GetQuickEntryById(id)
}
func SetQuickEntryAsProcessed(id string) error {
return db.SetQuickEntryAsProcessed(id, time.Now())
}
func GetAttachmentById(id string) (*db.Attachment, error) {
return db.GetAttachmentById(id)
}
func GetAllBackupFiles() ([]string, error) {
var files []string
folder := createConfigFolderIfNotExists("backups")
err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
files = append(files, path)
}
return nil
})
sort.Sort(sort.Reverse(sort.StringSlice(files)))
return files, err
}
func GetFileSize(path string) (int64, error) {
info, err := os.Stat(path)
if err != nil {
return 0, err
}
return info.Size(), nil
}
func deleteOldBackup() {
files, err := GetAllBackupFiles()
if err != nil {
return
}
if len(files) <= 5 {
return
}
toDelete := files[5:]
for _, file := range toDelete {
fmt.Println(file)
DeleteFile(file)
}
}
func GetFilePath(originalName string) string {
dataPath := os.Getenv("DATA")
return path.Join(dataPath, getFileName(originalName))
}
func getFileName(orig string) string {
ext := filepath.Ext(orig)
return uuid.NewV4().String() + ext
}
func CreateBackup() (string, error) {
backupFileName := "hammond_backup_" + time.Now().Format("2006.01.02_150405") + ".tar.gz"
folder := createConfigFolderIfNotExists("backups")
configPath := os.Getenv("CONFIG")
tarballFilePath := path.Join(folder, backupFileName)
file, err := os.Create(tarballFilePath)
if err != nil {
return "", errors.New(fmt.Sprintf("Could not create tarball file '%s', got error '%s'", tarballFilePath, err.Error()))
}
defer file.Close()
dbPath := path.Join(configPath, "hammond.db")
_, err = os.Stat(dbPath)
if err != nil {
return "", errors.New(fmt.Sprintf("Could not find db file '%s', got error '%s'", dbPath, err.Error()))
}
gzipWriter := gzip.NewWriter(file)
defer gzipWriter.Close()
tarWriter := tar.NewWriter(gzipWriter)
defer tarWriter.Close()
err = addFileToTarWriter(dbPath, tarWriter)
if err == nil {
deleteOldBackup()
}
return backupFileName, err
}
func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
file, err := os.Open(filePath)
if err != nil {
return errors.New(fmt.Sprintf("Could not open file '%s', got error '%s'", filePath, err.Error()))
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return errors.New(fmt.Sprintf("Could not get stat for file '%s', got error '%s'", filePath, err.Error()))
}
header := &tar.Header{
Name: filePath,
Size: stat.Size(),
Mode: int64(stat.Mode()),
ModTime: stat.ModTime(),
}
err = tarWriter.WriteHeader(header)
if err != nil {
return errors.New(fmt.Sprintf("Could not write header for file '%s', got error '%s'", filePath, err.Error()))
}
_, err = io.Copy(tarWriter, file)
if err != nil {
return errors.New(fmt.Sprintf("Could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error()))
}
return nil
}
func httpClient() *http.Client {
client := http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
// r.URL.Opaque = r.URL.Path
return nil
},
}
return &client
}
func createFolder(folder string, parent string) string {
folder = cleanFileName(folder)
//str := stringy.New(folder)
folderPath := path.Join(parent, folder)
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
os.MkdirAll(folderPath, 0777)
changeOwnership(folderPath)
}
return folderPath
}
func createDataFolderIfNotExists(folder string) string {
dataPath := os.Getenv("DATA")
return createFolder(folder, dataPath)
}
func createConfigFolderIfNotExists(folder string) string {
dataPath := os.Getenv("CONFIG")
return createFolder(folder, dataPath)
}
func cleanFileName(original string) string {
return sanitize.Name(original)
}
func changeOwnership(path string) {
uid, err1 := strconv.Atoi(os.Getenv("PUID"))
gid, err2 := strconv.Atoi(os.Getenv("PGID"))
fmt.Println(path)
if err1 == nil && err2 == nil {
fmt.Println(path + " : Attempting change")
os.Chown(path, uid, gid)
}
}
func DeleteFile(filePath string) error {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return err
}
if err := os.Remove(filePath); err != nil {
return err
}
return nil
}
func checkError(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,30 @@
package service
import (
"github.com/akhilrex/hammond/db"
)
func CanInitializeSystem() (bool, error) {
return db.CanInitializeSystem()
}
func UpdateSettings(currency string, distanceUnit db.DistanceUnit) error {
setting := db.GetOrCreateSetting()
setting.Currency = currency
setting.DistanceUnit = distanceUnit
return db.UpdateSettings(setting)
}
func UpdateUserSettings(userId, currency string, distanceUnit db.DistanceUnit) error {
user, err := db.GetUserById(userId)
if err != nil {
return err
}
user.Currency = currency
user.DistanceUnit = distanceUnit
return db.UpdateUser(user)
}
func GetSettings() *db.Setting {
return db.GetOrCreateSetting()
}

View File

@@ -0,0 +1,47 @@
package service
import (
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
)
func CreateUser(userModel *models.RegisterRequest, role db.Role) error {
setting := db.GetOrCreateSetting()
toCreate := db.User{
Email: userModel.Email,
Name: userModel.Name,
Role: role,
Currency: setting.Currency,
DistanceUnit: setting.DistanceUnit,
}
toCreate.SetPassword(userModel.Password)
return db.CreateUser(&toCreate)
}
func GetUserById(id string) (*db.User, error) {
var myUserModel db.User
tx := db.DB.Debug().Preload("Vehicles").First(&myUserModel, map[string]string{
"ID": id,
})
return &myUserModel, tx.Error
}
func GetAllUsers() (*[]db.User, error) {
return db.GetAllUsers()
}
func UpdatePassword(id, password string) (bool, error) {
user, err := GetUserById(id)
if err != nil {
return false, err
}
user.SetPassword(password)
err = db.UpdateUser(user)
if err != nil {
return false, err
}
return true, nil
}

View File

@@ -0,0 +1,234 @@
package service
import (
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
"gorm.io/gorm/clause"
)
func CreateVehicle(model models.CreateVehicleRequest, userId string) (*db.Vehicle, error) {
vehicle := db.Vehicle{
Nickname: model.Nickname,
Registration: model.Registration,
Model: model.Model,
Make: model.Make,
YearOfManufacture: model.YearOfManufacture,
EngineSize: model.EngineSize,
FuelUnit: *model.FuelUnit,
FuelType: *model.FuelType,
}
tx := db.DB.Create(&vehicle)
if tx.Error != nil {
return nil, tx.Error
}
association := db.UserVehicle{
UserID: userId,
VehicleID: vehicle.ID,
IsOwner: true,
}
tx = db.DB.Create(&association)
if tx.Error != nil {
return nil, tx.Error
}
return &vehicle, nil
}
func GetVehicleOwner(vehicleId string) (string, error) {
return db.GetVehicleOwner(vehicleId)
}
func GetVehicleUsers(vehicleId string) (*[]db.UserVehicle, error) {
return db.GetVehicleUsers(vehicleId)
}
func ShareVehicle(vehicleId, userId string) error {
return db.ShareVehicle(vehicleId, userId)
}
func UnshareVehicle(vehicleId, userId string) error {
return db.UnshareVehicle(vehicleId, userId)
}
func GetVehicleById(vehicleID string) (*db.Vehicle, error) {
return db.GetVehicleById(vehicleID)
}
func GetFillupsByVehicleId(vehicleId string) (*[]db.Fillup, error) {
return db.GetFillupsByVehicleId(vehicleId)
}
func GetExpensesByVehicleId(vehicleId string) (*[]db.Expense, error) {
return db.GetExpensesByVehicleId(vehicleId)
}
func GetFillupById(fillupId string) (*db.Fillup, error) {
return db.GetFillupById(fillupId)
}
func GetExpenseById(expenseId string) (*db.Expense, error) {
return db.GetExpenseById(expenseId)
}
func UpdateVehicle(vehicleID string, model models.UpdateVehicleRequest) error {
toUpdate, err := GetVehicleById(vehicleID)
if err != nil {
return err
}
//return db.DB.Model(&toUpdate).Updates(db.Vehicle{
toUpdate.Nickname = model.Nickname
toUpdate.Registration = model.Registration
toUpdate.Model = model.Model
toUpdate.Make = model.Make
toUpdate.YearOfManufacture = model.YearOfManufacture
toUpdate.EngineSize = model.EngineSize
toUpdate.FuelUnit = *model.FuelUnit
toUpdate.FuelType = *model.FuelType
//}).Error
return db.DB.Omit(clause.Associations).Save(toUpdate).Error
}
func GetAllVehicles() (*[]db.Vehicle, error) {
return db.GetAllVehicles("")
}
func GetUserVehicles(id string) (*[]db.Vehicle, error) {
return db.GetUserVehicles(id)
}
func CreateFillup(model models.CreateFillupRequest) (*db.Fillup, error) {
user, err := db.GetUserById(model.UserID)
if err != nil {
return nil, err
}
fillup := db.Fillup{
VehicleID: model.VehicleID,
FuelUnit: *model.FuelUnit,
FuelQuantity: model.FuelQuantity,
PerUnitPrice: model.PerUnitPrice,
TotalAmount: model.TotalAmount,
OdoReading: model.OdoReading,
IsTankFull: model.IsTankFull,
HasMissedFillup: model.HasMissedFillup,
Comments: model.Comments,
FillingStation: model.FillingStation,
UserID: model.UserID,
Date: model.Date,
Currency: user.Currency,
DistanceUnit: user.DistanceUnit,
}
tx := db.DB.Create(&fillup)
if tx.Error != nil {
return nil, tx.Error
}
return &fillup, nil
}
func CreateExpense(model models.CreateExpenseRequest) (*db.Expense, error) {
user, err := db.GetUserById(model.UserID)
if err != nil {
return nil, err
}
expense := db.Expense{
VehicleID: model.VehicleID,
Amount: model.Amount,
OdoReading: model.OdoReading,
ExpenseType: model.ExpenseType,
Comments: model.Comments,
UserID: model.UserID,
Date: model.Date,
Currency: user.Currency,
DistanceUnit: user.DistanceUnit,
}
tx := db.DB.Create(&expense)
if tx.Error != nil {
return nil, tx.Error
}
return &expense, nil
}
func UpdateFillup(fillupId string, model models.UpdateFillupRequest) error {
toUpdate, err := GetFillupById(fillupId)
if err != nil {
return err
}
return db.DB.Model(&toUpdate).Updates(db.Fillup{
VehicleID: model.VehicleID,
FuelUnit: *model.FuelUnit,
FuelQuantity: model.FuelQuantity,
PerUnitPrice: model.PerUnitPrice,
TotalAmount: model.TotalAmount,
OdoReading: model.OdoReading,
IsTankFull: model.IsTankFull,
HasMissedFillup: model.HasMissedFillup,
Comments: model.Comments,
FillingStation: model.FillingStation,
UserID: model.UserID,
Date: model.Date,
}).Error
}
func UpdateExpense(fillupId string, model models.UpdateExpenseRequest) error {
toUpdate, err := GetExpenseById(fillupId)
if err != nil {
return err
}
return db.DB.Model(&toUpdate).Updates(db.Expense{
VehicleID: model.VehicleID,
Amount: model.Amount,
OdoReading: model.OdoReading,
ExpenseType: model.ExpenseType,
Comments: model.Comments,
UserID: model.UserID,
Date: model.Date,
}).Error
}
func DeleteFillupById(fillupId string) error {
return db.DeleteFillupById(fillupId)
}
func DeleteExpenseById(expenseId string) error {
return db.DeleteExpenseById(expenseId)
}
func CreateVehicleAttachment(vehicleId, attachmentId, title string) error {
model := &db.VehicleAttachment{
AttachmentID: attachmentId,
VehicleID: vehicleId,
Title: title,
}
return db.DB.Create(model).Error
}
func GetVehicleAttachments(vehicleId string) (*[]db.Attachment, error) {
return db.GetVehicleAttachments(vehicleId)
}
func GetUserStats(userId string, model models.UserStatsQueryModel) ([]models.VehicleStatsModel, error) {
vehicles, err := GetUserVehicles(userId)
if err != nil {
return nil, err
}
var vehicleIds []string
for _, v := range *vehicles {
vehicleIds = append(vehicleIds, v.ID)
}
expenses, err := db.FindExpensesForDateRange(vehicleIds, model.Start, model.End)
if err != nil {
return nil, err
}
fillups, err := db.FindFillupsForDateRange(vehicleIds, model.Start, model.End)
if err != nil {
return nil, err
}
toReturn := models.VehicleStatsModel{}
stats := toReturn.SetStats(fillups, expenses)
return stats, nil
}