first commit
This commit is contained in:
3
server/.env
Normal file
3
server/.env
Normal 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
21
server/.gitignore
vendored
Normal 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
41
server/Dockerfile
Normal 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
89
server/common/utils.go
Normal 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
179
server/controllers/auth.go
Normal 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(®isterRequest); err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.CreateUser(®isterRequest, *registerRequest.Role); err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"success": true})
|
||||
}
|
||||
func initializeSystem(c *gin.Context) {
|
||||
|
||||
canInitialize, err := service.CanInitializeSystem()
|
||||
if !canInitialize {
|
||||
c.JSON(http.StatusUnprocessableEntity, err)
|
||||
}
|
||||
|
||||
var registerRequest models.RegisterRequest
|
||||
if err := c.ShouldBind(®isterRequest); err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.CreateUser(®isterRequest, db.ADMIN); err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewError("initializeSystem", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.UpdateSettings(registerRequest.Currency, *registerRequest.DistanceUnit)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"success": true})
|
||||
}
|
||||
|
||||
func userLogin(c *gin.Context) {
|
||||
var loginRequest models.LoginRequest
|
||||
if err := c.ShouldBind(&loginRequest); err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
|
||||
return
|
||||
}
|
||||
user, err := db.FindOneUser(&db.User{Email: loginRequest.Email})
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
|
||||
return
|
||||
}
|
||||
|
||||
if user.CheckPassword(loginRequest.Password) != nil {
|
||||
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
|
||||
return
|
||||
}
|
||||
UpdateContextUserModel(c, user.ID)
|
||||
token, refreshToken := common.GenToken(user.ID, user.Role)
|
||||
response := models.LoginResponse{
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
RefreshToken: refreshToken,
|
||||
Role: user.RoleDetail().Long,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func refresh(c *gin.Context) {
|
||||
type tokenReqBody struct {
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
tokenReq := tokenReqBody{}
|
||||
c.Bind(&tokenReq)
|
||||
|
||||
token, _ := jwt.Parse(tokenReq.RefreshToken, func(token *jwt.Token) (interface{}, error) {
|
||||
// Don't forget to validate the alg is what you expect:
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
|
||||
return []byte(os.Getenv("JWT_SECRET")), nil
|
||||
})
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
// Get the user record from database or
|
||||
// run through your business logic to verify if the user can log in
|
||||
user, err := service.GetUserById(claims["id"].(string))
|
||||
if err == nil {
|
||||
|
||||
token, refreshToken := common.GenToken(user.ID, user.Role)
|
||||
|
||||
response := models.LoginResponse{
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
RefreshToken: refreshToken,
|
||||
Role: user.RoleDetail().Long,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
} else {
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{})
|
||||
}
|
||||
} else {
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{})
|
||||
}
|
||||
}
|
||||
|
||||
func changePassword(c *gin.Context) {
|
||||
var request models.ChangePasswordRequest
|
||||
if err := c.ShouldBind(&request); err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
|
||||
return
|
||||
}
|
||||
user, err := service.GetUserById(c.GetString("userId"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Not Registered email or invalid password")))
|
||||
return
|
||||
}
|
||||
|
||||
if user.CheckPassword(request.OldPassword) != nil {
|
||||
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Not Registered email or invalid password")))
|
||||
return
|
||||
}
|
||||
|
||||
user.SetPassword(request.NewPassword)
|
||||
success, err := service.UpdatePassword(user.ID, request.NewPassword)
|
||||
c.JSON(http.StatusOK, success)
|
||||
}
|
||||
157
server/controllers/files.go
Normal file
157
server/controllers/files.go
Normal 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
|
||||
}
|
||||
64
server/controllers/masters.go
Normal file
64
server/controllers/masters.go
Normal 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))
|
||||
}
|
||||
|
||||
}
|
||||
71
server/controllers/middlewares.go
Normal file
71
server/controllers/middlewares.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
70
server/controllers/setup.go
Normal file
70
server/controllers/setup.go
Normal 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))
|
||||
}
|
||||
}
|
||||
22
server/controllers/users.go
Normal file
22
server/controllers/users.go
Normal 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)
|
||||
|
||||
}
|
||||
436
server/controllers/vehicle.go
Normal file
436
server/controllers/vehicle.go
Normal 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
22
server/db/base.go
Normal 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
223
server/db/clarkson.go
Normal 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
58
server/db/db.go
Normal 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
192
server/db/dbModels.go
Normal 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
304
server/db/dbfunctions.go
Normal 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
111
server/db/enums.go
Normal 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
43
server/db/migrations.go
Normal 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
20
server/go.mod
Normal 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
110
server/go.sum
Normal 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
22
server/internal/sanitize/.gitignore
vendored
Normal 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
|
||||
27
server/internal/sanitize/LICENSE
Normal file
27
server/internal/sanitize/LICENSE
Normal 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.
|
||||
62
server/internal/sanitize/README.md
Normal file
62
server/internal/sanitize/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
sanitize [](https://godoc.org/github.com/kennygrant/sanitize) [](https://goreportcard.com/report/github.com/kennygrant/sanitize) [](https://circleci.com/gh/kennygrant/sanitize)
|
||||
========
|
||||
|
||||
Package sanitize provides functions to sanitize html and paths with go (golang).
|
||||
|
||||
FUNCTIONS
|
||||
|
||||
|
||||
```go
|
||||
sanitize.Accents(s string) string
|
||||
```
|
||||
|
||||
Accents replaces a set of accented characters with ascii equivalents.
|
||||
|
||||
```go
|
||||
sanitize.BaseName(s string) string
|
||||
```
|
||||
|
||||
BaseName makes a string safe to use in a file name, producing a sanitized basename replacing . or / with -. Unlike Name no attempt is made to normalise text as a path.
|
||||
|
||||
```go
|
||||
sanitize.HTML(s string) string
|
||||
```
|
||||
|
||||
HTML strips html tags with a very simple parser, replace common entities, and escape < and > in the result. The result is intended to be used as plain text.
|
||||
|
||||
```go
|
||||
sanitize.HTMLAllowing(s string, args...[]string) (string, error)
|
||||
```
|
||||
|
||||
HTMLAllowing parses html and allow certain tags and attributes from the lists optionally specified by args - args[0] is a list of allowed tags, args[1] is a list of allowed attributes. If either is missing default sets are used.
|
||||
|
||||
```go
|
||||
sanitize.Name(s string) string
|
||||
```
|
||||
|
||||
Name makes a string safe to use in a file name by first finding the path basename, then replacing non-ascii characters.
|
||||
|
||||
```go
|
||||
sanitize.Path(s string) string
|
||||
```
|
||||
|
||||
Path makes a string safe to use as an url path.
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
Version 1.2
|
||||
|
||||
Adjusted HTML function to avoid linter warning
|
||||
Added more tests from https://githubengineering.com/githubs-post-csp-journey/
|
||||
Chnaged name of license file
|
||||
Added badges and change log to readme
|
||||
|
||||
Version 1.1
|
||||
Fixed type in comments.
|
||||
Merge pull request from Povilas Balzaravicius Pawka
|
||||
- replace br tags with newline even when they contain a space
|
||||
|
||||
Version 1.0
|
||||
First release
|
||||
388
server/internal/sanitize/sanitize.go
Normal file
388
server/internal/sanitize/sanitize.go
Normal 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, "‘", "'", -1)
|
||||
output = strings.Replace(output, "’", "'", -1)
|
||||
output = strings.Replace(output, "“", "\"", -1)
|
||||
output = strings.Replace(output, "”", "\"", -1)
|
||||
output = strings.Replace(output, " ", " ", -1)
|
||||
output = strings.Replace(output, """, "\"", -1)
|
||||
output = strings.Replace(output, "'", "'", -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, """, "\"", -1)
|
||||
output = strings.Replace(output, "'", "'", -1)
|
||||
output = strings.Replace(output, "& ", "& ", -1) // NB space after
|
||||
output = strings.Replace(output, "&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
80
server/main.go
Normal 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
30
server/models/auth.go
Normal 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
849
server/models/currency.go
Normal 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
11
server/models/errors.go
Normal 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
5
server/models/files.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
type CreateQuickEntryModel struct {
|
||||
Comments string `json:"comments" form:"comments"`
|
||||
}
|
||||
12
server/models/misc.go
Normal file
12
server/models/misc.go
Normal 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
150
server/models/vehicle.go
Normal 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"`
|
||||
}
|
||||
236
server/service/fileService.go
Normal file
236
server/service/fileService.go
Normal 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)
|
||||
}
|
||||
}
|
||||
30
server/service/miscService.go
Normal file
30
server/service/miscService.go
Normal 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()
|
||||
}
|
||||
47
server/service/userService.go
Normal file
47
server/service/userService.go
Normal 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
|
||||
}
|
||||
234
server/service/vehicleService.go
Normal file
234
server/service/vehicleService.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user