WIP: Update to device components

This commit is contained in:
Jesse Malotaux 2025-03-24 20:22:49 +01:00
parent 3b38372b4b
commit 6872269266
8 changed files with 203 additions and 56 deletions

37
fe/package-lock.json generated
View file

@ -14,6 +14,7 @@
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@basitcodeenv/vue3-device-detect": "^1.0.3",
"@eslint/js": "^9.20.0", "@eslint/js": "^9.20.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
@ -504,6 +505,16 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@basitcodeenv/vue3-device-detect": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@basitcodeenv/vue3-device-detect/-/vue3-device-detect-1.0.3.tgz",
"integrity": "sha512-BDHoM2KYJx/PrEyEzgYOLEVSTxQmvd1PR7UqRQSMClgnM7O1cGoagG/5K0CZy6LUHGifyU5qnrQdC3Sy9nt3zQ==",
"dev": true,
"dependencies": {
"ua-parser-js": "^1.0.37",
"vue": "^3.0.0-0"
}
},
"node_modules/@bufbuild/protobuf": { "node_modules/@bufbuild/protobuf": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz",
@ -4603,6 +4614,32 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/ua-parser-js": {
"version": "1.0.40",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
"integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
},
"node_modules/unicorn-magic": { "node_modules/unicorn-magic": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",

View file

@ -17,6 +17,7 @@
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@basitcodeenv/vue3-device-detect": "^1.0.3",
"@eslint/js": "^9.20.0", "@eslint/js": "^9.20.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",

View file

@ -34,7 +34,9 @@
.panel__content { .panel__content {
@apply grid @apply grid
h-full
pt-4 sm:pt-0 pt-4 sm:pt-0
pl-0 sm:pl-4; pl-0 sm:pl-4
overflow-auto;
} }
} }

View file

@ -25,8 +25,9 @@ defineProps({
@reference "@/assets/main.css"; @reference "@/assets/main.css";
.alert { .alert {
@apply flex @apply grid
items-center grid-cols-[1rem_1fr]
items-start
gap-4 gap-4
p-4 p-4
border border
@ -36,7 +37,7 @@ defineProps({
backdrop-blur-md; backdrop-blur-md;
&.alert__info { &.alert__info {
@apply text-sky-400 bg-sky-400/10; @apply text-sky-100 bg-sky-400/40;
} }
&.alert__success { &.alert__success {

View file

@ -21,6 +21,17 @@
Navigate to <em class="font-semibold">http://localhost:6970/devices</em> on your pc to Navigate to <em class="font-semibold">http://localhost:6970/devices</em> on your pc to
authorize. authorize.
</p> </p>
<template v-if="server.link == 'checking'">
<div class="grid grid-cols-[2rem_1fr] gap-2">
<IconReload class="animate-spin" />
Checking server for link...
</div>
</template>
<template v-if="server.link === false">
<ButtonComp variant="subtle" @click="pingLink()" class="w-fit">
<IconReload />Check for server link
</ButtonComp>
</template>
</div> </div>
</AlertComp> </AlertComp>
@ -37,22 +48,37 @@
Disconnect Disconnect
</ButtonComp> </ButtonComp>
</div> </div>
<DialogComp ref="linkPinDialog">
<template #content>
<div class="grid gap-4">
<h3>Enter server link pin:</h3>
<input class="input" type="number" v-model="server.linkPin" />
<ButtonComp variant="primary" @click="getDeviceKey()">Enter</ButtonComp>
</div>
</template>
</DialogComp>
</div> </div>
</template> </template>
<script setup> <script setup>
import { IconKey, IconPlugConnectedX, IconServer } from '@tabler/icons-vue' import { IconKey, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
import AlertComp from '../base/AlertComp.vue' import AlertComp from '../base/AlertComp.vue'
import ButtonComp from '../base/ButtonComp.vue' import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, reactive } from 'vue' import { onMounted, reactive, ref } from 'vue'
import { useDeviceStore } from '@/stores/device' import { useDeviceStore } from '@/stores/device'
import { deviceType, deviceModel, deviceVendor } from '@basitcodeenv/vue3-device-detect'
import DialogComp from '../base/DialogComp.vue'
const device = useDeviceStore() const device = useDeviceStore()
const linkPinDialog = ref()
const server = reactive({ const server = reactive({
host: '', host: '',
status: false, status: false,
access: false, access: false,
link: false,
linkPin: '',
}) })
onMounted(async () => { onMounted(async () => {
@ -62,20 +88,29 @@ onMounted(async () => {
if (Object.keys(state.server).length) server.status = state.server.status if (Object.keys(state.server).length) server.status = state.server.status
}) })
const status = await device.checkServerAccess() const status = await device.remoteCheckServerAccess()
server.status = status server.status = status
}) })
function requestAccess() { function requestAccess() {
const request = device.requestServerAccess() let deviceName = `${deviceVendor() ? deviceVendor() : 'Unknown'} ${deviceVendor() ? deviceModel() : deviceType()}`
request device.remoteRequestServerAccess(deviceName, deviceType()).then((data) => {
.then((data) => { if (data.data) pingLink()
console.log('1', data) })
}) }
.then((data) => {
console.log('2', data) function pingLink() {
}) server.link = 'checking'
device.remotePingLink((link) => {
server.link = true
linkPinDialog.value.toggleDialog(true)
console.log(link, 'opendialog')
})
}
function getDeviceKey() {
device.remoteHandshake(server.linkPin)
} }
</script> </script>

View file

@ -8,60 +8,97 @@
</AlertComp> </AlertComp>
<div class="mcrm-block block__light flex flex-wrap items-start"> <div class="mcrm-block block__light flex flex-wrap items-start">
<!-- {{ remote.devices }} --> <!-- {{ Object.keys(remote.devices).length }} -->
<div <template v-if="Object.keys(remote.devices).length > 0">
class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start" <div
v-for="(device, id) in remote.devices" class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start"
:key="id" v-for="(remoteDevice, id) in remote.devices"
> :key="id"
<div class="grid gap-2"> >
<h5 class="grid grid-cols-[auto_1fr] gap-2"> <div class="grid gap-2">
<IconDeviceUnknown v-if="device.settings.type == 'unknown'" /> <h5 class="grid grid-cols-[auto_1fr] gap-2">
<IconDeviceMobile v-if="device.settings.type == 'phone'" /> <IconDeviceUnknown v-if="remoteDevice.settings.type == 'unknown'" />
<IconDeviceTablet v-if="device.settings.type == 'tablet'" /> <IconDeviceMobile v-if="remoteDevice.settings.type == 'mobile'" />
<span class="w-full truncate"> <IconDeviceTablet v-if="remoteDevice.settings.type == 'tablet'" />
{{ device.settings.name }} <IconDeviceDesktop v-if="remoteDevice.settings.type == 'desktop'" />
</span> <span class="w-full truncate">
</h5> {{ remoteDevice.settings.name }}
<em>{{ id }}</em> </span>
</h5>
<em>{{ id }}</em>
</div>
<template v-if="remoteDevice.key">
<AlertComp type="success">Linked</AlertComp>
<ButtonComp variant="danger"> <IconLinkOff />Unlink device </ButtonComp>
</template>
<template v-else>
<AlertComp type="warning">Not linked</AlertComp>
<ButtonComp variant="primary" @click="startLink(id)">
<IconLink />Link device
</ButtonComp>
</template>
</div> </div>
<template v-if="device.key"> </template>
<AlertComp type="success">Registered</AlertComp> <template v-else>
<ButtonComp variant="danger"> <IconDevicesMinus />Unregister device </ButtonComp> <div class="grid w-full gap-4">
<em class="text-slate-300">No remote devices</em>
<div class="flex justify-end">
<ButtonComp variant="primary" @click="device.serverGetRemotes()">
<IconReload />Check for access requests
</ButtonComp>
</div>
</div>
</template>
<DialogComp ref="pinDialog">
<template #content>
<div class="grid gap-4">
<h3>Pin code</h3>
<span class="text-4xl font-mono tracking-wide">{{ remote.pinlink }}</span>
</div>
</template> </template>
<template v-else> </DialogComp>
<AlertComp type="warning">Not registered</AlertComp>
<ButtonComp variant="primary"> <IconDevicesPlus />Register device </ButtonComp>
</template>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, reactive } from 'vue' import { onMounted, reactive, ref } from 'vue'
import AlertComp from '../base/AlertComp.vue' import AlertComp from '../base/AlertComp.vue'
import { useDeviceStore } from '@/stores/device' import { useDeviceStore } from '@/stores/device'
import { import {
IconDeviceDesktop,
IconDeviceMobile, IconDeviceMobile,
IconDevicesMinus, IconDevicesMinus,
IconDevicesPlus, IconDevicesPlus,
IconDeviceTablet, IconDeviceTablet,
IconDeviceUnknown, IconDeviceUnknown,
IconLink,
IconLinkOff,
IconReload,
} from '@tabler/icons-vue' } from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue' import ButtonComp from '../base/ButtonComp.vue'
import DialogComp from '../base/DialogComp.vue'
const device = useDeviceStore() const device = useDeviceStore()
const remote = reactive({ devices: [] }) const pinDialog = ref()
const remote = reactive({ devices: [], pinlink: '' })
onMounted(() => { onMounted(() => {
device.getRemoteDevices() device.serverGetRemotes()
device.$subscribe((mutation, state) => { device.$subscribe((mutation, state) => {
if (Object.keys(state.remote).length) remote.devices = device.remote if (Object.keys(state.remote).length) remote.devices = device.remote
}) })
}) })
async function startLink(deviceUuid) {
const pin = await device.serverStartLink(deviceUuid)
remote.pinlink = pin
pinDialog.value.toggleDialog(true)
}
</script> </script>
<style scoped> <style scoped>

View file

@ -1,7 +1,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import axios from 'axios' import axios from 'axios'
import { appUrl } from '@/services/ApiService' import { appUrl, encrypt } from '@/services/ApiService'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
export const useDeviceStore = defineStore('device', () => { export const useDeviceStore = defineStore('device', () => {
@ -15,6 +15,7 @@ export const useDeviceStore = defineStore('device', () => {
status: false, status: false,
}) })
// Current device
const uuid = () => { const uuid = () => {
if (!current.value.uuid && localStorage.getItem('deviceId')) { if (!current.value.uuid && localStorage.getItem('deviceId')) {
current.value.uuid = localStorage.getItem('deviceId') current.value.uuid = localStorage.getItem('deviceId')
@ -30,32 +31,65 @@ export const useDeviceStore = defineStore('device', () => {
return uuid return uuid
} }
const getRemoteDevices = async () => { // Server application
// remote.value = { penis: 'penis' } const serverGetRemotes = async (remoteUuid) => {
// return axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => {
axios.post(appUrl() + '/device/list').then((data) => {
if (data.data.devices) remote.value = data.data.devices if (data.data.devices) remote.value = data.data.devices
}) })
} }
const checkServerAccess = async () => { const serverStartLink = async (deviceUuid) => {
const request = await axios.post(appUrl() + '/device/link/start', { uuid: deviceUuid })
return request.data
}
// Remote application
const remoteCheckServerAccess = async () => {
const check = await axios.post(appUrl() + '/device/access/check', { uuid: uuid() }) const check = await axios.post(appUrl() + '/device/access/check', { uuid: uuid() })
server.value.access = check.data server.value.access = check.data
return check.data return check.data
} }
const requestServerAccess = async () => { const remoteRequestServerAccess = async (deviceName, deviceType) => {
const request = await axios.post(appUrl() + '/device/access/request', { uuid: uuid() }) const request = await axios.post(appUrl() + '/device/access/request', {
uuid: uuid(),
name: deviceName,
type: deviceType,
})
return request return request
} }
const remotePingLink = async (cb) => {
// const linkRequest = await axios.post(appUrl() + '/device/link/ping', { uuid: deviceUuid })
// if (linkRequest.data)
const pingInterval = setInterval(() => {
axios.post(appUrl() + '/device/link/ping', { uuid: uuid() }).then((data) => {
if (data.data) {
clearInterval(pingInterval)
cb(data.data)
}
})
}, 1000)
}
const remoteHandshake = async (pin) => {
// send encrypt(uuid + pin)
// then decrypt data with pin = key
axios.post(appUrl() + '/device/handshake', { shake: pin }).then((data) => {
console.log(data)
})
}
return { return {
remote, remote,
server, server,
uuid, uuid,
setDeviceId, setDeviceId,
getRemoteDevices, serverGetRemotes,
checkServerAccess, serverStartLink,
requestServerAccess, remoteCheckServerAccess,
remoteRequestServerAccess,
remotePingLink,
remoteHandshake,
} }
}) })

View file

@ -3,9 +3,9 @@
<h1 class="panel__title"> <h1 class="panel__title">
Devices <span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span> Devices <span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span>
</h1> </h1>
<div class="panel__content"> <div class="panel__content grid sm:grid-cols-2 gap-8">
<ServerView v-if="!isLocal()" /> <ServerView />
<RemoteView v-else /> <RemoteView />
</div> </div>
</div> </div>
</template> </template>