checkpoint before gorm fix
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Vue + TS</title>
|
<title>Snoop</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -2,43 +2,68 @@
|
|||||||
import {
|
import {
|
||||||
DropdownMenu, DropdownMenuItem,
|
DropdownMenu, DropdownMenuItem,
|
||||||
DropdownMenuTrigger, DropdownMenuContent
|
DropdownMenuTrigger, DropdownMenuContent
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
import EditUserDialog from './EditUserDialog.vue';
|
import EditUserDialog from './EditUserDialog.vue'
|
||||||
import DeleteUserDialog from './DeleteUserDialog.vue';
|
import DeleteUserDialog from './DeleteUserDialog.vue'
|
||||||
import { Ellipsis } from 'lucide-vue-next';
|
import { Ellipsis } from 'lucide-vue-next'
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue'
|
||||||
|
import { api } from '@/lib/api' // <-- use your axios/fetch wrapper
|
||||||
|
|
||||||
const props = defineProps<{ userId: string }>()
|
const props = defineProps<{ userId: string }>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'deleted', id: string): void
|
||||||
|
(e: 'error', err: unknown): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const isEditOpen = ref(false)
|
const isEditOpen = ref(false)
|
||||||
const isDeleteOpen = ref(false)
|
const isDeleteOpen = ref(false)
|
||||||
|
const deleting = ref(false)
|
||||||
|
|
||||||
// your actual delete logic
|
// DELETE /users/:id
|
||||||
function onDeleteConfirmed() {
|
async function onDeleteConfirmed() {
|
||||||
// e.g. await api.deleteUser(props.userId)
|
try {
|
||||||
isDeleteOpen.value = false
|
deleting.value = true
|
||||||
|
await api.delete(`/users/${encodeURIComponent(props.userId)}`)
|
||||||
|
emit('deleted', props.userId)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
emit('error', err)
|
||||||
|
} finally {
|
||||||
|
deleting.value = false
|
||||||
|
// The dialog already closes itself on confirm; nothing else required.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEditConfirm() {
|
function onEditConfirm() {
|
||||||
isEditOpen.value = false
|
isEditOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button class="p-2 rounded hover:bg-muted">
|
<button class="p-2 rounded hover:bg-muted" :disabled="deleting">
|
||||||
<Ellipsis />
|
<Ellipsis />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent align="end" class="w-[160px]">
|
<DropdownMenuContent align="end" class="w-[160px]">
|
||||||
<DropdownMenuItem @click.prevent="isEditOpen = true">
|
<DropdownMenuItem @click.prevent="isEditOpen = true">
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click.prevent="isDeleteOpen = true">
|
<DropdownMenuItem @click.prevent="isDeleteOpen = true" :disabled="deleting">
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<EditUserDialog v-model:modelValue="isEditOpen" @confirm="onEditConfirm()" />
|
<EditUserDialog v-model:modelValue="isEditOpen" @confirm="onEditConfirm()" />
|
||||||
<DeleteUserDialog v-model:modelValue="isDeleteOpen" @confirm="onDeleteConfirmed" />
|
|
||||||
|
<!-- pass 'deleting' to disable the confirm button during request -->
|
||||||
|
<DeleteUserDialog
|
||||||
|
v-model:modelValue="isDeleteOpen"
|
||||||
|
:loading="deleting"
|
||||||
|
@confirm="onDeleteConfirmed"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -12,10 +12,8 @@ import {
|
|||||||
import { defineProps, defineEmits, type PropType } from 'vue'
|
import { defineProps, defineEmits, type PropType } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: { type: Boolean as PropType<boolean>, required: true },
|
||||||
type: Boolean as PropType<boolean>,
|
loading: { type: Boolean as PropType<boolean>, default: false },
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', v: boolean): void
|
(e: 'update:modelValue', v: boolean): void
|
||||||
@@ -36,10 +34,12 @@ const emit = defineEmits<{
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel :disabled="props.loading">Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
|
:disabled="props.loading"
|
||||||
@click="() => { emit('confirm'); emit('update:modelValue', false) }"
|
@click="() => { emit('confirm'); emit('update:modelValue', false) }"
|
||||||
> Delete
|
>
|
||||||
|
{{ props.loading ? 'Deleting…' : 'Delete' }}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -1,28 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu, DropdownMenuContent,
|
DropdownMenu, DropdownMenuContent,
|
||||||
DropdownMenuTrigger, DropdownMenuSeparator,
|
DropdownMenuTrigger, DropdownMenuSeparator,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem, DropdownMenuLabel
|
||||||
DropdownMenuLabel
|
} from '@/components/ui/dropdown-menu'
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Settings } from 'lucide-vue-next';
|
import { Settings } from 'lucide-vue-next'
|
||||||
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
|
|
||||||
const { customComponent } = defineProps<{ customComponent?: any }>()
|
const { customComponent } = defineProps<{ customComponent?: any }>()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
function isActive(prefix: string) {
|
||||||
|
const p = route.path.replace(/\/+$/, '')
|
||||||
|
const tgt = prefix.replace(/\/+$/, '')
|
||||||
|
return p === tgt || p.startsWith(tgt + '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function navLinkClass(prefix: string) {
|
||||||
|
return cn(
|
||||||
|
'text-sm font-medium transition-colors',
|
||||||
|
isActive(prefix) ? 'text-primary' : 'text-muted-foreground hover:text-primary'
|
||||||
|
)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col min-h-screen">
|
<div class="flex flex-col min-h-screen">
|
||||||
<div class="flex items-center justify-end space-x-6 p-4">
|
<div class="flex items-center justify-end space-x-6 p-4">
|
||||||
<nav :class="cn('flex items-center space-x-4 lg:space-x-6', $attrs.class ?? '')">
|
<nav :class="cn('flex items-center space-x-4 lg:space-x-6', $attrs.class ?? '')">
|
||||||
<a href="/admin" class="text-sm font-medium transition-colors hover:text-primary">
|
<RouterLink
|
||||||
|
to="/admin"
|
||||||
|
:class="navLinkClass('/admin')"
|
||||||
|
:aria-current="isActive('/admin') ? 'page' : undefined"
|
||||||
|
>
|
||||||
Admin
|
Admin
|
||||||
</a>
|
</RouterLink>
|
||||||
<a href="/devices"
|
<RouterLink
|
||||||
class="text-sm font-medium text-muted-foreground transition-colors hover:text-primary">
|
to="/devices"
|
||||||
|
:class="navLinkClass('/devices')"
|
||||||
|
:aria-current="isActive('/devices') ? 'page' : undefined"
|
||||||
|
>
|
||||||
Devices
|
Devices
|
||||||
</a>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button class="p-2 rounded hover:bg-muted">
|
<button class="p-2 rounded hover:bg-muted">
|
||||||
@@ -30,28 +53,21 @@ const { customComponent } = defineProps<{ customComponent?: any }>()
|
|||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent class="w-48">
|
<DropdownMenuContent class="w-48">
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>Admin</DropdownMenuLabel>
|
||||||
Admin
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<a href="/settings">
|
<RouterLink to="/settings">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||||
Settings
|
</RouterLink>
|
||||||
</DropdownMenuItem>
|
|
||||||
</a>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<a href="/login">
|
<RouterLink to="/login">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||||
Logout
|
</RouterLink>
|
||||||
</DropdownMenuItem>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-4 p-4">
|
<div class="flex flex-1 flex-col gap-4 p-4">
|
||||||
<!-- <component v-if="customComponent" :is="customComponent" /> -->
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@@ -96,3 +96,30 @@ func (h *UsersHandler) Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusCreated, dto.MapUser(u))
|
c.JSON(http.StatusCreated, dto.MapUser(u))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DELETE /users/:id (admin) — delete user and clear device relations
|
||||||
|
func (h *UsersHandler) Delete(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.Preload("Devices").First(&u, id).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// optional safeguard: prevent self-delete; uncomment if desired
|
||||||
|
// if ClaimUserID(MustClaims(c)) == u.ID { c.JSON(http.StatusBadRequest, gin.H{"error":"cannot delete yourself"}); return }
|
||||||
|
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Model(&u).Association("Devices").Clear(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Delete(&u).Error
|
||||||
|
}); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
|
|||||||
r.POST("/users/:id/set_role", authMW, adminOnly, usersH.SetRole)
|
r.POST("/users/:id/set_role", authMW, adminOnly, usersH.SetRole)
|
||||||
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.GET("/devices", authMW, middleware.DeviceAccessFilter(), devH.List)
|
r.GET("/devices", authMW, middleware.DeviceAccessFilter(), devH.List)
|
||||||
r.POST("/devices/create", authMW, devH.Create)
|
r.POST("/devices/create", authMW, devH.Create)
|
||||||
|
|||||||
Reference in New Issue
Block a user