chenged display of devices according to user`s role. all changes are made in backend
This commit is contained in:
109
management-ui/package-lock.json
generated
109
management-ui/package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
"vaul-vue": "^0.4.1",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
@@ -2624,6 +2625,114 @@
|
|||||||
"uuid": "dist/esm/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vaul-vue": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vaul-vue/-/vaul-vue-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-A6jOWOZX5yvyo1qMn7IveoWN91mJI5L3BUKsIwkg6qrTGgHs1Sb1JF/vyLJgnbN1rH4OOOxFbtqL9A46bOyGUQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^10.8.0",
|
||||||
|
"reka-ui": "^2.0.0",
|
||||||
|
"vue": "^3.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"reka-ui": "^2.0.0",
|
||||||
|
"vue": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vaul-vue/node_modules/@types/web-bluetooth": {
|
||||||
|
"version": "0.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||||
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/vaul-vue/node_modules/@vueuse/core": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.20",
|
||||||
|
"@vueuse/metadata": "10.11.1",
|
||||||
|
"@vueuse/shared": "10.11.1",
|
||||||
|
"vue-demi": ">=0.14.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vaul-vue/node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vaul-vue/node_modules/@vueuse/metadata": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vaul-vue/node_modules/@vueuse/shared": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": ">=0.14.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vaul-vue/node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
"vaul-vue": "^0.4.1",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
|
|||||||
22
management-ui/src/components/ui/drawer/Drawer.vue
Normal file
22
management-ui/src/components/ui/drawer/Drawer.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue"
|
||||||
|
import { useForwardPropsEmits } from "reka-ui"
|
||||||
|
import { DrawerRoot } from "vaul-vue"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<DrawerRootProps>(), {
|
||||||
|
shouldScaleBackground: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<DrawerRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DrawerRoot
|
||||||
|
data-slot="drawer"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DrawerRoot>
|
||||||
|
</template>
|
||||||
15
management-ui/src/components/ui/drawer/DrawerClose.vue
Normal file
15
management-ui/src/components/ui/drawer/DrawerClose.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DrawerCloseProps } from "vaul-vue"
|
||||||
|
import { DrawerClose } from "vaul-vue"
|
||||||
|
|
||||||
|
const props = defineProps<DrawerCloseProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DrawerClose
|
||||||
|
data-slot="drawer-close"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DrawerClose>
|
||||||
|
</template>
|
||||||
34
management-ui/src/components/ui/drawer/DrawerContent.vue
Normal file
34
management-ui/src/components/ui/drawer/DrawerContent.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { useForwardPropsEmits } from "reka-ui"
|
||||||
|
import { DrawerContent, DrawerPortal } from "vaul-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import DrawerOverlay from "./DrawerOverlay.vue"
|
||||||
|
|
||||||
|
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DrawerPortal>
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerContent
|
||||||
|
data-slot="drawer-content"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(
|
||||||
|
`group/drawer-content bg-background fixed z-50 flex h-auto flex-col`,
|
||||||
|
`data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg`,
|
||||||
|
`data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg`,
|
||||||
|
`data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm`,
|
||||||
|
`data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm`,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<div class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
|
<slot />
|
||||||
|
</DrawerContent>
|
||||||
|
</DrawerPortal>
|
||||||
|
</template>
|
||||||
21
management-ui/src/components/ui/drawer/DrawerDescription.vue
Normal file
21
management-ui/src/components/ui/drawer/DrawerDescription.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DrawerDescriptionProps } from "vaul-vue"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DrawerDescription } from "vaul-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DrawerDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DrawerDescription
|
||||||
|
data-slot="drawer-description"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DrawerDescription>
|
||||||
|
</template>
|
||||||
17
management-ui/src/components/ui/drawer/DrawerFooter.vue
Normal file
17
management-ui/src/components/ui/drawer/DrawerFooter.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
management-ui/src/components/ui/drawer/DrawerHeader.vue
Normal file
17
management-ui/src/components/ui/drawer/DrawerHeader.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
19
management-ui/src/components/ui/drawer/DrawerOverlay.vue
Normal file
19
management-ui/src/components/ui/drawer/DrawerOverlay.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DialogOverlayProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DrawerOverlay } from "vaul-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DrawerOverlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
21
management-ui/src/components/ui/drawer/DrawerTitle.vue
Normal file
21
management-ui/src/components/ui/drawer/DrawerTitle.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DrawerTitleProps } from "vaul-vue"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DrawerTitle } from "vaul-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DrawerTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DrawerTitle
|
||||||
|
data-slot="drawer-title"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('text-foreground font-semibold', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DrawerTitle>
|
||||||
|
</template>
|
||||||
15
management-ui/src/components/ui/drawer/DrawerTrigger.vue
Normal file
15
management-ui/src/components/ui/drawer/DrawerTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DrawerTriggerProps } from "vaul-vue"
|
||||||
|
import { DrawerTrigger } from "vaul-vue"
|
||||||
|
|
||||||
|
const props = defineProps<DrawerTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DrawerTrigger
|
||||||
|
data-slot="drawer-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DrawerTrigger>
|
||||||
|
</template>
|
||||||
9
management-ui/src/components/ui/drawer/index.ts
Normal file
9
management-ui/src/components/ui/drawer/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { default as Drawer } from "./Drawer.vue"
|
||||||
|
export { default as DrawerClose } from "./DrawerClose.vue"
|
||||||
|
export { default as DrawerContent } from "./DrawerContent.vue"
|
||||||
|
export { default as DrawerDescription } from "./DrawerDescription.vue"
|
||||||
|
export { default as DrawerFooter } from "./DrawerFooter.vue"
|
||||||
|
export { default as DrawerHeader } from "./DrawerHeader.vue"
|
||||||
|
export { default as DrawerOverlay } from "./DrawerOverlay.vue"
|
||||||
|
export { default as DrawerTitle } from "./DrawerTitle.vue"
|
||||||
|
export { default as DrawerTrigger } from "./DrawerTrigger.vue"
|
||||||
@@ -26,12 +26,52 @@ func (h *DevicesHandler) List(c *gin.Context) {
|
|||||||
limit = 50
|
limit = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
var total int64
|
// Get user context
|
||||||
h.db.Model(&models.Device{}).Count(&total)
|
userContext, exists := c.Get("user")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, ok := userContext.(UserContext)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user data"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
var devs []models.Device
|
var devs []models.Device
|
||||||
if err := h.db.Preload("Users").Offset(offset).Limit(limit).Find(&devs).Error; err != nil {
|
var err error
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
|
||||||
|
if user.Role == models.RoleAdmin {
|
||||||
|
// Admin user - show all devices
|
||||||
|
err = h.db.Model(&models.Device{}).Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "count query failed: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.db.Preload("Users").Offset(offset).Limit(limit).Find(&devs).Error
|
||||||
|
} else {
|
||||||
|
err = h.db.Model(&models.Device{}).
|
||||||
|
Joins("INNER JOIN user_devices ON user_devices.id = devices.guid").
|
||||||
|
Where("user_devices.guid = ?", user.ID).
|
||||||
|
Count(&total).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "count query failed: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.db.Preload("Users").
|
||||||
|
Joins("INNER JOIN user_devices ON user_devices.id = devices.guid").
|
||||||
|
Where("user_devices.guid = ?", user.ID).
|
||||||
|
Offset(offset).Limit(limit).
|
||||||
|
Find(&devs).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +79,7 @@ func (h *DevicesHandler) List(c *gin.Context) {
|
|||||||
for _, d := range devs {
|
for _, d := range devs {
|
||||||
out = append(out, dto.MapDevice(d))
|
out = append(out, dto.MapDevice(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, dto.DeviceListDto{Devices: out, Offset: offset, Limit: limit, Total: total})
|
c.JSON(http.StatusOK, dto.DeviceListDto{Devices: out, Offset: offset, Limit: limit, Total: total})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"smoop-api/internal/crypto"
|
"smoop-api/internal/crypto"
|
||||||
|
"smoop-api/internal/models"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UserContext holds structured user information from JWT
|
||||||
|
type UserContext struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Role models.Role `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
func Auth(jwtMgr *crypto.JWTManager) gin.HandlerFunc {
|
func Auth(jwtMgr *crypto.JWTManager) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
h := c.GetHeader("Authorization")
|
h := c.GetHeader("Authorization")
|
||||||
@@ -23,6 +31,12 @@ func Auth(jwtMgr *crypto.JWTManager) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
claims, _ := token.Claims.(jwt.MapClaims)
|
claims, _ := token.Claims.(jwt.MapClaims)
|
||||||
|
userContext := UserContext{
|
||||||
|
ID: uint(claims["sub"].(float64)),
|
||||||
|
Username: claims["name"].(string),
|
||||||
|
Role: models.Role(claims["role"].(string)),
|
||||||
|
}
|
||||||
|
c.Set("user", userContext)
|
||||||
c.Set("claims", claims)
|
c.Set("claims", claims)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
@@ -30,8 +44,16 @@ func Auth(jwtMgr *crypto.JWTManager) gin.HandlerFunc {
|
|||||||
|
|
||||||
func RequireRole(role string) gin.HandlerFunc {
|
func RequireRole(role string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
claims := MustClaims(c)
|
userContext, exists := c.Get("user")
|
||||||
if ClaimRole(claims) != role {
|
if !exists {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||||
|
}
|
||||||
|
user, ok := userContext.(UserContext)
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid user data"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(user.Role) != role {
|
||||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -76,3 +98,14 @@ func ClaimRole(claims map[string]interface{}) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New helper to get UserContext from context
|
||||||
|
func GetUserContext(c *gin.Context) (UserContext, bool) {
|
||||||
|
userContext, exists := c.Get("user")
|
||||||
|
if !exists {
|
||||||
|
return UserContext{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
user, ok := userContext.(UserContext)
|
||||||
|
return user, ok
|
||||||
|
}
|
||||||
|
|||||||
37
server/internal/middleware/access.go
Normal file
37
server/internal/middleware/access.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"smoop-api/internal/handlers"
|
||||||
|
"smoop-api/internal/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeviceAccessFilter middleware sets filtering context for device access
|
||||||
|
func DeviceAccessFilter() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
userContext, exists := c.Get("user")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(401, gin.H{"error": "unauthorized"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, ok := userContext.(handlers.UserContext)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(401, gin.H{"error": "invalid user data"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set filter flag and user ID in context
|
||||||
|
if user.Role == models.RoleAdmin {
|
||||||
|
c.Set("filterDevices", false) // Admin sees all devices
|
||||||
|
} else {
|
||||||
|
c.Set("filterDevices", true) // Regular user needs filtering
|
||||||
|
c.Set("userID", user.ID) // Store user ID for filtering
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"smoop-api/internal/config"
|
"smoop-api/internal/config"
|
||||||
"smoop-api/internal/crypto"
|
"smoop-api/internal/crypto"
|
||||||
"smoop-api/internal/handlers"
|
"smoop-api/internal/handlers"
|
||||||
|
"smoop-api/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
|
func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
|
||||||
@@ -41,7 +42,7 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
|
|||||||
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.GET("/devices", authMW, devH.List)
|
r.GET("/devices", authMW, middleware.DeviceAccessFilter(), devH.List)
|
||||||
r.POST("/devices/create", authMW, devH.Create)
|
r.POST("/devices/create", authMW, devH.Create)
|
||||||
r.POST("/devices/:guid/rename", authMW, devH.Rename)
|
r.POST("/devices/:guid/rename", authMW, devH.Rename)
|
||||||
r.POST("/devices/:guid/add_to_user", authMW, devH.AddToUser)
|
r.POST("/devices/:guid/add_to_user", authMW, devH.AddToUser)
|
||||||
|
|||||||
Reference in New Issue
Block a user