ability to transfer vehicle and disable users
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
</a> -->
|
</a> -->
|
||||||
|
|
||||||
<h1 align="center" style="margin-bottom:0">Hammond</h1>
|
<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">
|
<p align="center">
|
||||||
A self-hosted vehicle expense tracking system with support for multiple users.
|
A self-hosted vehicle expense tracking system with support for multiple users.
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ func userLogin(c *gin.Context) {
|
|||||||
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 {
|
||||||
|
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)
|
UpdateContextUserModel(c, user.ID)
|
||||||
token, refreshToken := common.GenToken(user.ID, user.Role)
|
token, refreshToken := common.GenToken(user.ID, user.Role)
|
||||||
response := models.LoginResponse{
|
response := models.LoginResponse{
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/akhilrex/hammond/common"
|
||||||
"github.com/akhilrex/hammond/db"
|
"github.com/akhilrex/hammond/db"
|
||||||
|
"github.com/akhilrex/hammond/models"
|
||||||
|
"github.com/akhilrex/hammond/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterUserController(router *gin.RouterGroup) {
|
func RegisterUserController(router *gin.RouterGroup) {
|
||||||
router.GET("/users", allUsers)
|
router.GET("/users", allUsers)
|
||||||
|
router.POST("/users/:id/enable", ShouldBeAdmin(), enableUser)
|
||||||
|
router.POST("/users/:id/disable", ShouldBeAdmin(), disableUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func allUsers(c *gin.Context) {
|
func allUsers(c *gin.Context) {
|
||||||
@@ -20,3 +25,31 @@ func allUsers(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, users)
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func RegisterVehicleController(router *gin.RouterGroup) {
|
|||||||
router.GET("/vehicles/:id/users", getVehicleUsers)
|
router.GET("/vehicles/:id/users", getVehicleUsers)
|
||||||
router.POST("/vehicles/:id/users/:subId", shareVehicle)
|
router.POST("/vehicles/:id/users/:subId", shareVehicle)
|
||||||
router.DELETE("/vehicles/:id/users/:subId", unshareVehicle)
|
router.DELETE("/vehicles/:id/users/:subId", unshareVehicle)
|
||||||
|
router.POST("/vehicles/:id/users/:subId/transfer", transferVehicle)
|
||||||
|
|
||||||
router.GET("/me/vehicles", getMyVehicles)
|
router.GET("/me/vehicles", getMyVehicles)
|
||||||
router.GET("/me/stats", getMystats)
|
router.GET("/me/stats", getMystats)
|
||||||
@@ -409,6 +410,22 @@ func shareVehicle(c *gin.Context) {
|
|||||||
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
|
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) {
|
func unshareVehicle(c *gin.Context) {
|
||||||
var searchByIdQuery models.SubItemQuery
|
var searchByIdQuery models.SubItemQuery
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type User struct {
|
|||||||
Role Role `json:"role"`
|
Role Role `json:"role"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Vehicles []Vehicle `gorm:"many2many:user_vehicles;" json:"vehicles"`
|
Vehicles []Vehicle `gorm:"many2many:user_vehicles;" json:"vehicles"`
|
||||||
|
IsDisabled bool `json:"isDisabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *User) MarshalJSON() ([]byte, error) {
|
func (b *User) MarshalJSON() ([]byte, error) {
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ func FindOneUser(condition interface{}) (User, error) {
|
|||||||
err := DB.Where(condition).First(&model).Error
|
err := DB.Where(condition).First(&model).Error
|
||||||
return model, err
|
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) {
|
func GetAllUsers() (*[]User, error) {
|
||||||
|
|
||||||
sorting := "created_at desc"
|
sorting := "created_at desc"
|
||||||
@@ -92,6 +97,16 @@ func ShareVehicle(vehicleId, userId string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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 {
|
func UnshareVehicle(vehicleId, userId string) error {
|
||||||
var mapping UserVehicle
|
var mapping UserVehicle
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ type localMigration struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var migrations = []localMigration{
|
var migrations = []localMigration{
|
||||||
// {
|
{
|
||||||
// Name: "2020_11_03_04_42_SetDefaultDownloadStatus",
|
Name: "2021_06_24_04_42_SetUserDisabledFalse",
|
||||||
// Query: "update podcast_items set download_status=2 where download_path!='' and download_status=0",
|
Query: "update users set is_disabled=0",
|
||||||
// },
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunMigrations() {
|
func RunMigrations() {
|
||||||
|
|||||||
@@ -45,3 +45,6 @@ func UpdatePassword(id, password string) (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
func SetDisabledStatusForUser(userId string, isDisabled bool) error {
|
||||||
|
return db.SetDisabledStatusForUser(userId, isDisabled)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/akhilrex/hammond/db"
|
"github.com/akhilrex/hammond/db"
|
||||||
"github.com/akhilrex/hammond/models"
|
"github.com/akhilrex/hammond/models"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
@@ -59,6 +61,17 @@ func DeleteVehicle(vehicleId string) error {
|
|||||||
func ShareVehicle(vehicleId, userId string) error {
|
func ShareVehicle(vehicleId, userId string) error {
|
||||||
return db.ShareVehicle(vehicleId, userId)
|
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 {
|
func UnshareVehicle(vehicleId, userId string) error {
|
||||||
return db.UnshareVehicle(vehicleId, userId)
|
return db.UnshareVehicle(vehicleId, userId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,19 +50,61 @@ export default {
|
|||||||
axios.delete(url).then((data) => {})
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="box">
|
<div class="box" style="max-width:600px">
|
||||||
<h1 class="subtitle">Share {{ vehicle.nickname }}</h1>
|
<h1 class="subtitle">Share {{ vehicle.nickname }}</h1>
|
||||||
<section>
|
<section>
|
||||||
<b-field v-for="model in models" :key="model.id">
|
<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)">
|
<b-switch v-model="model.isShared" :disabled="model.isOwner" @input="changeShareStatus(model)">
|
||||||
{{ model.name }}
|
{{ model.name }}
|
||||||
</b-switch>
|
</b-switch>
|
||||||
</b-field>
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faTrash,
|
faTrash,
|
||||||
faShare,
|
faShare,
|
||||||
|
faUserFriends,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
@@ -48,7 +49,8 @@ library.add(
|
|||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faTrash,
|
faTrash,
|
||||||
faShare
|
faShare,
|
||||||
|
faUserFriends,
|
||||||
)
|
)
|
||||||
Vue.use(Buefy, {
|
Vue.use(Buefy, {
|
||||||
defaultIconComponent: 'vue-fontawesome',
|
defaultIconComponent: 'vue-fontawesome',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default {
|
|||||||
password: '',
|
password: '',
|
||||||
authError: null,
|
authError: null,
|
||||||
tryingToLogIn: false,
|
tryingToLogIn: false,
|
||||||
|
errorMessage:''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -37,6 +38,7 @@ export default {
|
|||||||
// and password they provided.
|
// and password they provided.
|
||||||
tryToLogIn() {
|
tryToLogIn() {
|
||||||
this.tryingToLogIn = true
|
this.tryingToLogIn = true
|
||||||
|
this.errorMessage='';
|
||||||
// Reset the authError if it existed.
|
// Reset the authError if it existed.
|
||||||
this.authError = null
|
this.authError = null
|
||||||
return this.logIn({
|
return this.logIn({
|
||||||
@@ -51,6 +53,9 @@ export default {
|
|||||||
// Redirect to the originally requested page, or to the home page
|
// Redirect to the originally requested page, or to the home page
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
if(error.response.data?.errors?.login){
|
||||||
|
this.errorMessage=error.response.data.errors.login
|
||||||
|
}
|
||||||
this.tryingToLogIn = false
|
this.tryingToLogIn = false
|
||||||
this.authError = error
|
this.authError = error
|
||||||
})
|
})
|
||||||
@@ -85,7 +90,7 @@ export default {
|
|||||||
</span>
|
</span>
|
||||||
</b-button>
|
</b-button>
|
||||||
<p v-if="authError">
|
<p v-if="authError">
|
||||||
There was an error logging in to your account.
|
There was an error logging in to your account. {{errorMessage}}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export default {
|
|||||||
<table class="table is-hoverable">
|
<table class="table is-hoverable">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Current Version</td>
|
<td>Current Version</td>
|
||||||
<td>2021.06.08</td>
|
<td>2021.06.24</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Website</td>
|
<td>Website</td>
|
||||||
|
|||||||
@@ -53,6 +53,36 @@ export default {
|
|||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return parseAndFormatDate(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() {
|
resetUserForm() {
|
||||||
this.registerModel = {
|
this.registerModel = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -157,7 +187,7 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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">
|
<b-table-column v-slot="props" field="name" label="Name">
|
||||||
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">(You)</template>
|
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">(You)</template>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
@@ -170,6 +200,10 @@ export default {
|
|||||||
<b-table-column v-slot="props" field="createdAt" label="Created" sortable date>
|
<b-table-column v-slot="props" field="createdAt" label="Created" sortable date>
|
||||||
{{ formatDate(props.row.createdAt) }}
|
{{ formatDate(props.row.createdAt) }}
|
||||||
</b-table-column>
|
</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>
|
</b-table>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export default {
|
|||||||
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
||||||
></b-button>
|
></b-button>
|
||||||
<b-button v-if="vehicle.isOwner" title="Share vehicle" @click="showShareVehicleModal">
|
<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>
|
||||||
<b-button v-if="vehicle.isOwner" title="Delete Vehicle" @click="deleteVehicle">
|
<b-button v-if="vehicle.isOwner" title="Delete Vehicle" @click="deleteVehicle">
|
||||||
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
||||||
|
|||||||
Reference in New Issue
Block a user