ability to transfer vehicle and disable users
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user