Created edit for users
This commit is contained in:
@@ -14,11 +14,13 @@ const props = defineProps<{ row: Users }>() // ← accept full row
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'deleted', id: string): void
|
(e: 'deleted', id: string): void
|
||||||
(e: 'error', err: unknown): void
|
(e: 'error', err: unknown): void
|
||||||
|
(e: 'updated', payload: { id: string; username: string; role: 'admin' | 'user' }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isEditOpen = ref(false)
|
const isEditOpen = ref(false)
|
||||||
const isDeleteOpen = ref(false)
|
const isDeleteOpen = ref(false)
|
||||||
const deleting = ref(false)
|
const deleting = ref(false)
|
||||||
|
const updating = ref(false)
|
||||||
|
|
||||||
async function onDeleteConfirmed() {
|
async function onDeleteConfirmed() {
|
||||||
try {
|
try {
|
||||||
@@ -34,8 +36,50 @@ async function onDeleteConfirmed() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEditConfirm() {
|
async function onEditConfirm(payload: { username: string; password?: string; role: 'admin' | 'user' }) {
|
||||||
isEditOpen.value = false
|
try {
|
||||||
|
updating.value = true
|
||||||
|
|
||||||
|
// Build UpdateUserDto payload
|
||||||
|
const body: {
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
role?: 'admin' | 'user'
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
// Only include fields that really changed / are provided
|
||||||
|
if (payload.username && payload.username !== props.row.username) {
|
||||||
|
body.username = payload.username
|
||||||
|
}
|
||||||
|
if (payload.password) {
|
||||||
|
body.password = payload.password
|
||||||
|
}
|
||||||
|
if (payload.role && payload.role !== props.row.role) {
|
||||||
|
body.role = payload.role
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing changed, skip request
|
||||||
|
if (Object.keys(body).length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.put(
|
||||||
|
`/users/${encodeURIComponent(String(props.row.id))}`,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
|
||||||
|
emit('updated', {
|
||||||
|
id: String(props.row.id),
|
||||||
|
username: payload.username || props.row.username,
|
||||||
|
role: payload.role || (props.row.role as 'admin' | 'user'),
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
emit('error', err)
|
||||||
|
} finally {
|
||||||
|
updating.value = false
|
||||||
|
// dialog is already closed in EditUserDialog via v-model update
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -55,6 +99,6 @@ function onEditConfirm() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<EditUserDialog v-model:modelValue="isEditOpen" @confirm="onEditConfirm" />
|
<EditUserDialog v-model:modelValue="isEditOpen" :user="props.row" @confirm="onEditConfirm" />
|
||||||
<DeleteUserDialog v-model:modelValue="isDeleteOpen" :loading="deleting" @confirm="onDeleteConfirmed" />
|
<DeleteUserDialog v-model:modelValue="isDeleteOpen" :loading="deleting" @confirm="onDeleteConfirmed" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { defineProps, defineEmits } from 'vue'
|
import { defineProps, defineEmits, reactive, watch } from 'vue'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
|
import type { Users } from '@/lib/interfaces'
|
||||||
|
|
||||||
// 1) runtime props so Vue + TS agree
|
// 1) runtime props so Vue + TS agree
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -20,26 +21,58 @@ const props = defineProps({
|
|||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<boolean>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
type: Object as PropType<Users>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2) two emits: v-model and confirm
|
// 2) two emits: v-model and confirm
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'confirm',
|
||||||
|
payload: { username: string; password?: string; role: 'admin' | 'user' }
|
||||||
|
): void
|
||||||
(e: 'update:modelValue', v: boolean): void
|
(e: 'update:modelValue', v: boolean): void
|
||||||
(e: 'confirm'): void
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
isAdmin: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// when dialog opens or user changes – sync form with props.user
|
||||||
|
watch(
|
||||||
|
() => [props.modelValue, props.user],
|
||||||
|
() => {
|
||||||
|
if (props.modelValue && props.user) {
|
||||||
|
form.username = props.user.username
|
||||||
|
form.password = ''
|
||||||
|
form.isAdmin = props.user.role === 'admin'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
function onSave() {
|
function onSave() {
|
||||||
emit('confirm')
|
const payload: { username: string; password?: string; role: 'admin' | 'user' } = {
|
||||||
// close the dialog
|
username: form.username,
|
||||||
|
role: form.isAdmin ? 'admin' : 'user',
|
||||||
|
}
|
||||||
|
|
||||||
|
// only send password if user entered something
|
||||||
|
if (form.password.trim() !== '') {
|
||||||
|
payload.password = form.password.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('confirm', payload)
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)">
|
||||||
:open="props.modelValue"
|
|
||||||
@update:open="(v: boolean) => emit('update:modelValue', v)"
|
|
||||||
>
|
|
||||||
<DialogContent class="sm:max-w-[425px]">
|
<DialogContent class="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit profile</DialogTitle>
|
<DialogTitle>Edit profile</DialogTitle>
|
||||||
@@ -51,15 +84,15 @@ function onSave() {
|
|||||||
<div class="grid gap-4 py-4">
|
<div class="grid gap-4 py-4">
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="username" class="text-right">Username</Label>
|
<Label for="username" class="text-right">Username</Label>
|
||||||
<Input id="username" class="col-span-3" />
|
<Input id="username" class="col-span-3" v-model="form.username"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="password" class="text-right">Password</Label>
|
<Label for="password" class="text-right">Password</Label>
|
||||||
<Input id="password" class="col-span-3" type="password" />
|
<Input id="password" class="col-span-3" type="password" v-model="form.password"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="isAdmin" class="text-right">Make admin</Label>
|
<Label for="isAdmin" class="text-right">Make admin</Label>
|
||||||
<Switch id="isAdmin"/>
|
<Switch id="isAdmin" v-model:checked="form.isAdmin"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ type CreateUserDto struct {
|
|||||||
Role string `json:"role" binding:"required,oneof=admin user"`
|
Role string `json:"role" binding:"required,oneof=admin user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateUserDto struct {
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Role string `json:"role,omitempty" binding:"oneof=admin user"`
|
||||||
|
}
|
||||||
|
|
||||||
func MapUser(u models.User) UserDto {
|
func MapUser(u models.User) UserDto {
|
||||||
return UserDto{
|
return UserDto{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
|
|||||||
@@ -137,3 +137,82 @@ func (h *UsersHandler) GetProfile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, dto.MapUser(u))
|
c.JSON(http.StatusOK, dto.MapUser(u))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT /users/:id (admin) — update username, password and/or role
|
||||||
|
func (h *UsersHandler) Update(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, _ := strconv.Atoi(idStr)
|
||||||
|
if id <= 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var u models.User
|
||||||
|
if err := h.db.First(&u, id).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.UpdateUserDto
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := false
|
||||||
|
|
||||||
|
// --- Update username ---
|
||||||
|
if strings.TrimSpace(req.Username) != "" {
|
||||||
|
name := strings.TrimSpace(req.Username)
|
||||||
|
if name == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "username cannot be empty"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.Username = name
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update password ---
|
||||||
|
if req.Password != "" {
|
||||||
|
if len(req.Password) < 4 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "password too short"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := crypto.Hash(req.Password, crypto.DefaultArgon2)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "hash error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.Password = hash
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update role ---
|
||||||
|
if strings.TrimSpace(req.Role) != "" {
|
||||||
|
role := strings.ToLower(strings.TrimSpace(req.Role))
|
||||||
|
if role != "admin" && role != "user" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.Role = models.Role(role)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "nothing to update"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Save(&u).Error; err != nil {
|
||||||
|
// detect duplicate username
|
||||||
|
e := strings.ToLower(err.Error())
|
||||||
|
if strings.Contains(e, "duplicate") || strings.Contains(e, "unique") || strings.Contains(e, "exists") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "username already exists"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, dto.MapUser(u))
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
|
|||||||
r.POST("/auth/change_password", authMW, authH.ChangePassword)
|
r.POST("/auth/change_password", authMW, authH.ChangePassword)
|
||||||
|
|
||||||
r.GET("/users/profile", authMW, usersH.Profile)
|
r.GET("/users/profile", authMW, usersH.Profile)
|
||||||
r.POST("/users/:id/set_role", authMW, adminOnly, usersH.SetRole)
|
r.PUT("/users/:id", authMW, adminOnly, usersH.Update)
|
||||||
r.GET("/users", authMW, adminOnly, usersH.List)
|
r.GET("/users", authMW, adminOnly, usersH.List)
|
||||||
r.POST("/users/create", authMW, adminOnly, usersH.Create)
|
r.POST("/users/create", authMW, adminOnly, usersH.Create)
|
||||||
r.DELETE("/users/:id", authMW, adminOnly, usersH.Delete)
|
r.DELETE("/users/:id", authMW, adminOnly, usersH.Delete)
|
||||||
|
|||||||
Reference in New Issue
Block a user