From 2bd848167090e226ce404e41fb0c5e33e5f0eafe Mon Sep 17 00:00:00 2001 From: Akhil Gupta Date: Thu, 24 Jun 2021 10:24:20 +0530 Subject: [PATCH] ability to transfer vehicle and disable users --- README.md | 2 +- server/controllers/auth.go | 5 +++ server/controllers/users.go | 33 ++++++++++++++++++ server/controllers/vehicle.go | 17 ++++++++++ server/db/dbModels.go | 1 + server/db/dbfunctions.go | 15 +++++++++ server/db/migrations.go | 8 ++--- server/service/userService.go | 3 ++ server/service/vehicleService.go | 13 +++++++ ui/src/components/shareVehicle.vue | 54 ++++++++++++++++++++++++++---- ui/src/main.js | 4 ++- ui/src/router/views/login.vue | 7 +++- ui/src/router/views/settings.vue | 2 +- ui/src/router/views/users.vue | 36 +++++++++++++++++++- ui/src/router/views/vehicle.vue | 2 +- 15 files changed, 186 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6d95fea..d34ccc8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ -->

Hammond

-

Current Version - 2021.06.08

+

Current Version - 2021.06.24

A self-hosted vehicle expense tracking system with support for multiple users. diff --git a/server/controllers/auth.go b/server/controllers/auth.go index b98b654..b130739 100644 --- a/server/controllers/auth.go +++ b/server/controllers/auth.go @@ -102,6 +102,11 @@ func userLogin(c *gin.Context) { c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password"))) return } + + 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."))) + return + } UpdateContextUserModel(c, user.ID) token, refreshToken := common.GenToken(user.ID, user.Role) response := models.LoginResponse{ diff --git a/server/controllers/users.go b/server/controllers/users.go index 1ea48c4..e9ad2ad 100644 --- a/server/controllers/users.go +++ b/server/controllers/users.go @@ -3,12 +3,17 @@ 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 RegisterUserController(router *gin.RouterGroup) { router.GET("/users", allUsers) + router.POST("/users/:id/enable", ShouldBeAdmin(), enableUser) + router.POST("/users/:id/disable", ShouldBeAdmin(), disableUser) } func allUsers(c *gin.Context) { @@ -20,3 +25,31 @@ func allUsers(c *gin.Context) { c.JSON(http.StatusOK, users) } +func enableUser(c *gin.Context) { + var searchByIdQuery models.SearchByIdQuery + if err := c.ShouldBindUri(&searchByIdQuery); err == nil { + err := service.SetDisabledStatusForUser(searchByIdQuery.Id, false) + if err != nil { + c.JSON(http.StatusBadRequest, err) + return + } + c.JSON(http.StatusOK, gin.H{}) + } else { + c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err)) + } + +} +func disableUser(c *gin.Context) { + var searchByIdQuery models.SearchByIdQuery + if err := c.ShouldBindUri(&searchByIdQuery); err == nil { + err := service.SetDisabledStatusForUser(searchByIdQuery.Id, true) + if err != nil { + c.JSON(http.StatusBadRequest, err) + return + } + c.JSON(http.StatusOK, gin.H{}) + } else { + c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err)) + } + +} diff --git a/server/controllers/vehicle.go b/server/controllers/vehicle.go index f5947d5..83148b7 100644 --- a/server/controllers/vehicle.go +++ b/server/controllers/vehicle.go @@ -20,6 +20,7 @@ func RegisterVehicleController(router *gin.RouterGroup) { router.GET("/vehicles/:id/users", getVehicleUsers) router.POST("/vehicles/:id/users/:subId", shareVehicle) router.DELETE("/vehicles/:id/users/:subId", unshareVehicle) + router.POST("/vehicles/:id/users/:subId/transfer", transferVehicle) router.GET("/me/vehicles", getMyVehicles) router.GET("/me/stats", getMystats) @@ -409,6 +410,22 @@ func shareVehicle(c *gin.Context) { c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err)) } } +func transferVehicle(c *gin.Context) { + var searchByIdQuery models.SubItemQuery + + if err := c.ShouldBindUri(&searchByIdQuery); err == nil { + + err := service.TransferVehicle(searchByIdQuery.Id, c.MustGet("userId").(string), 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 diff --git a/server/db/dbModels.go b/server/db/dbModels.go index 72188d0..d0966d1 100644 --- a/server/db/dbModels.go +++ b/server/db/dbModels.go @@ -17,6 +17,7 @@ type User struct { Role Role `json:"role"` Name string `json:"name"` Vehicles []Vehicle `gorm:"many2many:user_vehicles;" json:"vehicles"` + IsDisabled bool `json:"isDisabled"` } func (b *User) MarshalJSON() ([]byte, error) { diff --git a/server/db/dbfunctions.go b/server/db/dbfunctions.go index ee06921..f2142e7 100644 --- a/server/db/dbfunctions.go +++ b/server/db/dbfunctions.go @@ -33,6 +33,11 @@ func FindOneUser(condition interface{}) (User, error) { err := DB.Where(condition).First(&model).Error return model, err } +func SetDisabledStatusForUser(userId string, isDisabled bool) error { + //Cannot do this for admin + tx := DB.Debug().Model(&User{}).Where("id= ? and role=?", userId, USER).Update("is_disabled", isDisabled) + return tx.Error +} func GetAllUsers() (*[]User, error) { sorting := "created_at desc" @@ -92,6 +97,16 @@ func ShareVehicle(vehicleId, userId string) error { } return nil } +func TransferVehicle(vehicleId, ownerId, newUserID string) error { + + tx := DB.Model(&UserVehicle{}).Where("vehicle_id = ? AND user_id = ?", vehicleId, ownerId).Update("is_owner", false) + if tx.Error != nil { + return tx.Error + } + tx = DB.Model(&UserVehicle{}).Where("vehicle_id = ? AND user_id = ?", vehicleId, newUserID).Update("is_owner", true) + + return tx.Error +} func UnshareVehicle(vehicleId, userId string) error { var mapping UserVehicle diff --git a/server/db/migrations.go b/server/db/migrations.go index 6bb9d83..cddd3b3 100644 --- a/server/db/migrations.go +++ b/server/db/migrations.go @@ -14,10 +14,10 @@ type localMigration struct { } 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", - // }, + { + Name: "2021_06_24_04_42_SetUserDisabledFalse", + Query: "update users set is_disabled=0", + }, } func RunMigrations() { diff --git a/server/service/userService.go b/server/service/userService.go index 1799371..6799afa 100644 --- a/server/service/userService.go +++ b/server/service/userService.go @@ -45,3 +45,6 @@ func UpdatePassword(id, password string) (bool, error) { } return true, nil } +func SetDisabledStatusForUser(userId string, isDisabled bool) error { + return db.SetDisabledStatusForUser(userId, isDisabled) +} diff --git a/server/service/vehicleService.go b/server/service/vehicleService.go index 536c4c6..5866d7b 100644 --- a/server/service/vehicleService.go +++ b/server/service/vehicleService.go @@ -1,6 +1,8 @@ package service import ( + "fmt" + "github.com/akhilrex/hammond/db" "github.com/akhilrex/hammond/models" "gorm.io/gorm/clause" @@ -59,6 +61,17 @@ func DeleteVehicle(vehicleId string) error { func ShareVehicle(vehicleId, userId string) error { return db.ShareVehicle(vehicleId, userId) } +func TransferVehicle(vehicleId, ownerId, newUserID string) error { + vehicleOwnerId, err := GetVehicleOwner(vehicleId) + if err != nil { + return err + } + if vehicleOwnerId != ownerId { + return fmt.Errorf("Only vehicle owner can transfer the vehicle") + } + + return db.TransferVehicle(vehicleId, ownerId, newUserID) +} func UnshareVehicle(vehicleId, userId string) error { return db.UnshareVehicle(vehicleId, userId) } diff --git a/ui/src/components/shareVehicle.vue b/ui/src/components/shareVehicle.vue index baec3e9..d324b50 100644 --- a/ui/src/components/shareVehicle.vue +++ b/ui/src/components/shareVehicle.vue @@ -50,19 +50,61 @@ export default { axios.delete(url).then((data) => {}) } }, + transferVehicle(model) { + if (!model.isShared) { + return + } + this.$buefy.dialog.confirm({ + title: 'Transfer Vehicle', + message: 'Are you sure you want to do this? You will lose ownership and all editing rights if you confirm.', + cancelText: 'Cancel', + confirmText: 'Go Ahead', + onConfirm: () => { + var url = `/api/vehicles/${this.vehicle.id}/users/${model.id}/transfer` + axios + .post(url, {}) + .then((data) => { + this.$buefy.toast.open({ + message: 'Vehicle Transferred Successfully', + type: 'is-success', + duration: 3000, + }) + setTimeout(() => { + this.$router.go() + }, 3000); + }) + .catch((ex) => { + this.$buefy.toast.open({ + duration: 5000, + message: ex.message, + position: 'is-bottom', + type: 'is-danger', + }) + }) + }, + }) + }, }, } diff --git a/ui/src/main.js b/ui/src/main.js index 63c0a24..191dd10 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -20,6 +20,7 @@ import { faEyeSlash, faTrash, faShare, + faUserFriends, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' @@ -48,7 +49,8 @@ library.add( faEye, faEyeSlash, faTrash, - faShare + faShare, + faUserFriends, ) Vue.use(Buefy, { defaultIconComponent: 'vue-fontawesome', diff --git a/ui/src/router/views/login.vue b/ui/src/router/views/login.vue index 350fb92..2b1765b 100644 --- a/ui/src/router/views/login.vue +++ b/ui/src/router/views/login.vue @@ -16,6 +16,7 @@ export default { password: '', authError: null, tryingToLogIn: false, + errorMessage:'' } }, computed: { @@ -37,6 +38,7 @@ export default { // and password they provided. tryToLogIn() { this.tryingToLogIn = true + this.errorMessage=''; // Reset the authError if it existed. this.authError = null return this.logIn({ @@ -51,6 +53,9 @@ export default { // Redirect to the originally requested page, or to the home page }) .catch((error) => { + if(error.response.data?.errors?.login){ + this.errorMessage=error.response.data.errors.login + } this.tryingToLogIn = false this.authError = error }) @@ -85,7 +90,7 @@ export default {

- There was an error logging in to your account. + There was an error logging in to your account. {{errorMessage}}

diff --git a/ui/src/router/views/settings.vue b/ui/src/router/views/settings.vue index d8ed1ea..f0878b1 100644 --- a/ui/src/router/views/settings.vue +++ b/ui/src/router/views/settings.vue @@ -172,7 +172,7 @@ export default { - + diff --git a/ui/src/router/views/users.vue b/ui/src/router/views/users.vue index d96ce03..6aa18ed 100644 --- a/ui/src/router/views/users.vue +++ b/ui/src/router/views/users.vue @@ -53,6 +53,36 @@ export default { formatDate(date) { return parseAndFormatDate(date) }, + changeDisabledStatus(userId,status){ + this.$buefy.dialog.confirm({ + title: status?'Disable User':"Enable User", + message: 'Are you sure you want to do this?', + cancelText: 'Cancel', + confirmText: 'Go Ahead', + onConfirm: () => { + + var url = `/api/users/${userId}/${status?"disable":"enable"}` + axios + .post(url, {}) + .then((data) => { + this.$buefy.toast.open({ + message: status?"User disabled successfully":'User enabled successfully', + type: 'is-success', + duration: 3000, + }) + this.getUsers(); + }) + .catch((ex) => { + this.$buefy.toast.open({ + duration: 5000, + message: ex.message, + position: 'is-bottom', + type: 'is-danger', + }) + }) + }, + }) + }, resetUserForm() { this.registerModel = { name: '', @@ -157,7 +187,7 @@ export default { - + {{ `${props.row.name}` }} @@ -170,6 +200,10 @@ export default { {{ formatDate(props.row.createdAt) }} + + Enable + Disable + diff --git a/ui/src/router/views/vehicle.vue b/ui/src/router/views/vehicle.vue index 4b6f09e..f130420 100644 --- a/ui/src/router/views/vehicle.vue +++ b/ui/src/router/views/vehicle.vue @@ -289,7 +289,7 @@ export default { -
Current Version2021.06.082021.06.24
Website