ability to transfer vehicle and disable users

This commit is contained in:
Akhil Gupta
2021-06-24 10:24:20 +05:30
parent b111e23dea
commit 2bd8481670
15 changed files with 186 additions and 16 deletions

View File

@@ -8,7 +8,7 @@
</a> -->
<h1 align="center" style="margin-bottom:0">Hammond</h1>
<p align="center">Current Version - 2021.06.08</p>
<p align="center">Current Version - 2021.06.24</p>
<p align="center">
A self-hosted vehicle expense tracking system with support for multiple users.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',
})
})
},
})
},
},
}
</script>
<template>
<div class="box">
<div class="box" style="max-width:600px">
<h1 class="subtitle">Share {{ vehicle.nickname }}</h1>
<section>
<b-field v-for="model in models" :key="model.id">
<b-switch v-model="model.isShared" :disabled="model.isOwner" @input="changeShareStatus(model)">
{{ model.name }}
</b-switch>
</b-field>
<div class="columns is-mobile" v-for="model in models" :key="model.id">
<div class="column is-one-third">
<b-field>
<b-switch v-model="model.isShared" :disabled="model.isOwner" @input="changeShareStatus(model)">
{{ model.name }}
</b-switch>
</b-field> </div
><div class="column is-three-quarters">
<b-field>
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">Make Owner</b-button>
</b-field></div
></div
>
</section>
</div>
</template>

View File

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

View File

@@ -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 {
</span>
</b-button>
<p v-if="authError">
There was an error logging in to your account.
There was an error logging in to your account. {{errorMessage}}
</p>
</form>
</Layout>

View File

@@ -172,7 +172,7 @@ export default {
<table class="table is-hoverable">
<tr>
<td>Current Version</td>
<td>2021.06.08</td>
<td>2021.06.24</td>
</tr>
<tr>
<td>Website</td>

View File

@@ -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 {
</div>
</form>
</div>
<b-table :data="users" hoverable mobile-cards detail-key="id" paginated per-page="10">
<b-table :data="users" hoverable mobile-cards detail-key="id" paginated per-page="10" :row-class="(row, index) => row.isDisabled && 'is-disabled'">
<b-table-column v-slot="props" field="name" label="Name">
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">(You)</template>
</b-table-column>
@@ -170,6 +200,10 @@ export default {
<b-table-column v-slot="props" field="createdAt" label="Created" sortable date>
{{ formatDate(props.row.createdAt) }}
</b-table-column>
<b-table-column v-slot="props">
<b-button type="is-success" v-if="props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, false)">Enable</b-button>
<b-button type="is-danger" v-if="!props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, true)">Disable</b-button>
</b-table-column>
</b-table>
</div>
</Layout>

View File

@@ -289,7 +289,7 @@ export default {
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
></b-button>
<b-button v-if="vehicle.isOwner" title="Share vehicle" @click="showShareVehicleModal">
<b-icon pack="fas" icon="share" type="is-info"> </b-icon
<b-icon pack="fas" icon="user-friends" type="is-info"> </b-icon
></b-button>
<b-button v-if="vehicle.isOwner" title="Delete Vehicle" @click="deleteVehicle">
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon