User presence improvements

This commit is contained in:
Douglas Barone 2020-11-19 11:23:31 -04:00
parent bfa7ec4573
commit 10a990c5e3
17 changed files with 348 additions and 64 deletions

View File

@ -0,0 +1,68 @@
# Migration `20201119134248-add-controller-to-wifi-device`
This migration has been generated by Douglas Barone at 11/19/2020, 9:42:48 AM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
ALTER TABLE "WifiDevice" ADD COLUMN "controller" TEXT NOT NULL DEFAULT E'unknown'
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201110194349-init..20201119134248-add-controller-to-wifi-device
--- datamodel.dml
+++ datamodel.dml
@@ -3,9 +3,9 @@
}
datasource db {
provider = "postgresql"
- url = "***"
+ url = "***"
}
model ResetToken {
id Int @id @default(autoincrement())
@@ -71,23 +71,24 @@
WifiDevice WifiDevice[] @relation("wifidevice_to_user")
}
model WifiDevice {
- id Int @id @default(autoincrement())
- oui String?
- mac String @unique
- hostname String?
- firstSeen DateTime?
- lastSeen DateTime?
- essid String?
- ip String?
- uptime String?
- apName String?
- status Status?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- userId Int?
- user User? @relation(fields: [userId], references: [id], name: "wifidevice_to_user")
+ id Int @id @default(autoincrement())
+ mac String @unique
+ controller String @default("unknown")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ oui String?
+ hostname String?
+ firstSeen DateTime?
+ lastSeen DateTime?
+ essid String?
+ ip String?
+ uptime String?
+ apName String?
+ status Status?
+ userId Int?
+ user User? @relation(fields: [userId], references: [id], name: "wifidevice_to_user")
}
enum Status {
ONLINE
```

View File

@ -0,0 +1,96 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = "***"
}
model ResetToken {
id Int @id @default(autoincrement())
token String @unique
expiration DateTime
usedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], name: "resettoken_to_user")
userId Int
creator User @relation(fields: [creatorId], references: [id], name: "resettoken_to_creator")
creatorId Int
}
model User {
id Int @id @default(autoincrement())
lastLogin DateTime?
lastLoginPrior DateTime?
roles Json?
groups Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accountExpires String?
badPasswordTime String?
badPwdCount String?
cn String?
department String?
description String?
displayName String?
distinguishedName String?
dn String?
extensionAttribute1 String?
extensionAttribute10 String?
extensionAttribute2 String?
extensionAttribute6 String?
extensionAttribute7 String?
givenName String?
homeDirectory String?
homeDrive String?
lastLogoff String?
lastLogon String?
lastLogonTimestamp String?
lockoutTime String?
logonCount String?
mail String?
name String?
objectCategory String?
objectGUID String?
objectSid String?
primaryGroupID String?
pwdLastSet DateTime?
sAMAccountName String @unique
sAMAccountType String?
sn String?
thumbnailPhoto String?
title String?
userAccountControl String?
userPrincipalName String?
whenChanged String?
whenCreated String?
createdTokens ResetToken[] @relation("resettoken_to_user")
tokens ResetToken[] @relation("resettoken_to_creator")
WifiDevice WifiDevice[] @relation("wifidevice_to_user")
}
model WifiDevice {
id Int @id @default(autoincrement())
mac String @unique
controller String @default("unknown")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
oui String?
hostname String?
firstSeen DateTime?
lastSeen DateTime?
essid String?
ip String?
uptime String?
apName String?
status Status?
userId Int?
user User? @relation(fields: [userId], references: [id], name: "wifidevice_to_user")
}
enum Status {
ONLINE
OFFLINE
}

View File

@ -0,0 +1,37 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateField",
"model": "WifiDevice",
"field": "controller",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "WifiDevice",
"field": "controller"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "WifiDevice",
"field": "controller"
},
"directive": "default"
},
"argument": "",
"value": "\"unknown\""
}
]
}

View File

@ -1,3 +1,4 @@
# Prisma Migrate lockfile v1 # Prisma Migrate lockfile v1
20201110194349-init 20201110194349-init
20201119134248-add-controller-to-wifi-device

View File

@ -72,21 +72,22 @@ model User {
} }
model WifiDevice { model WifiDevice {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
oui String? mac String @unique
mac String @unique controller String @default("unknown")
hostname String? createdAt DateTime @default(now())
firstSeen DateTime? updatedAt DateTime @updatedAt
lastSeen DateTime? oui String?
essid String? hostname String?
ip String? firstSeen DateTime?
uptime String? lastSeen DateTime?
apName String? essid String?
status Status? ip String?
createdAt DateTime @default(now()) uptime String?
updatedAt DateTime @updatedAt apName String?
userId Int? status Status?
user User? @relation(fields: [userId], references: [id], name: "wifidevice_to_user") userId Int?
user User? @relation(fields: [userId], references: [id], name: "wifidevice_to_user")
} }
enum Status { enum Status {

View File

@ -106,11 +106,9 @@ const Query = {
}, },
userPresence: async (_, { search }) => { userPresence: async (_, { search }) => {
if (!search) { if (!search) search = ''
//await updateDBWithOnlineDevices()
updateDBWithOnlineDevices() updateDBWithOnlineDevices()
search = ''
}
const usersWithWifiDevices = await prisma.user.findMany({ const usersWithWifiDevices = await prisma.user.findMany({
where: { where: {
@ -123,7 +121,9 @@ const Query = {
.filter( .filter(
user => user =>
user.displayName.toLowerCase().includes(search.toLowerCase()) || user.displayName.toLowerCase().includes(search.toLowerCase()) ||
user.WifiDevice[0].apName.toLowerCase().includes(search.toLowerCase()) user.WifiDevice.some(device =>
device.apName.toLowerCase().includes(search.toLowerCase())
)
) )
.map(user => ({ .map(user => ({
user: { user: {

View File

@ -28,7 +28,7 @@ const typeDefs = gql`
identifiedOnly: Boolean = true identifiedOnly: Boolean = true
): [WifiDevice]! ): [WifiDevice]!
userPresence(search: String): [UserPresence!] @auth(roles: ["watcher"]) userPresence(search: String = ""): [UserPresence!] @auth(roles: ["watcher"])
} }
type Mutation { type Mutation {
@ -140,9 +140,10 @@ const typeDefs = gql`
type WifiDevice { type WifiDevice {
user: User user: User
id: String id: ID!
oui: String oui: String
mac: String mac: String!
controller: String!
hostname: String hostname: String
firstSeen: String firstSeen: String
lastSeen: String lastSeen: String

View File

@ -61,7 +61,8 @@ export async function getOnlineWifiDevices() {
ip: client.IP, ip: client.IP,
uptime: client.UT.toString(), uptime: client.UT.toString(),
apName: client.AP, apName: client.AP,
status: client.ST == 'Online' ? 'ONLINE' : 'OFFLINE' status: client.ST == 'Online' ? 'ONLINE' : 'OFFLINE',
controller: "Cisco"
})) }))
return hydratedOnlineDevices return hydratedOnlineDevices

View File

@ -188,15 +188,14 @@ export async function getOnlineWifiDevices() {
ip: client.ip, ip: client.ip,
uptime: client.uptime.toString(), uptime: client.uptime.toString(),
apName: accessPoints[0].find(ap => ap.mac === client.ap_mac).name, apName: accessPoints[0].find(ap => ap.mac === client.ap_mac).name,
status: 'ONLINE' status: 'ONLINE',
controller: "UniFi"
})) }))
unifiController.logout() await unifiController.logout()
return hydratedOnlineDevices return hydratedOnlineDevices
} catch (e) { } catch (e) {
throw new Error('Error getting devices. ' + e) throw new Error('Error getting devices. ' + e)
} }
} }

View File

@ -13,41 +13,49 @@ async function updateDBWithOnlineDevices() {
const onlineUnifiDevicesPromise = getOnlineUnifiDevices() const onlineUnifiDevicesPromise = getOnlineUnifiDevices()
const onlineCiscoDevicesPromise = getOnlineCiscoDevices() const onlineCiscoDevicesPromise = getOnlineCiscoDevices()
const [onlineUnifiDevices, onlineCiscoDevices] = await Promise.all([ try {
onlineUnifiDevicesPromise, const [onlineUnifiDevices, onlineCiscoDevices] = await Promise.all([
onlineCiscoDevicesPromise onlineUnifiDevicesPromise,
]) onlineCiscoDevicesPromise
])
await prisma.wifiDevice.updateMany({ await prisma.wifiDevice.updateMany({
data: { data: {
status: 'OFFLINE' status: 'OFFLINE'
} }
}) })
const onlineDevices = [...onlineUnifiDevices, ...onlineCiscoDevices] const onlineDevices = [...onlineUnifiDevices, ...onlineCiscoDevices]
for (const onlineDevice of onlineDevices) { for (const onlineDevice of onlineDevices) {
const device = { const device = {
...onlineDevice, ...onlineDevice,
user: onlineDevice.user user: onlineDevice.user
? { connect: { sAMAccountName: onlineDevice.user } } ? { connect: { sAMAccountName: onlineDevice.user } }
: undefined : undefined
}
try {
await prisma.wifiDevice.upsert({
where: { mac: onlineDevice.mac },
create: device,
update: device
})
} catch (e) {
if (e.code != 'P2016') console.log('[wifiDevice upsert error]', e)
}
} }
try { setTimeout(() => {
await prisma.wifiDevice.upsert({ updating = false
where: { mac: onlineDevice.mac }, }, 10000)
create: device,
update: device // TODO: pubsub
}) return onlineDevices.length
} catch (e) { } catch (e) {
if (e.code != 'P2016') console.log('[wifiDevice upsert error]', e) console.log('Error updating DB: ', e)
} return -2
} }
updating = false
// TODO: pubsub
return onlineDevices.length
} }
export { updateDBWithOnlineDevices } export { updateDBWithOnlineDevices }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="logo_1_" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 400 300"
style="enable-background:new 0 0 400 300;" xml:space="preserve">
<style type="text/css">
.st0{fill:#049FD9;}
</style>
<path id="logo" transform="translate(-446.85715,-442.59426)" class="st0" d="M836.9,554.6c0-4.6-3.7-8.2-8.2-8.2
c-4.6,0-8.3,3.7-8.3,8.2v17.3c0,4.6,3.7,8.3,8.3,8.3c4.5,0,8.2-3.7,8.2-8.3V554.6z M791.5,531.8c0-4.5-3.7-8.2-8.3-8.2
c-4.6,0-8.2,3.7-8.2,8.2v40.1c0,4.6,3.7,8.3,8.2,8.3c4.7,0,8.3-3.7,8.3-8.3C791.5,571.9,791.5,531.8,791.5,531.8z M746,500.7
c0-4.6-3.7-8.2-8.2-8.2c-4.6,0-8.2,3.7-8.2,8.2v87.7c0,4.6,3.7,8.3,8.2,8.3c4.6,0,8.2-3.7,8.2-8.3C746,588.4,746,500.7,746,500.7z
M700.6,531.8c0-4.5-3.7-8.2-8.2-8.2c-4.5,0-8.2,3.7-8.2,8.2v40.1c0,4.6,3.7,8.3,8.2,8.3c4.6,0,8.2-3.7,8.2-8.3
C700.6,571.9,700.6,531.8,700.6,531.8z M655.1,554.6c0-4.6-3.7-8.2-8.2-8.2c-4.6,0-8.3,3.7-8.3,8.2v17.3c0,4.6,3.7,8.3,8.3,8.3
c4.5,0,8.2-3.7,8.2-8.3C655.1,571.9,655.1,554.6,655.1,554.6z M609.7,531.8c0-4.5-3.7-8.2-8.3-8.2c-4.6,0-8.3,3.7-8.3,8.2v40.1
c0,4.6,3.7,8.3,8.3,8.3c4.6,0,8.3-3.7,8.3-8.3C609.7,571.9,609.7,531.8,609.7,531.8z M564.3,500.7c0-4.6-3.7-8.2-8.3-8.2
c-4.6,0-8.3,3.7-8.3,8.2v87.7c0,4.6,3.7,8.3,8.3,8.3c4.6,0,8.3-3.7,8.3-8.3C564.3,588.4,564.3,500.7,564.3,500.7z M518.8,531.8
c0-4.5-3.7-8.2-8.3-8.2s-8.3,3.7-8.3,8.2v40.1c0,4.6,3.7,8.3,8.3,8.3s8.3-3.7,8.3-8.3V531.8z M473.4,554.6c0-4.6-3.7-8.2-8.3-8.2
c-4.6,0-8.3,3.7-8.3,8.2v17.3c0,4.6,3.7,8.3,8.3,8.3c4.6,0,8.3-3.7,8.3-8.3L473.4,554.6L473.4,554.6z M644.2,626.4
c-0.5-0.1-8.1-2.1-16.2-2.1c-15.4,0-24.6,8.3-24.6,20.6c0,10.9,7.7,16.4,17,19.3c1,0.3,2.5,0.8,3.6,1.1c4.1,1.3,7.4,3.2,7.4,6.6
c0,3.7-3.8,6.2-12.1,6.2c-7.3,0-14.3-2.1-15.7-2.5v15.2c0.8,0.2,9.1,1.8,18,1.8c12.8,0,27.3-5.6,27.3-22.2c0-8-4.9-15.5-15.7-18.9
l-4.6-1.5c-2.7-0.9-7.6-2.3-7.6-6.3c0-3.2,3.6-5.4,10.3-5.4c5.8,0,12.8,1.9,13,2V626.4L644.2,626.4z M785,658.5
c0,9.6-7.4,17.4-17.2,17.4c-9.9,0-17.2-7.8-17.2-17.4c0-9.6,7.3-17.4,17.2-17.4C777.6,641.2,785,649,785,658.5 M767.8,624.4
c-20.3,0-34.9,15.3-34.9,34.2c0,18.9,14.6,34.2,34.9,34.2c20.3,0,34.9-15.3,34.9-34.2C802.7,639.7,788.1,624.4,767.8,624.4
M541.4,626.7c-1.6-0.5-7.4-2.4-15.2-2.4c-20.3,0-35.2,14.5-35.2,34.2c0,21.3,16.4,34.2,35.2,34.2c7.4,0,13.1-1.8,15.2-2.4v-17.7
c-0.7,0.4-6.2,3.5-14,3.5c-11.1,0-18.3-7.8-18.3-17.6c0-10.1,7.5-17.6,18.3-17.6c8,0,13.3,3.2,14,3.5V626.7z M715.3,626.7
c-1.6-0.5-7.4-2.4-15.2-2.4c-20.3,0-35.2,14.5-35.2,34.2c0,21.3,16.5,34.2,35.2,34.2c7.4,0,13.1-1.8,15.2-2.4v-17.7
c-0.7,0.4-6.2,3.5-14,3.5c-11.1,0-18.2-7.8-18.2-17.6c0-10.1,7.5-17.6,18.2-17.6c8,0,13.3,3.2,14,3.5V626.7z M581,691.6h-16.7v-66
H581V691.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="720" height="1024" viewBox="0 0 720 1024">
<g id="icomoon-ignore">
</g>
<path fill="#4A4A4A" d="M288.4 419c3.2-19.6 10.4-45.6 26.6-68 21.8-29.8 58.2-55.6 129.4-55.4l7.2-33.4c-83.8-2.6-135.8 29.8-163.6 69.2-21.8 30.6-30 64-33.2 87.4l33.6 0.2zM349 419c2.8-9.8 7.2-20.4 14-29.8 12.4-16.8 31.6-31.6 69-34.6l7.2-34c-51.4 1.4-85 22.8-103.2 49-12 16.8-18 34.4-21.2 49.2zM314.4 419h34.6z"></path>
<path fill="#4A4A4A" d="M417.8 418.8l7.4-34c-20.8 1.6-33 11-41.2 23-3 4.2-4.8 7-6.6 11h40.4zM642.2 418.8h-36l7.6-34c0 0.2 27.8 6.4 28.4 34zM81.4 385l-38.4 181c-6.8 31.8 2.8 46 27.2 46 24 0 40-14.2 46.8-46l38.6-181h40.2l-37.8 177.8c-12.4 58.4-46 79.4-94 79.4-48.4 0-73-21.2-60.6-79.4l37.8-177.8h40.2zM211.6 447.4h36.2l-4.6 22.2h1c13.2-17 33.4-27.2 54.6-27.2 29 0 44.6 13.2 36.4 51.6l-30.4 143.6h-38.2l27.6-130.4c5.2-24.8 0-34.4-19.2-34.4-15.6 0-33 12.4-38.2 37.2l-27.2 127.6h-38l40-190.2zM373.8 447.4h38.2l-40.4 190.2h-38.2l40.4-190.2zM455.4 385h128.8l-7.4 34h-88.4l-14.8 70h88.6l-7 34h-88.6l-24.4 114.6h-40.4l53.6-252.6zM600.4 447.4h38.2l-40.4 190.2h-38.2l40.4-190.2z"></path>
<path fill="#4A4A4A" d="M694.2 438c-15.2 0-26-11.4-26-26.4 0-16 12-26.4 26-26.4 13.8 0 25.8 10.6 25.8 26.4 0.2 15.8-11.8 26.4-25.8 26.4zM694.2 389.6c-11.4 0-20.4 9-20.4 21.8 0 12 7.8 21.8 20.4 21.8 11.4 0 20.2-9 20.2-21.8 0.2-12.8-8.8-21.8-20.2-21.8zM689 426.6h-4.8v-29.2h11c7 0 10.6 2.4 10.6 8.4 0 5.4-3.2 7.4-7.6 8l8.2 12.8h-5.4l-7.6-12.6h-4.6l0.2 12.6zM694.4 410c3.6 0 6.8-0.4 6.8-4.6 0-3.6-3.4-4-6.4-4h-5.8v8.6h5.4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,26 @@
<template>
<v-avatar size="28px" class="ml-2">
<v-img contain :src="logo" />
</v-avatar>
</template>
<script>
import ciscoLogo from '../assets/cisco_logo.svg'
import unifiLogo from '../assets/unifi_logo.svg'
export default {
props: {
controller: {
type: String,
default: 'unknown'
}
},
computed: {
logo() {
return {
Cisco: ciscoLogo,
UniFi: unifiLogo
}[this.controller]
}
}
}
</script>

View File

@ -43,6 +43,7 @@
<span class="font-weight-medium" <span class="font-weight-medium"
>{{ userPresence.wifiDevices[0].apName }} >{{ userPresence.wifiDevices[0].apName }}
</span> </span>
<ApIcon :controller="userPresence.wifiDevices[0].controller" />
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
<v-list-item-icon> <v-list-item-icon>
@ -96,9 +97,11 @@
<script> <script>
import Avatar from './Avatar' import Avatar from './Avatar'
import ApIcon from './ApIcon'
export default { export default {
name: 'UserPresenceStatusList', name: 'UserPresenceStatusList',
components: { Avatar }, components: { Avatar, ApIcon },
props: { props: {
userPresences: { userPresences: {
type: Array, type: Array,

View File

@ -5,19 +5,20 @@
<v-text-field <v-text-field
v-model="search" v-model="search"
label="Pesquisar" label="Pesquisar"
hint="Usuário ou Access Point (Ex.: jose, pp, sala 102)"
prepend-inner-icon="mdi-account-search" prepend-inner-icon="mdi-account-search"
outlined outlined
rounded rounded
autofocus
clearable clearable
hide-details
style="max-width: 480px" style="max-width: 480px"
/> />
<v-btn <v-btn
:loading="$apollo.queries.userPresence.loading" :loading="$apollo.queries.userPresence.loading"
color="primary" color="primary"
class="ml-4" class="ml-4 mb-8"
icon icon
large x-large
@click="$apollo.queries.userPresence.refresh()" @click="$apollo.queries.userPresence.refresh()"
> >
<v-icon>mdi-refresh</v-icon> <v-icon>mdi-refresh</v-icon>
@ -71,7 +72,7 @@ export default {
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
pollInterval: 300000, // 5min pollInterval: 300000, // 5min
query: gql` query: gql`
query($search: String) { query($search: String = "") {
userPresence(search: $search) { userPresence(search: $search) {
user { user {
id id
@ -83,6 +84,7 @@ export default {
id id
lastSeen lastSeen
status status
controller
} }
} }
} }