This commit is contained in:
Douglas Barone 2022-06-08 16:43:45 +00:00
parent fcd61655ce
commit cfab03d1ea
17 changed files with 462 additions and 245 deletions

View File

@ -83,11 +83,11 @@ class User {
const rolesGroups = [
{
role: 'superAdmin',
adGroup: process.env.SUPER_ADMIN_GROUP || 'PP-SERTI'
adGroup: process.env.SUPER_ADMIN_GROUP || 'PP-PTI-Admins'
},
{
role: 'tokenCreator',
adGroup: process.env.TOKEN_CREATOR_GROUP || 'PTI-TokenGen'
adGroup: process.env.TOKEN_CREATOR_GROUP || 'PP-PTI-TokenCreators'
},
{
role: 'student',
@ -99,7 +99,7 @@ class User {
},
{
role: 'watcher',
adGroup: process.env.WATCHER_GROUP || 'PTI-Vigias'
adGroup: process.env.WATCHER_GROUP || 'PP-PTI-Watchers'
}
]
@ -392,9 +392,8 @@ class User {
})
logSuccess({
message: `Importado ${index + 1}/${allAdUsers.length} (${
user.sAMAccountName
}) ${user.displayName}`,
message: `Importado ${index + 1}/${allAdUsers.length} (${user.sAMAccountName
}) ${user.displayName}`,
data: dbUser
})
}
@ -403,12 +402,11 @@ class User {
logSuccess({
tags: ['user', 'AD'],
message: `${
allAdUsers.length
} usuários importados do Active Directory em ${(
(endTime - startTime) /
1000
).toFixed(2)}s`
message: `${allAdUsers.length
} usuários importados do Active Directory em ${(
(endTime - startTime) /
1000
).toFixed(2)}s`
})
return allAdUsers.length

View File

@ -34,6 +34,7 @@ export async function wifiUsers(parent, { take = 10, skip = 0, search }) {
]
}
return {
data:
prisma.user.findMany({

View File

@ -60,7 +60,26 @@ const User = {
const campus = parent.extensionAttribute1?.split('-')[0]
return campus || '--'
}
},
onlineWifiDevicesCount: (parent, data, { auth }) => prisma.wifiDevice.count({
where: {
status: 'ONLINE',
user: { id: parent.id }
}
}),
offlineWifiDevicesCount: (parent, data, { auth }) => prisma.wifiDevice.count({
where: {
OR: [
{ status: 'OFFLINE' },
{ status: 'RECENT' },
],
user: { id: parent.id }
}
})
}
export { User }

View File

@ -121,6 +121,10 @@ const typeDefs = gql`
type User {
id: ID
wifiDevices: [WifiDevice!]
onlineWifiDevicesCount: Int!
offlineWifiDevicesCount: Int!
lastLogin: String
lastLoginPrior: String
roles: [String!]

View File

@ -50,7 +50,7 @@
</template>
<script>
import SignalStrength from '../signalStrength.vue'
import SignalStrength from '../ui/signalStrength.vue'
import UserTD from './UserTD.vue'
import Bytes from '../ui/Bytes.vue'

View File

@ -1,5 +1,12 @@
<template>
<div v-if="user" class="d-flex">
<router-link
v-if="user"
class="d-flex text-decoration-none"
:to="{
name: 'wifi-user-single',
params: { sAMAccountName: user.sAMAccountName }
}"
>
<Avatar
class="flex-column center-block"
left
@ -10,7 +17,7 @@
{{ user.displayName }} <br />
{{ user.sAMAccountName }}
</small>
</div>
</router-link>
<span v-else>
<Avatar left size="28" no-auth /><small>Não autenticado</small>
</span>
@ -23,7 +30,8 @@ export default {
components: { Avatar },
props: {
user: {
type: Object
type: Object,
default: () => ({})
}
}
}

View File

@ -70,7 +70,7 @@
</template>
<script>
import SignalStrength from '../signalStrength.vue'
import SignalStrength from '../ui/signalStrength.vue'
import Bytes from '../ui/Bytes.vue'
export default {

View File

@ -74,7 +74,7 @@
</template>
<script>
import SignalStrength from '../signalStrength.vue'
import SignalStrength from '../ui/signalStrength.vue'
import UserTD from './UserTD.vue'
import Bytes from '../ui/Bytes.vue'

View File

@ -0,0 +1,45 @@
<template>
<v-data-table :items="items" :headers="headers">
<template #[`item.combinedUser`]="{ item }">
<UserTD :user="item" />
</template>
</v-data-table>
</template>
<script>
import UserTD from './UserTD.vue'
export default {
components: { UserTD },
props: {
wifiUsers: {
type: Array,
default: () => []
}
},
data: () => ({
headers: [
{ text: 'Usuário', value: 'combinedUser', sortable: false },
{
text: 'Dispositivos Online',
value: 'onlineWifiDevicesCount',
sortable: false
},
{
text: 'Dispositivos Offline',
value: 'offlineWifiDevicesCount',
sortable: false
}
]
}),
computed: {
items() {
return this.wifiUsers.map(wifiUser => ({
...wifiUser,
combinedUser: `${wifiUser.displayName} (${wifiUser.sAMAccountName})`
}))
}
}
}
</script>
<style></style>

View File

@ -0,0 +1,46 @@
<template>
<div>
<v-icon v-if="showIcon" left>mdi-file-tree-outline</v-icon>
<span
v-for="(item, index) in items"
:key="index"
class="path text--secondary"
>
{{ item.text }}
</span>
</div>
</template>
<script>
export default {
props: {
dn: {
type: String,
required: true
},
showIcon: {
type: Boolean,
default: false
}
},
computed: {
items() {
const dirs = this.dn.split(',OU=IFMS,DC=ifms')[0].split(',').reverse()
return dirs.map(dir => ({
text: dir.split('=')[1]
}))
}
}
}
</script>
<style>
.path {
font-size: 0.7em;
}
.path::after {
content: '/';
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<v-chip label :color="color + ' darken-4'" small dark outlined>
{{ text }}</v-chip
>
</template>
<script>
export default {
props: {
role: {
type: String,
required: true
}
},
data: () => ({
roles: [
{
text: 'Administrador',
value: 'superAdmin',
color: 'error'
},
{
text: 'Criador de token',
value: 'tokenCreator',
color: 'warning'
},
{
text: 'Servidor',
value: 'servant',
color: 'success'
},
{
text: 'Aluno',
value: 'student',
color: 'teal'
},
{
text: 'Localizador',
value: 'watcher',
color: 'amber'
}
]
}),
computed: {
text() {
const text = this.roles.find(role => role.value == this.role).text
return text ? text : this.role
},
color() {
const color = this.roles.find(role => role.value == this.role).color
return color ? color : 'grey'
}
}
}
</script>
<style></style>

View File

@ -55,7 +55,8 @@ export default {
return 'mdi-signal-cellular-outline'
},
signalStrengthColor(signalStrength) {
if (signalStrength > -50) return 'green'
if (signalStrength > -30) return 'teal'
else if (signalStrength > -50) return 'green'
else if (signalStrength > -60) return 'light-green'
else if (signalStrength > -67) return 'lime'
else if (signalStrength > -70) return 'amber'
@ -64,7 +65,8 @@ export default {
else return 'red'
},
signalStrengthText(signalStrength) {
if (signalStrength > -50) return 'Excelente'
if (signalStrength > -30) return 'Excelente'
else if (signalStrength > -50) return 'Muito bom'
else if (signalStrength > -60) return 'Bom'
else if (signalStrength > -67) return 'Regular'
else if (signalStrength > -70) return 'Fraco'

View File

@ -228,7 +228,16 @@ const routes = [
roles: ['superAdmin']
},
component: () =>
import(/* webpackChunkName: "wifi-users" */ '../views/WifiUsers.vue')
import(/* webpackChunkName: "wifi-users" */ '../views/WifiUsers')
},
{
path: '/wifi-users/:sAMAccountName/',
name: 'wifi-user-single',
component: () =>
import(
/* webpackChunkName: "wifi-user-single" */ '../views/WifiUsers/single.vue'
)
},
{

View File

@ -0,0 +1,9 @@
<template>
<v-container> </v-container>
</template>
<script>
export default {}
</script>
<style></style>

View File

@ -1,223 +0,0 @@
<template>
<v-container fluid>
<v-toolbar flat outlined>
<v-text-field
v-model="search"
label="Pesquisar"
prepend-icon="mdi-devices"
clearable
hide-details
:loading="$apollo.queries.wifiUsers.loading && !!search"
/>
<v-spacer />
<v-select
v-model="itemsPerPage"
class="shrink"
:items="[5, 10, 20, 30, 50, 100]"
label="Items por página"
hide-details
outlined
dense
/>
<v-tooltip bottom>
<span>Atualizar</span>
<template #activator="{ on }">
<v-btn
class="ml-2"
:loading="$apollo.queries.wifiUsers.loading"
icon
large
v-on="on"
@click="$apollo.queries.wifiUsers.refetch()"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</template>
</v-tooltip>
</v-toolbar>
<div v-if="stats" class="text-center my-4">
<v-chip color="primary" class="mr-2" dark>
{{ stats.onlineUsers }}
</v-chip>
usuários online de
<v-chip class="ml-2">
{{ stats.totalUsers }}
</v-chip>
</div>
<div v-else class="text-center my-4">
<v-chip color="primary" class="mr-2" dark>
<v-progress-circular indeterminate size="12" width="1" />
</v-chip>
usuários online de
<v-chip class="ml-2">
<v-progress-circular indeterminate size="12" width="1" />
</v-chip>
</div>
<v-data-iterator
:loading="$apollo.queries.wifiUsers.loading"
:items="sortedWifiUsers"
hide-default-footer
:items-per-page="itemsPerPage"
>
<template #loading>
<v-expansion-panels disabled>
<v-expansion-panel v-for="i in 10" :key="i">
<v-expansion-panel-header class="pa-1">
<v-skeleton-loader class="ma-0 pa-0" type="list-item-avatar" />
</v-expansion-panel-header>
</v-expansion-panel>
</v-expansion-panels>
</template>
<template #default="{ items }">
<v-expansion-panels>
<v-expansion-panel v-for="user in items" :key="user.sAMAccountName">
<v-expansion-panel-header>
<div class="ml-1">
<v-badge color="grey darken-1" bottom left offset-y="12">
<template #badge>
<small>
{{
user.wifiDevices
.filter(wifiDevice => wifiDevice.status != 'ONLINE')
.length.toString()
}}
</small>
</template>
<v-badge color="green darken-1" top left offset-y="12">
<template #badge>
<small>
{{
user.wifiDevices
.filter(wifiDevice => wifiDevice.status == 'ONLINE')
.length.toString()
}}
</small>
</template>
<Avatar left size="32" :src="user.thumbnailPhoto" />
</v-badge>
</v-badge>
{{ user.displayName }} ({{ user.sAMAccountName }})
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-expansion-panels accordion>
<UserWifiDevicesDataTable
class="grow"
:wifi-devices="user.wifiDevices"
/>
</v-expansion-panels>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</template>
</v-data-iterator>
<v-pagination
v-model="page"
class="my-4"
:length="pagesTotal"
:total-visible="7"
/>
</v-container>
</template>
<script>
import gql from 'graphql-tag'
import Avatar from '../components/Avatar.vue'
import UserWifiDevicesDataTable from '../components/DataTables/UserWifiDevicesDataTable.vue'
export default {
components: { Avatar, UserWifiDevicesDataTable },
data: () => ({
search: '',
page: 1,
itemsPerPage: 10
}),
computed: {
sortedWifiUsers() {
return this.wifiUsers?.data?.map(user => ({
...user,
ips: user.wifiDevices.reduce((ips, device) => ` ${device.ip}`, ''),
wifiDevices: user.wifiDevices.sort(a => (a.status == 'ONLINE' ? -1 : 1))
}))
},
pagesTotal() {
if (this.wifiUsers?.total)
return Math.ceil(this.wifiUsers.total / this.itemsPerPage)
else return 1
}
},
watch: {
search(newValue) {
if (!newValue) this.$router.push({ query: {} })
else if (this.$route.query.search != newValue)
this.$router.push({ query: { search: newValue } })
}
},
mounted() {
this.search = this.$route.query.search || ''
},
apollo: {
wifiUsers: {
fetchPolicy: 'cache-and-network',
query: gql`
query wifiUsers($search: String, $skip: Int, $take: Int) {
wifiUsers(search: $search, skip: $skip, take: $take) {
total
data {
displayName
sAMAccountName
thumbnailPhoto
wifiDevices {
mac
oui
controller
hostname
firstSeen
lastSeen
status
apName
essid
ip
uptime
signalStrength
frequency
protocol
speed
usage
accessPoint {
id
name
hostname
local
}
}
}
}
}
`,
variables() {
return {
search: this.search,
skip: this.page * this.itemsPerPage - this.itemsPerPage,
take: this.itemsPerPage
}
}
},
stats: {
query: gql`
query {
stats {
onlineUsers
totalUsers
}
}
`
}
}
}
</script>

View File

@ -0,0 +1,141 @@
<template>
<v-container fluid>
<v-toolbar flat outlined>
<v-text-field
v-model="search"
label="Pesquisar"
prepend-icon="mdi-devices"
clearable
hide-details
:loading="$apollo.queries.wifiUsers.loading && !!search"
/>
<v-spacer />
<v-select
v-model="itemsPerPage"
class="shrink"
:items="[5, 10, 20, 30, 50, 100]"
label="Items por página"
hide-details
outlined
dense
/>
<v-tooltip bottom>
<span>Atualizar</span>
<template #activator="{ on }">
<v-btn
class="ml-2"
:loading="$apollo.queries.wifiUsers.loading"
icon
large
v-on="on"
@click="$apollo.queries.wifiUsers.refetch()"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</template>
</v-tooltip>
</v-toolbar>
<div v-if="stats" class="text-center my-4">
<v-chip color="primary" class="mr-2" dark>
{{ stats.onlineUsers }}
</v-chip>
usuários online de
<v-chip class="ml-2">
{{ stats.totalUsers }}
</v-chip>
</div>
<div v-else class="text-center my-4">
<v-chip color="primary" class="mr-2" dark>
<v-progress-circular indeterminate size="12" width="1" />
</v-chip>
usuários online de
<v-chip class="ml-2">
<v-progress-circular indeterminate size="12" width="1" />
</v-chip>
</div>
<WifiUsersDataTable v-if="wifiUsers" :wifi-users="wifiUsers.data" />
<v-pagination
v-model="page"
class="my-4"
:length="pagesTotal"
:total-visible="7"
/>
</v-container>
</template>
<script>
import gql from 'graphql-tag'
import WifiUsersDataTable from '../../components/DataTables/WifiUsersDataTable.vue'
export default {
components: { WifiUsersDataTable },
data: () => ({
search: '',
page: 1,
itemsPerPage: 10
}),
computed: {
items() {
return this.wifiUsers.data.map(wifiUser => ({
...wifiUser,
combinedUser: `${wifiUser.displayName} (${wifiUser.sAMAccountName})`
}))
},
pagesTotal() {
if (this.wifiUsers?.total)
return Math.ceil(this.wifiUsers.total / this.itemsPerPage)
else return 1
}
},
watch: {
search(newValue) {
if (!newValue) this.$router.push({ query: {} })
else if (this.$route.query.search != newValue)
this.$router.push({ query: { search: newValue } })
}
},
mounted() {
this.search = this.$route.query.search || ''
},
apollo: {
wifiUsers: {
fetchPolicy: 'cache-and-network',
query: gql`
query wifiUsers($search: String, $skip: Int, $take: Int) {
wifiUsers(search: $search, skip: $skip, take: $take) {
total
data {
displayName
sAMAccountName
thumbnailPhoto
onlineWifiDevicesCount
offlineWifiDevicesCount
}
}
}
`,
variables() {
return {
search: this.search,
skip: this.page * this.itemsPerPage - this.itemsPerPage,
take: this.itemsPerPage
}
}
},
stats: {
query: gql`
query {
stats {
onlineUsers
totalUsers
}
}
`
}
}
}
</script>

View File

@ -0,0 +1,99 @@
<template>
<div>
<v-btn class="ma-2" icon @click="$router.back()">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-container v-if="user">
<v-card class="mb-2">
<v-card-title class="font-weight-regular">
<Avatar :src="user.thumbnailPhoto" size="56" left />
<div>
{{ user.displayName }}
<DistinguishedNameBreadcrumb :dn="user.dn" />
<RoleBadge
v-for="role in user.roles"
:key="role"
class="mr-1"
:role="role"
/>
</div>
<v-spacer />
</v-card-title>
<v-divider />
<v-card-text>
<UserWifiDevicesDataTable :wifi-devices="user.wifiDevices" />
</v-card-text>
</v-card>
</v-container>
<v-container v-else indeterminate>
<v-progress-linear indeterminate />
<v-skeleton-loader class="my-4" type="card-heading, card" />
</v-container>
</div>
</template>
<script>
import gql from 'graphql-tag'
import UserWifiDevicesDataTable from '../../components/DataTables/UserWifiDevicesDataTable.vue'
import Avatar from '../../components/Avatar.vue'
import DistinguishedNameBreadcrumb from '../../components/ui/DistinguishedNameBreadcrumb.vue'
import RoleBadge from '../../components/ui/RoleBadge.vue'
export default {
components: {
UserWifiDevicesDataTable,
Avatar,
DistinguishedNameBreadcrumb,
RoleBadge
},
apollo: {
user: {
query: gql`
query user($sAMAccountName: String!) {
user(sAMAccountName: $sAMAccountName) {
displayName
sAMAccountName
thumbnailPhoto
dn
roles
wifiDevices {
mac
oui
controller
hostname
firstSeen
lastSeen
status
apName
essid
ip
uptime
signalStrength
frequency
protocol
speed
usage
accessPoint {
id
name
hostname
local
}
}
}
}
`,
variables() {
return {
sAMAccountName: this.$route.params.sAMAccountName
}
}
}
}
}
</script>
<style></style>