Compare commits

..

1 Commits

Author SHA1 Message Date
Alf Sebastian Houge
fb742f19a7 Change currency field from select to autocomplete 2022-04-17 18:52:19 +02:00
13 changed files with 87 additions and 78 deletions

View File

@@ -4,7 +4,7 @@ services:
image: akhilrex/hammond image: akhilrex/hammond
container_name: hammond container_name: hammond
environment: environment:
- JWT_SECRET=somethingverystrong - JWT_SECRET = somethingverystrong
volumes: volumes:
- /path/to/config:/config - /path/to/config:/config
- /path/to/data:/assets - /path/to/data:/assets

View File

@@ -94,17 +94,17 @@ func userLogin(c *gin.Context) {
user, err := db.FindOneUser(&db.User{Email: loginRequest.Email}) user, err := db.FindOneUser(&db.User{Email: loginRequest.Email})
if err != nil { if err != nil {
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("not Registered email or invalid password"))) c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
return return
} }
if user.CheckPassword(loginRequest.Password) != nil { if user.CheckPassword(loginRequest.Password) != nil {
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("not Registered email or invalid password"))) c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
return return
} }
if user.IsDisabled { if user.IsDisabled {
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("your user has been disabled by the admin. Please contact them to get it re-enabled"))) c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Your user has been disabled by the admin. Please contact them to get it re-enabled.")))
return return
} }
UpdateContextUserModel(c, user.ID) UpdateContextUserModel(c, user.ID)
@@ -170,16 +170,16 @@ func changePassword(c *gin.Context) {
user, err := service.GetUserById(c.GetString("userId")) user, err := service.GetUserById(c.GetString("userId"))
if err != nil { if err != nil {
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("not Registered email or invalid password"))) c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Not Registered email or invalid password")))
return return
} }
if user.CheckPassword(request.OldPassword) != nil { if user.CheckPassword(request.OldPassword) != nil {
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("incorrect old password"))) c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Incorrect old password")))
return return
} }
user.SetPassword(request.NewPassword) user.SetPassword(request.NewPassword)
success, _ := service.UpdatePassword(user.ID, request.NewPassword) success, err := service.UpdatePassword(user.ID, request.NewPassword)
c.JSON(http.StatusOK, success) c.JSON(http.StatusOK, success)
} }

View File

@@ -23,8 +23,8 @@ func stripBearerPrefixFromTokenString(tok string) (string, error) {
// Extract token from Authorization header // Extract token from Authorization header
// Uses PostExtractionFilter to strip "TOKEN " prefix from header // Uses PostExtractionFilter to strip "TOKEN " prefix from header
var AuthorizationHeaderExtractor = &request.PostExtractionFilter{ var AuthorizationHeaderExtractor = &request.PostExtractionFilter{
Extractor: request.HeaderExtractor{"Authorization"}, request.HeaderExtractor{"Authorization"},
Filter: stripBearerPrefixFromTokenString, stripBearerPrefixFromTokenString,
} }
// Extractor for OAuth2 access tokens. Looks in 'Authorization' // Extractor for OAuth2 access tokens. Looks in 'Authorization'

View File

@@ -51,7 +51,7 @@ func migrate(c *gin.Context) {
canMigrate, _, _ := db.CanMigrate(request.Url) canMigrate, _, _ := db.CanMigrate(request.Url)
if !canMigrate { if !canMigrate {
c.JSON(http.StatusBadRequest, fmt.Errorf("cannot migrate database. please check connection string")) c.JSON(http.StatusBadRequest, fmt.Errorf("cannot migrate database. please check connection string."))
return return
} }

View File

@@ -397,7 +397,7 @@ func deleteVehicle(c *gin.Context) {
return return
} }
if !canDelete { if !canDelete {
c.JSON(http.StatusUnprocessableEntity, common.NewError("shareVehicle", errors.New("you are not allowed to delete this vehicle"))) c.JSON(http.StatusUnprocessableEntity, common.NewError("shareVehicle", errors.New("You are not allowed to delete this vehicle.")))
return return
} }
err = service.DeleteVehicle(searchByIdQuery.Id) err = service.DeleteVehicle(searchByIdQuery.Id)

View File

@@ -117,7 +117,7 @@ func UnshareVehicle(vehicleId, userId string) error {
return nil return nil
} }
if mapping.IsOwner { if mapping.IsOwner {
return fmt.Errorf("cannot unshare owner") return fmt.Errorf("Cannot unshare owner")
} }
result := DB.Where("id=?", mapping.ID).Delete(&UserVehicle{}) result := DB.Where("id=?", mapping.ID).Delete(&UserVehicle{})
return result.Error return result.Error
@@ -332,7 +332,8 @@ func UnlockMissedJobs() {
if (job.Date == time.Time{}) { if (job.Date == time.Time{}) {
continue continue
} }
var duration = time.Duration(job.Duration) var duration time.Duration
duration = time.Duration(job.Duration)
d := job.Date.Add(time.Minute * duration) d := job.Date.Add(time.Minute * duration)
if d.Before(time.Now()) { if d.Before(time.Now()) {
fmt.Println(job.Name + " is unlocked") fmt.Println(job.Name + " is unlocked")

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -125,14 +126,14 @@ func CreateBackup() (string, error) {
tarballFilePath := path.Join(folder, backupFileName) tarballFilePath := path.Join(folder, backupFileName)
file, err := os.Create(tarballFilePath) file, err := os.Create(tarballFilePath)
if err != nil { if err != nil {
return "", fmt.Errorf("could not create tarball file '%s', got error '%s'", tarballFilePath, err.Error()) return "", errors.New(fmt.Sprintf("Could not create tarball file '%s', got error '%s'", tarballFilePath, err.Error()))
} }
defer file.Close() defer file.Close()
dbPath := path.Join(configPath, "hammond.db") dbPath := path.Join(configPath, "hammond.db")
_, err = os.Stat(dbPath) _, err = os.Stat(dbPath)
if err != nil { if err != nil {
return "", fmt.Errorf("could not find db file '%s', got error '%s'", dbPath, err.Error()) return "", errors.New(fmt.Sprintf("Could not find db file '%s', got error '%s'", dbPath, err.Error()))
} }
gzipWriter := gzip.NewWriter(file) gzipWriter := gzip.NewWriter(file)
defer gzipWriter.Close() defer gzipWriter.Close()
@@ -150,13 +151,13 @@ func CreateBackup() (string, error) {
func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error { func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
return fmt.Errorf("could not open file '%s', got error '%s'", filePath, err.Error()) return errors.New(fmt.Sprintf("Could not open file '%s', got error '%s'", filePath, err.Error()))
} }
defer file.Close() defer file.Close()
stat, err := file.Stat() stat, err := file.Stat()
if err != nil { if err != nil {
return fmt.Errorf("could not get stat for file '%s', got error '%s'", filePath, err.Error()) return errors.New(fmt.Sprintf("Could not get stat for file '%s', got error '%s'", filePath, err.Error()))
} }
header := &tar.Header{ header := &tar.Header{
@@ -168,12 +169,12 @@ func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
err = tarWriter.WriteHeader(header) err = tarWriter.WriteHeader(header)
if err != nil { if err != nil {
return fmt.Errorf("could not write header for file '%s', got error '%s'", filePath, err.Error()) return errors.New(fmt.Sprintf("Could not write header for file '%s', got error '%s'", filePath, err.Error()))
} }
_, err = io.Copy(tarWriter, file) _, err = io.Copy(tarWriter, file)
if err != nil { if err != nil {
return fmt.Errorf("could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error()) return errors.New(fmt.Sprintf("Could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error()))
} }
return nil return nil

View File

@@ -1,7 +1,6 @@
package service package service
import ( import (
"sort"
"time" "time"
"github.com/akhilrex/hammond/db" "github.com/akhilrex/hammond/db"
@@ -16,9 +15,6 @@ func GetMileageByVehicleId(vehicleId string, since time.Time) (mileage []models.
fillups := make([]db.Fillup, len(*data)) fillups := make([]db.Fillup, len(*data))
copy(fillups, *data) copy(fillups, *data)
sort.Slice(fillups, func(i, j int) bool {
return fillups[i].OdoReading > fillups[j].OdoReading
})
var mileages []models.MileageModel var mileages []models.MileageModel

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -5,7 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="shortcut icon" href="<%= webpackConfig.output.publicPath %>hammond.png" /> <link rel="shortcut icon" href="<%= webpackConfig.output.publicPath %>hammond.png" />
<link rel="apple-touch-icon" href="<%= webpackConfig.output.publicPath %>touch-icon.png" />
<title><%= webpackConfig.name %></title> <title><%= webpackConfig.name %></title>
</head> </head>
<body> <body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -21,13 +21,27 @@ export default {
email: '', email: '',
password: '', password: '',
distanceUnit: 1, distanceUnit: 1,
currency: 'INR', currency: '',
}, },
} }
}, },
computed: { computed: {
...mapGetters('auth', ['isInitialized']), ...mapGetters('auth', ['isInitialized']),
...mapState('vehicles', ['currencyMasters', 'distanceUnitMasters']), ...mapState('vehicles', ['currencyMasters', 'distanceUnitMasters']),
filteredCurrencyMasters() {
return this.currencyMasters.filter((option) => {
return (
option.namePlural
.toString()
.toLowerCase()
.indexOf(this.registerModel.currency.toLowerCase()) >= 0 ||
option.code
.toString()
.toLowerCase()
.indexOf(this.registerModel.currency.toLowerCase()) >= 0
)
})
},
}, },
mounted() { mounted() {
store.dispatch('vehicles/fetchMasters').then((data) => {}) store.dispatch('vehicles/fetchMasters').then((data) => {})
@@ -139,6 +153,9 @@ export default {
}) })
.finally(() => (this.isWorking = false)) .finally(() => (this.isWorking = false))
}, },
formatCurrency(option) {
return `${option.namePlural} (${option.code})`
},
}, },
} }
</script> </script>
@@ -148,15 +165,10 @@ export default {
<div v-if="!migrationMode" class="box"> <div v-if="!migrationMode" class="box">
<h1 class="title">Migrate from Clarkson</h1> <h1 class="title">Migrate from Clarkson</h1>
<p> <p>
If you have an existing Clarkson deployment and you want to migrate your data from that, If you have an existing Clarkson deployment and you want to migrate your data from that, press the following button.
press the following button.
</p> </p>
<br /> <br />
<b-field> <b-field> <b-button type="is-primary" @click="migrationMode = 'clarkson'">Migrate from Clarkson</b-button></b-field>
<b-button type="is-primary" @click="migrationMode = 'clarkson'"
>Migrate from Clarkson</b-button
></b-field
>
</div> </div>
<div v-if="!migrationMode" class="box"> <div v-if="!migrationMode" class="box">
<h1 class="title">Fresh Install</h1> <h1 class="title">Fresh Install</h1>
@@ -170,21 +182,12 @@ export default {
</div> </div>
<div v-if="migrationMode === 'clarkson'" class="box content"> <div v-if="migrationMode === 'clarkson'" class="box content">
<h1 class="title">Migrate from Clarkson</h1> <h1 class="title">Migrate from Clarkson</h1>
<p>You need to make sure that this deployment of Hammond can access the MySQL database used by Clarkson.</p>
<p>If that is not directly possible, you can make a copy of that database somewhere accessible from this instance.</p>
<p>Once that is done, enter the connection string to the MySQL instance in the following format.</p>
<p <p
>You need to make sure that this deployment of Hammond can access the MySQL database used by >All the users imported from Clarkson will have their username as their email in Clarkson database and pasword set to
Clarkson.</p <span class="" style="font-weight:bold">hammond</span></p
>
<p
>If that is not directly possible, you can make a copy of that database somewhere accessible
from this instance.</p
>
<p
>Once that is done, enter the connection string to the MySQL instance in the following
format.</p
>
<p
>All the users imported from Clarkson will have their username as their email in Clarkson
database and pasword set to <span class="" style="font-weight:bold">hammond</span></p
> >
<code> <code>
user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
@@ -200,15 +203,8 @@ export default {
</b-field> </b-field>
<div class="buttons"> <div class="buttons">
<b-button <b-button v-if="!testSuccess" type="is-primary" :disabled="isWorking" @click="testConnection">Test Connection</b-button
v-if="!testSuccess" ><b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate">Migrate</b-button>
type="is-primary"
:disabled="isWorking"
@click="testConnection"
>Test Connection</b-button
><b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate"
>Migrate</b-button
>
<b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button> <b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button>
</div> </div>
</div> </div>
@@ -222,28 +218,22 @@ export default {
<b-input v-model="registerModel.email" type="email" required></b-input> <b-input v-model="registerModel.email" type="email" required></b-input>
</b-field> </b-field>
<b-field label="Your Password"> <b-field label="Your Password">
<b-input <b-input v-model="registerModel.password" type="password" required minlength="8" password-reveal></b-input>
v-model="registerModel.password"
type="password"
required
minlength="8"
password-reveal
></b-input>
</b-field> </b-field>
<b-field label="Currency"> <b-field label="Currency">
<b-select v-model="registerModel.currency" placeholder="Currency" required expanded> <b-autocomplete
<option v-for="option in currencyMasters" :key="option.code" :value="option.code"> v-model="registerModel.currency"
{{ `${option.namePlural} (${option.code})` }} :custom-formatter="formatCurrency"
</option> placeholder="Currency"
</b-select> :data="filteredCurrencyMasters"
:keep-first="true"
:open-on-focus="true"
required
@select="(option) => (selected = option)"
></b-autocomplete>
</b-field> </b-field>
<b-field label="Distance Unit"> <b-field label="Distance Unit">
<b-select <b-select v-model.number="registerModel.distanceUnit" placeholder="Distance Unit" required expanded>
v-model.number="registerModel.distanceUnit"
placeholder="Distance Unit"
required
expanded
>
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key"> <option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
{{ `${option.long} (${option.short})` }} {{ `${option.long} (${option.short})` }}
</option> </option>

View File

@@ -44,6 +44,20 @@ export default {
return this.changePassModel.new === this.changePassModel.renew return this.changePassModel.new === this.changePassModel.renew
}, },
filteredCurrencyMasters() {
return this.currencyMasters.filter((option) => {
return (
option.namePlural
.toString()
.toLowerCase()
.indexOf(this.settingsModel.currency.toLowerCase()) >= 0 ||
option.code
.toString()
.toLowerCase()
.indexOf(this.settingsModel.currency.toLowerCase()) >= 0
)
})
},
}, },
methods: { methods: {
changePassword() { changePassword() {
@@ -109,6 +123,9 @@ export default {
this.tryingToSave = false this.tryingToSave = false
}) })
}, },
formatCurrency(option) {
return `${option.namePlural} (${option.code})`
},
}, },
} }
</script> </script>
@@ -123,11 +140,16 @@ export default {
These will be used as default values whenever you create a new fillup or expense. These will be used as default values whenever you create a new fillup or expense.
</h1> </h1>
<b-field label="Currency"> <b-field label="Currency">
<b-select v-model="settingsModel.currency" placeholder="Currency" required expanded> <b-autocomplete
<option v-for="option in currencyMasters" :key="option.code" :value="option.code"> v-model="settingsModel.currency"
{{ `${option.namePlural} (${option.code})` }} :custom-formatter="formatCurrency"
</option> placeholder="Currency"
</b-select> :data="filteredCurrencyMasters"
:keep-first="true"
:open-on-focus="true"
required
@select="(option) => (selected = option)"
></b-autocomplete>
</b-field> </b-field>
<b-field label="Distance Unit"> <b-field label="Distance Unit">
<b-select v-model.number="settingsModel.distanceUnit" placeholder="Distance Unit" required expanded> <b-select v-model.number="settingsModel.distanceUnit" placeholder="Distance Unit" required expanded>