first commit, i i have no idea what i have done
This commit is contained in:
14
management-ui/src/pages/Admin.vue
Normal file
14
management-ui/src/pages/Admin.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import Navbar from '@/customcompometns/Navbar.vue';
|
||||
import Admincomponent from '@/customcompometns/Admincomponent.vue';
|
||||
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <Navbar :custom-component="Admincomponent"/> -->
|
||||
<Navbar>
|
||||
<Admincomponent/>
|
||||
</Navbar>
|
||||
</template>
|
||||
246
management-ui/src/pages/Create.vue
Normal file
246
management-ui/src/pages/Create.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<script setup lang="ts">
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import AssignDevice from '@/customcompometns/AssignDevice.vue'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import Navbar from '@/customcompometns/Navbar.vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineProps, defineEmits, reactive, watch, ref, computed } from 'vue';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const mode = useColorMode()
|
||||
|
||||
type CreateDevicePayload = {
|
||||
guid: string
|
||||
name: string
|
||||
userIds: number[]
|
||||
}
|
||||
|
||||
type CreateUserPayload = {
|
||||
username: string
|
||||
password: string
|
||||
role: string // 'admin' | 'user'
|
||||
}
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// local form state
|
||||
const device_form = reactive({
|
||||
guid: uuidv4(), // default fresh UUID
|
||||
name: '',
|
||||
})
|
||||
|
||||
const user_form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
isAdmin: false,
|
||||
})
|
||||
|
||||
// userIds from AssignDevice (expects v-model of string[] ids)
|
||||
const selectedUserIds = ref<string[]>([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
device_form.guid = uuidv4()
|
||||
device_form.name = ''
|
||||
selectedUserIds.value = []
|
||||
user_form.username = ''
|
||||
user_form.password = ''
|
||||
user_form.isAdmin = false
|
||||
userError.value = null
|
||||
userSubmitting.value = false
|
||||
userSuccess.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// simple UUIDv4 check (best-effort)
|
||||
const uuidV4Re = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
const submitting = ref(false)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
|
||||
const userSubmitting = ref(false)
|
||||
const userError = ref<string | null>(null)
|
||||
const userSuccess = ref<string | null>(null)
|
||||
const userRole = computed<string>(() => (user_form.isAdmin ? 'admin' : 'user'))
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return (
|
||||
!!device_form.guid &&
|
||||
uuidV4Re.test(device_form.guid) &&
|
||||
!!device_form.name.trim() &&
|
||||
!submitting.value
|
||||
)
|
||||
})
|
||||
|
||||
async function submitDevice() {
|
||||
errorMsg.value = null
|
||||
if (!canSubmit.value) {
|
||||
errorMsg.value = 'Please provide a valid GUID and name.'
|
||||
return
|
||||
}
|
||||
|
||||
const userIds: number[] = selectedUserIds.value
|
||||
.map((s) => Number(s))
|
||||
.filter((n) => Number.isFinite(n) && n >= 0)
|
||||
|
||||
const payload: CreateDevicePayload = {
|
||||
guid: device_form.guid,
|
||||
name: device_form.name.trim(),
|
||||
userIds,
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await api.post('/devices/create', payload)
|
||||
router.replace('/admin')
|
||||
} catch (e: any) {
|
||||
// keep client error generic
|
||||
errorMsg.value = e?.response?.status === 403 ? 'Access denied.' : 'Failed to create device.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const canSubmitUser = computed(() => {
|
||||
return (
|
||||
user_form.username.trim().length >= 3 &&
|
||||
user_form.password.length >= 8 && // basic client-side check; real policy enforced server-side
|
||||
!userSubmitting.value
|
||||
)
|
||||
})
|
||||
|
||||
async function submitUser() {
|
||||
userError.value = null
|
||||
userSuccess.value = null
|
||||
|
||||
if (!canSubmitUser.value) {
|
||||
userError.value = 'Username must be at least 3 chars; password at least 8.'
|
||||
return
|
||||
}
|
||||
|
||||
const payload: CreateUserPayload = {
|
||||
username: user_form.username.trim(),
|
||||
password: user_form.password, // do not trim passwords
|
||||
role: userRole.value,
|
||||
}
|
||||
|
||||
userSubmitting.value = true
|
||||
try {
|
||||
await api.post('/users/create', payload)
|
||||
userSuccess.value = 'User created.'
|
||||
router.replace('/admin')
|
||||
} catch (e: any) {
|
||||
userError.value = e?.response?.status === 403 ? 'Access denied.' : 'Failed to create user.'
|
||||
} finally {
|
||||
userSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Navbar>
|
||||
<div class="w-full py-8">
|
||||
<div class="mx-auto">
|
||||
<!-- Horizontal cards with gap -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-stretch">
|
||||
<!-- Create device -->
|
||||
<Card class="h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>Create device</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="flex-1">
|
||||
<!-- add vertical spacing between rows -->
|
||||
<div class="grid gap-5">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="guid" class="text-right">GUID</Label>
|
||||
<Input id="guid" class="col-span-3 w-full" v-model="device_form.guid" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="name" class="text-right">Name</Label>
|
||||
<Input id="name" class="col-span-3 w-full" v-model="device_form.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="users" class="text-right">Allowed users</Label>
|
||||
<!-- make the component span and fill -->
|
||||
<AssignDevice id="users" class="col-span-3 w-full" v-model="selectedUserIds" />
|
||||
</div>
|
||||
<p v-if="errorMsg" class="text-sm text-red-600" aria-live="assertive">
|
||||
{{ errorMsg }}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<Button :disabled="!canSubmit" @click="submitDevice">
|
||||
{{ submitting ? 'Saving…' : 'Save' }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<!-- Create user -->
|
||||
<Card class="h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>Create user</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="flex-1">
|
||||
<div class="grid gap-5">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="username" class="text-right">Username</Label>
|
||||
<Input id="username" class="col-span-3 w-full" v-model="user_form.username" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="password" class="text-right">Password</Label>
|
||||
<Input id="password" type="password" class="col-span-3 w-full"
|
||||
v-model="user_form.password" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="isAdmin" class="text-right">Make admin</Label>
|
||||
<div class="col-span-3">
|
||||
<Switch id="isAdmin" v-model:checked="user_form.isAdmin"
|
||||
v-model="user_form.isAdmin"
|
||||
@update:checked="(v: any) => user_form.isAdmin = !!v"
|
||||
@update:modelValue="(v: any) => user_form.isAdmin = !!v"/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="userError" class="text-sm text-red-600" aria-live="assertive">
|
||||
{{ userError }}
|
||||
</p>
|
||||
<p v-if="userSuccess" class="text-sm text-green-600" aria-live="polite">
|
||||
{{ userSuccess }}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<Button :disabled="!canSubmitUser" @click="submitUser">
|
||||
{{ userSubmitting ? 'Saving…' : 'Save changes' }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Navbar>
|
||||
</template>
|
||||
13
management-ui/src/pages/DeviceView.vue
Normal file
13
management-ui/src/pages/DeviceView.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import Navbar from '@/customcompometns/Navbar.vue';
|
||||
import DeviceComponent from '@/customcompometns/DeviceComponent.vue';
|
||||
const mode = useColorMode()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Navbar>
|
||||
<DeviceComponent></DeviceComponent>
|
||||
</Navbar>
|
||||
</template>
|
||||
22
management-ui/src/pages/Devices.vue
Normal file
22
management-ui/src/pages/Devices.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import Navbar from '@/customcompometns/Navbar.vue';
|
||||
import DevicesGrid from '@/customcompometns/DevicesGrid.vue';
|
||||
import type { Device } from '@/lib/interfaces';
|
||||
const mode = useColorMode()
|
||||
|
||||
const device_data: Device[] = [
|
||||
{
|
||||
guid: "c78dfff6-d7b1-4da9-b499-d5cf66533ae2",
|
||||
devicename: "1",
|
||||
assigned_users: "admin, lox"
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <Navbar :custom-component="DevicesGrid"/> -->
|
||||
<Navbar>
|
||||
<DevicesGrid :devices="device_data" />
|
||||
</Navbar>
|
||||
</template>
|
||||
12
management-ui/src/pages/Forbidden.vue
Normal file
12
management-ui/src/pages/Forbidden.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
<template>
|
||||
<div class="h-screen w-full flex items-center justify-center px-4">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-2xl font-semibold">403 — Forbidden</h1>
|
||||
<p class="text-muted-foreground">You don’t have permission to view this page.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
82
management-ui/src/pages/Login.vue
Normal file
82
management-ui/src/pages/Login.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { api } from '@/lib/api';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const mode = useColorMode()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const form = ref({ username: '', password: '' })
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
|
||||
async function onSubmit() {
|
||||
errorMsg.value = null
|
||||
if (!form.value.username || !form.value.password) {
|
||||
errorMsg.value = 'Username and password are required.'
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
// POST /auth/signin { username, password } -> { accessToken }
|
||||
const { data } = await api.post<{ accessToken: string }>('/auth/signin', {
|
||||
username: form.value.username.trim(),
|
||||
password: form.value.password
|
||||
})
|
||||
|
||||
if (!data?.accessToken) throw new Error('Missing access token')
|
||||
|
||||
auth.setToken(data.accessToken)
|
||||
|
||||
// Redirect to originally requested route if present
|
||||
const next = (route.query.redirect as string) || '/devices'
|
||||
router.replace(next)
|
||||
} catch (e: any) {
|
||||
// Avoid leaking server error details to the UI
|
||||
errorMsg.value = 'Invalid login or server unavailable.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-screen flex items-center justify-center px-4">
|
||||
<Card class="flex w-full mx-auto max-w-xl">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-2xl">
|
||||
Login
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form class="grid gap-4" @submit.prevent="onSubmit" :aria-busy="loading">
|
||||
<div class="grid gap-2">
|
||||
<Label for="usernmae">User</Label>
|
||||
<Input id="usernmae" required v-model.trim="form.username" autocapitalize="none"
|
||||
spellcheck="false" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center">
|
||||
<Label for="password">Password</Label>
|
||||
</div>
|
||||
<Input id="password" type="password" required v-model="form.password" />
|
||||
</div>
|
||||
<Button type="submit" class="w-full" :disabled="loading">
|
||||
<span v-if="!loading">Login</span>
|
||||
<span v-else>Signing in…</span>
|
||||
</Button>
|
||||
<p v-if="errorMsg" class="text-sm text-red-600" aria-live="assertive">
|
||||
{{ errorMsg }}
|
||||
</p>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
12
management-ui/src/pages/Settings.vue
Normal file
12
management-ui/src/pages/Settings.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import Navbar from '@/customcompometns/Navbar.vue';
|
||||
import UserSettings from '@/customcompometns/UserSettings.vue';
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Navbar>
|
||||
<UserSettings/>
|
||||
</Navbar>
|
||||
</template>
|
||||
Reference in New Issue
Block a user