OK... That's a lot of changes...

This commit is contained in:
Douglas Barone 2022-06-14 13:26:01 +00:00
parent 3ad1b6e1c7
commit 69575c69d8
54 changed files with 522 additions and 355 deletions

14
.prettierrc Executable file
View File

@ -0,0 +1,14 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"overrides": [
{
"files": "*.js, *.vue, *.css, *.scss",
"excludeFiles": "**/dist/**, **/node_modules/**"
}
]
}

View File

@ -1,9 +1,9 @@
module.exports = {
client: {
includes: ["web/src/**/*.{js,jsx,ts,tsx,vue,gql}"],
includes: ['web/src/**/*.{js,jsx,ts,tsx,vue,gql}'],
service: {
name: "ifms-pti-srv",
url: "http://localhost:4000/graphql",
},
},
};
name: 'ifms-pti-srv',
url: 'http://localhost:4000/graphql'
}
}
}

View File

@ -1,5 +1,5 @@
version: '3'
services:
services:
postgres:
image: postgres:latest
restart: 'no'
@ -10,7 +10,7 @@ services:
- 'postgres:/var/lib/postgresql/data'
ports:
- '5432:5432'
pgadmin:
image: dpage/pgadmin4:latest
restart: 'no'

View File

@ -5,5 +5,10 @@
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"excludeFiles": "dist/**"
"overrides": [
{
"files": "*.js, *.vue, *.css, *.scss",
"excludeFiles": "**/dist/**, **/node_modules/**"
}
]
}

View File

@ -1,4 +1,5 @@
# Portal de TI - Server
Servidor de API GraphQL para o Portal de TI do IFMS
## Requisitos
@ -8,30 +9,35 @@ Servidor de API GraphQL para o Portal de TI do IFMS
- Docker Compose
## Desenvolvimento
~~~
```
cp .env.example .env
~~~
```
Altere as variáveis de ambiente em `.env`
~~~
```
docker-compose up -d
npm run prisma-deploy # Rodar sempre que alterar o schema
npm run dev
~~~
```
## Compilar para produção
~~~
```
npm run prisma-deploy
npm run build
~~~
```
## Produção
Não é recomendado usar o arquivo .env em produção.
Não é recomendado usar o arquivo .env em produção.
Configure as variáveis de ambiente no servidor.
~~~
```
npm start
~~~
```
---
Desenvolvido pelo SERTI Ponta Porã
Desenvolvido pelo SERTI Ponta Porã

View File

@ -0,0 +1,29 @@
/*
Warnings:
- The `uptime` column on the `AccessPoint` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `uptime` column on the `WifiDevice` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- You are about to drop the column `avgSignal` on the `WifiStats` table. All the data in the column will be lost.
- You are about to drop the column `maxSignal` on the `WifiStats` table. All the data in the column will be lost.
- You are about to drop the column `minSignal` on the `WifiStats` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "AccessPoint" ADD COLUMN "usage" BIGINT,
DROP COLUMN "uptime",
ADD COLUMN "uptime" INTEGER;
-- AlterTable
ALTER TABLE "WifiDevice" DROP COLUMN "uptime",
ADD COLUMN "uptime" INTEGER,
ALTER COLUMN "usage" SET DATA TYPE BIGINT;
-- AlterTable
ALTER TABLE "WifiStats" DROP COLUMN "avgSignal",
DROP COLUMN "maxSignal",
DROP COLUMN "minSignal",
ADD COLUMN "avgSignalStrength" INTEGER,
ADD COLUMN "maxSignalStrength" INTEGER,
ADD COLUMN "minSignalStrength" INTEGER,
ALTER COLUMN "avgUsage" SET DATA TYPE BIGINT,
ALTER COLUMN "sumUsage" SET DATA TYPE BIGINT;

View File

@ -89,13 +89,13 @@ model WifiDevice {
notes String?
essid String?
uptime String?
uptime Int?
apName String?
signalStrength Int?
frequency String?
protocol String?
speed Int?
usage Int?
usage BigInt?
status Status?
createdAt DateTime @default(now())
@ -160,12 +160,14 @@ model AccessPoint {
notes String?
inventoryTag String?
uptime String?
uptime Int?
controller String?
model String?
ip String?
clients Int?
usage BigInt?
encryptedSshUser String?
encryptedSshPassword String?
@ -182,9 +184,9 @@ model WifiStats {
clients Int?
avgSignal Int?
minSignal Int?
maxSignal Int?
avgSignalStrength Int?
minSignalStrength Int?
maxSignalStrength Int?
avgSpeed Int?
minSpeed Int?
@ -193,8 +195,8 @@ model WifiStats {
avgClientUptime Int?
maxClientUptime Int?
avgUsage Int?
sumUsage Int?
avgUsage BigInt?
sumUsage BigInt?
accessPointId Int
accessPoint AccessPoint @relation("wifistats_to_ap", fields: [accessPointId], references: [id])

View File

@ -392,8 +392,9 @@ 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
})
}
@ -402,11 +403,12 @@ 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

@ -5,7 +5,10 @@ import { User } from './classes/User'
import { deleteOldLogs, logInfo, logSuccess } from './lib/logger'
import { updateAccessPoints } from './lib/accessPoints'
import {
deleteOldStats,
generateStatsForAllAccessPoints
} from './lib/wifiStats'
// WARNING! All crontasks are blocking! Do not await inside it
@ -41,4 +44,12 @@ cron.schedule('0 0 2 * * *', () => {
cron.schedule('0 */2 * * * *', () => {
updateAccessPoints().catch(console.log)
})
})
cron.schedule('0 */5 * * * *', () => {
generateStatsForAllAccessPoints()
})
cron.schedule('0 0 2 * * *', () => {
deleteOldStats()
})

View File

@ -1,4 +1,4 @@
import { } from 'dotenv/config'
import {} from 'dotenv/config'
import './utils/capitalize'
import './utils/contains'
import './utils/cycle'

View File

@ -1,5 +1,5 @@
import prisma from '../prisma'
import { ACCESS_POINTS_UPDATED, pubsub } from '../pubsub'
import { getAccessPoints as getCiscoAccessPoints } from './ciscoController'
import { logInfo, logSuccess } from './logger'
import { getAccessPoints as getUnifiAccessPoints } from './unifiController'
@ -50,7 +50,7 @@ export async function updateAccessPoints() {
}
},
data: {
uptime: "-1"
uptime: -1
}
})

View File

@ -103,7 +103,7 @@ export async function getOnlineWifiDevices() {
lastSeen: now,
essid: client.SSID,
ip: client.IP,
uptime: client.UT.toString(),
uptime: +client.UT,
apName: client.AP,
status: client.ST == 'Online' ? 'ONLINE' : 'OFFLINE',
controller: 'Cisco',
@ -143,14 +143,15 @@ export function getAccessPoints() {
clearTimeout(timeout)
const restructuredAccessPoints = accessPoints.map(
({ Nm, Mc, Md, Ut, A4, Cl }) => ({
({ Nm, Mc, Md, Ut, A4, Cl, Vm }) => ({
mac: Mc,
hostname: Nm,
uptime: Ut.toString(),
uptime: +Ut,
controller: 'Cisco',
model: Md,
ip: A4,
clients: Cl
clients: +Cl,
usage: +Vm
})
)

View File

@ -20,7 +20,8 @@ async function log(
const entryTags = tags.length ? ` [${tags}] ` : ''
console.log(
`${color}[${format(now, 'HH:mm:ss')}]${entryTags ? entryTags : ''
`${color}[${format(now, 'HH:mm:ss')}]${
entryTags ? entryTags : ''
}\x1b[0m${message}`
)

View File

@ -105,8 +105,9 @@ async function updateUserIdMappings() {
logSuccess({
tags: ['paloalto', 'user-id'],
message: `Foram mapeados ${devices.length} user-ids em ${pAHost.description || pAHost.cidr
} em ${((endTime - startTime) / 1000).toFixed(2)}s.`,
message: `Foram mapeados ${devices.length} user-ids em ${
pAHost.description || pAHost.cidr
} em ${((endTime - startTime) / 1000).toFixed(2)}s.`,
data: devices
})
@ -114,8 +115,9 @@ async function updateUserIdMappings() {
} catch (e) {
logError({
tags: ['paloalto', 'user-id'],
message: `Erro atualizando user-id mappings em ${pAHost.description || pAHost.cidr
}: ${e.message}`
message: `Erro atualizando user-id mappings em ${
pAHost.description || pAHost.cidr
}: ${e.message}`
})
//console.log(e) // Do not add e to log DB for security reasons...

View File

@ -187,7 +187,7 @@ export async function getOnlineWifiDevices() {
lastSeen: new Date(client.last_seen * 1000),
essid: client.essid,
ip: client.ip,
uptime: client.uptime.toString(),
uptime: +client.uptime,
apName: accessPoints[0].find(ap => ap.mac === client.ap_mac).name,
status: 'ONLINE',
controller: 'UniFi',
@ -209,14 +209,15 @@ export async function getAccessPoints() {
const accessPoints = await unifiController.getAccessDevices('default')
const restructuredAccessPoints = accessPoints[0].map(
({ mac, model, ip, uptime, name, num_sta }) => ({
({ mac, model, ip, uptime, name, num_sta, bytes }) => ({
mac,
hostname: name,
uptime: uptime ? uptime.toString() : "-1",
uptime: uptime ? +uptime : -1,
controller: 'UniFi',
model,
ip,
clients: num_sta
clients: num_sta,
usage: +bytes
})
)

View File

@ -20,8 +20,9 @@ let working = false
const wifiControllers = [unifiController, ciscoController]
async function getOnlineDevices() {
const onlineDevicesPromises = wifiControllers.map(wifiController => wifiController.getOnlineWifiDevices())
const onlineDevicesPromises = wifiControllers.map(wifiController =>
wifiController.getOnlineWifiDevices()
)
const onlineDevices = (await Promise.all(onlineDevicesPromises)).flat()
@ -86,10 +87,10 @@ async function updateDB(onlineDevices) {
const user = device.user
? {
connect: {
sAMAccountName: device.user.replace('IFMS\\', '').toLowerCase()
connect: {
sAMAccountName: device.user.replace('IFMS\\', '').toLowerCase()
}
}
}
: undefined
const hostname = device.hostname || mockHostName(device)
@ -184,17 +185,16 @@ function updateDevicesInfo() {
onlineDevices.length > 0
? resolve(
`${onlineDevices.length} atualizados em ${Math.floor(
endTime - startTime
)}ms, DB em ${Math.floor(endTimeDB - startTimeDB)}ms`
)
`${onlineDevices.length} atualizados em ${Math.floor(
endTime - startTime
)}ms, DB em ${Math.floor(endTimeDB - startTimeDB)}ms`
)
: reject('Não há dispositivos conectados no momento.')
pubsub.publish(USER_PRESENCE_UPDATED, {
userPresenceUpdated: onlineDevices.length
})
const updatedDbAccessPoints = await prisma.accessPoint.findMany({
include: {
wifiDevices: {
@ -205,12 +205,17 @@ function updateDevicesInfo() {
}
})
pubsub.publish(ACCESS_POINTS_UPDATED, { accessPointsUpdated: updatedDbAccessPoints })
pubsub.publish(ACCESS_POINTS_UPDATED, {
accessPointsUpdated: updatedDbAccessPoints
})
logSuccess({
tags: ['wifiDevices'],
message: `${onlineDevices.length} dispositivos Wi-Fi atualizados em
${((endTime - startTime) / 1000).toFixed(2)}s. BD em ${((endTimeDB - startTimeDB) / 1000).toFixed(2)}s`
${((endTime - startTime) / 1000).toFixed(2)}s. BD em ${(
(endTimeDB - startTimeDB) /
1000
).toFixed(2)}s`
})
} catch (e) {
logError({
@ -229,7 +234,8 @@ function updateDevicesInfo() {
// Delete devices that are offline for more than OLD_DEVICES_THRESHOLD_IN_DAYS days
async function deleteOldDevices() {
const oldDevicesThresholdInMilliseconds = OLD_DEVICES_THRESHOLD_IN_DAYS * 24 * 60 * 60 * 1000
const oldDevicesThresholdInMilliseconds =
OLD_DEVICES_THRESHOLD_IN_DAYS * 24 * 60 * 60 * 1000
const oldDevices = await prisma.wifiDevice.deleteMany({
where: {

View File

@ -1,13 +1,17 @@
import prisma from "../prisma";
import prisma from '../prisma'
import { logError, logSuccess } from './logger'
import { subDays } from 'date-fns'
generateStatsForAccessPoint(1)
const DAYS_TO_KEEP = 90
async function generateStatsForAccessPoint(accessPointId) {
async function generateStatsForAccessPoint(mac) {
const timestamp = new Date()
const stats = await prisma.wifiDevice.aggregate({
const dbStats = await prisma.wifiDevice.aggregate({
where: {
accessPointId,
accessPoint: {
mac
},
status: 'ONLINE'
},
_count: {
@ -16,41 +20,84 @@ async function generateStatsForAccessPoint(accessPointId) {
_avg: {
signalStrength: true,
speed: true,
usage: true
usage: true,
uptime: true
},
_max: {
signalStrength: true,
speed: true,
usage: true
usage: true,
uptime: true
},
_min: {
signalStrength: true,
speed: true,
speed: true
},
_sum: {
usage: true
}
})
console.log(timestamp, stats)
const stats = {
timestamp: timestamp.toISOString(),
clients: dbStats._count._all,
avgSignalStrength: Math.floor(dbStats._avg.signalStrength, 0),
minSignalStrength: dbStats._min.signalStrength,
maxSignalStrength: dbStats._max.signalStrength,
avgSpeed: Math.floor(dbStats._avg.speed, 0),
minSpeed: dbStats._min.speed || 0,
maxSpeed: dbStats._max.speed || 0,
avgClientUptime: Math.floor(dbStats._avg.uptime, 0),
maxClientUptime: dbStats._max.uptime || 0,
avgUsage: Math.floor(dbStats._avg.usage, 0),
sumUsage: dbStats._sum.usage || 0
}
await prisma.wifiStats.create({
data: {
...stats,
accessPoint: { connect: { mac } }
}
})
}
// id Int @id @default(autoincrement())
// timestamp DateTime @default(now())
export async function generateStatsForAllAccessPoints() {
try {
const accessPoints = await prisma.accessPoint.findMany()
// clients Int?
for (const accessPoint of accessPoints) {
await generateStatsForAccessPoint(accessPoint.mac)
}
// avgSignal Int?
// minSignal Int?
// maxSignal Int?
logSuccess({
tags: ['wifiStats', 'generateStatsForAllAccessPoints'],
message: `Estatísticas geradas para ${accessPoints.length} access points`
})
} catch (e) {
console.log(e)
logError({
tags: ['wifiStats', 'generateStatsForAllAccessPoints'],
message: 'Erro ao gerar estatísticas para todos os access points',
data: e
})
}
}
// avgSpeed Int?
// minSpeed Int?
// maxSpeed Int?
export async function deleteOldStats() {
try {
const stats = await prisma.wifiStats.deleteMany({
where: { timestamp: { lt: subDays(new Date(), DAYS_TO_KEEP) } }
})
// avgClientUptime Int?
// maxClientUptime Int?
// avgUsage Int?
// sumUsage Int?
// accessPointId Int
// accessPoint AccessPoint @relation("wifistats_to_ap", fields: [accessPointId], references: [id])
logSuccess({
tags: ['wifiStats', 'deleteOldStats'],
message: `${stats.count} estatísticas com mais de ${DAYS_TO_KEEP} dias deletadas.`
})
} catch (e) {
console.log(e)
}
}

View File

@ -1,11 +1,10 @@
import prisma from "../prisma";
import { getSubnetInfo } from "../utils/subnetInfo";
import prisma from '../prisma'
import { getSubnetInfo } from '../utils/subnetInfo'
export const AccessPoint = {
updatedAt: (parent, data, context, info) => parent.updatedAt?.toISOString(),
async clients(parent, data, context, info) {
const clientsCount = await prisma.wifiDevice.count({
where: {
status: 'ONLINE',
@ -15,8 +14,9 @@ export const AccessPoint = {
}
})
return clientsCount;
return clientsCount
},
subnetInfo: parent => getSubnetInfo(parent.ip)
subnetInfo: parent => getSubnetInfo(parent.ip),
usage: (parent, data, context, info) => parent.usage.toString()
}

View File

@ -22,4 +22,4 @@ export async function addPAHost(
})
return host
}
}

View File

@ -2,4 +2,4 @@ import { ResetToken } from '../../classes/ResetToken'
export async function createResetToken(parent, { data }, { auth }) {
return ResetToken.createToken(data.username, auth.sAMAccountName)
}
}

View File

@ -7,4 +7,4 @@ export async function delPAHost(parent, { id }, { auth }) {
throw new Error('Você não pode apagar o host de outro usuário')
return prisma.pAHost.delete({ where: { id } })
}
}

View File

@ -2,4 +2,4 @@ import { ResetToken } from '../../classes/ResetToken'
export async function deleteExpiredTokens() {
return `Tokens deletados ${await ResetToken.deleteExpiredTokens()}`
}
}

View File

@ -3,4 +3,4 @@ import { User } from '../../classes/User'
export async function importUsers() {
User.importAllUsers()
return 'A importação está sendo feita. Isso pode demorar alguns minutos.'
}
}

View File

@ -11,7 +11,6 @@ import { delPAHost } from './delPAHost'
import { updateAccessPoint } from './updateAccessPoint'
const Mutation = {
login,
updatePassword,
replacePassword,

View File

@ -2,4 +2,4 @@ import { User } from '../../classes/User'
export async function login(parent, { data }) {
return User.login(data.username, data.password)
}
}

View File

@ -8,4 +8,4 @@ export async function replacePassword(parent, { data }, { auth }) {
})
return replaceADPassword(data.username, data.newPassword)
}
}

View File

@ -15,8 +15,7 @@ export async function replaceStudentPassword(parent, { data }, { auth }) {
if (isServant) throw new Error(`Usuário ${data.username} é um servidor`)
if (!isStudent)
throw new Error(`Usuário ${data.username} não é um estudante`)
if (!isStudent) throw new Error(`Usuário ${data.username} não é um estudante`)
logInfo({
tags: ['replaceStudentPassword', 'user'],
@ -24,4 +23,4 @@ export async function replaceStudentPassword(parent, { data }, { auth }) {
})
return replacePassword(data.username, data.newPassword)
}
}

View File

@ -1,22 +1,37 @@
import prisma from '../../prisma'
import { ACCESS_POINTS_UPDATED, pubsub } from '../../pubsub'
import { logError, logInfo } from '../../lib/logger'
import { getSubnetInfo } from '../../utils/subnetInfo'
export async function updateAccessPoint(_, { data: { id, name, local, notes } }, { auth },) {
const accessPoint = await prisma.accessPoint.findUnique({ where: { id: parseInt(id) }, })
export async function updateAccessPoint(
_,
{ data: { id, name, local, notes } },
{ auth }
) {
const accessPoint = await prisma.accessPoint.findUnique({
where: { id: parseInt(id) }
})
if (!accessPoint) throw new Error('Access Point não encontrado')
if (getSubnetInfo(accessPoint.ip).shortName !== auth.campus) {
logError({
tags: ['accessPointEdited', 'accessPoints'],
message: `O usuário ${auth.displayName} (${auth.sAMAccountName}) tentou atualizar as informações do
AP ${accessPoint.name || accessPoint.hostname}, mas não tinha permissão.`
message: `O usuário ${auth.displayName} (${
auth.sAMAccountName
}) tentou atualizar as informações do
AP ${
accessPoint.name || accessPoint.hostname
}, mas não tinha permissão.`
})
throw new Error(`O AP ${accessPoint.name || accessPoint.hostname} não está na rede do campus ${auth.campus}. Você só pode editar APs da rede do seu campus.`)
throw new Error(
`O AP ${
accessPoint.name || accessPoint.hostname
} não está na rede do campus ${
auth.campus
}. Você pode editar APs da rede do seu campus.`
)
}
const updatedAccessPoint = await prisma.accessPoint.update({
@ -26,7 +41,11 @@ export async function updateAccessPoint(_, { data: { id, name, local, notes } },
logInfo({
tags: ['accessPointEdited', 'accessPoints'],
message: `O usuário ${auth.displayName} (${auth.sAMAccountName}) atualizou as informações do AP ${updatedAccessPoint.name || updatedAccessPoint.hostname}`,
message: `O usuário ${auth.displayName} (${
auth.sAMAccountName
}) atualizou as informações do AP ${
updatedAccessPoint.name || updatedAccessPoint.hostname
}`,
data: updatedAccessPoint
})
@ -39,4 +58,4 @@ export async function updateAccessPoint(_, { data: { id, name, local, notes } },
})
return updatedAccessPoint
}
}

View File

@ -1,3 +1,3 @@
export async function updatePassword(parent, { data }, { auth }) {
return auth.updatePassword(data.oldPassword, data.newPassword)
}
}

View File

@ -2,4 +2,4 @@ import { ResetToken } from '../../classes/ResetToken'
export async function useResetToken(parent, { data }) {
return ResetToken.useToken(data.token, data.newPassword)
}
}

View File

@ -16,9 +16,9 @@ export async function logs(parent, { search, dateIn, dateOut, limit = 100 }) {
},
OR: search
? [
{ message: { contains: search, mode: 'insensitive' } },
{ tags: { contains: search, mode: 'insensitive' } }
]
{ message: { contains: search, mode: 'insensitive' } },
{ tags: { contains: search, mode: 'insensitive' } }
]
: undefined
},
orderBy: { timestamp: 'desc' },

View File

@ -18,7 +18,9 @@ export async function userPresence(_, { search, onlyServants }) {
if (onlyServants)
usersWithWifiDevices = usersWithWifiDevices.filter(
({ extensionAttribute2 }) => extensionAttribute2 == 'Técnico-administrativo' || extensionAttribute2 == 'Docente'
({ extensionAttribute2 }) =>
extensionAttribute2 == 'Técnico-administrativo' ||
extensionAttribute2 == 'Docente'
)
let filteredUsers = usersWithWifiDevices
@ -28,7 +30,6 @@ export async function userPresence(_, { search, onlyServants }) {
const searchTerms = search.split(' ')
for (const term of searchTerms) {
filteredUsers = filteredUsers.filter(
user =>
Object.keys(user).some(
@ -37,14 +38,12 @@ export async function userPresence(_, { search, onlyServants }) {
user.wifiDevices?.some(
device =>
device.status != 'OFFLINE' &&
(
device.ip?.startsWith(term) ||
(device.ip?.startsWith(term) ||
device.apName?.contains(term) ||
device.essid?.contains(term) ||
device.hostname?.contains(term) ||
device.accessPoint?.name?.contains(term) ||
device.accessPoint?.local?.contains(term)
)
device.accessPoint?.local?.contains(term))
)
)
}
@ -80,9 +79,13 @@ export async function userPresence(_, { search, onlyServants }) {
thumbnailPhoto: userPresence.thumbnailPhoto,
lastSeen: userPresence.wifiDevices[0].lastSeen,
status: userPresence.wifiDevices[0].status,
apName: userPresence.wifiDevices[0].accessPoint?.name || userPresence.wifiDevices[0].apName || userPresence.wifiDevices[0].accessPoint?.hostname,
apName:
userPresence.wifiDevices[0].accessPoint?.name ||
userPresence.wifiDevices[0].apName ||
userPresence.wifiDevices[0].accessPoint?.hostname,
local: userPresence.wifiDevices[0].accessPoint?.local,
campus: getSubnetInfo(userPresence.wifiDevices[0].accessPoint?.ip).shortName
campus: getSubnetInfo(userPresence.wifiDevices[0].accessPoint?.ip)
.shortName
}))
.slice(0, 200)

View File

@ -1,6 +1,9 @@
import prisma from '../../prisma'
export async function wifiDevices(parent, { take = 50, skip = 0, search, sortBy, sortDesc, onlineOnly }) {
export async function wifiDevices(
parent,
{ take = 50, skip = 0, search, sortBy, sortDesc, onlineOnly }
) {
const mode = 'insensitive'
if (!search) search = ''
@ -28,9 +31,7 @@ export async function wifiDevices(parent, { take = 50, skip = 0, search, sortBy,
}),
data: prisma.wifiDevice.findMany({
where,
orderBy: [
{ [sortBy || 'hostname']: sortDesc ? 'desc' : 'asc' },
],
orderBy: [{ [sortBy || 'hostname']: sortDesc ? 'desc' : 'asc' }],
include: { user: true, accessPoint: true },
take,
skip

View File

@ -4,8 +4,7 @@ import prisma from '../../prisma'
export async function wifiUsers(parent, { take = 10, skip = 0, search }) {
const mode = 'insensitive'
if (search === null)
search = undefined
if (search === null) search = undefined
const where = {
AND: [
@ -19,7 +18,7 @@ export async function wifiUsers(parent, { take = 10, skip = 0, search }) {
accessPoint: {
OR: [
{ name: { contains: search, mode } },
{ local: { contains: search, mode } },
{ local: { contains: search, mode } }
]
}
}
@ -27,27 +26,24 @@ export async function wifiUsers(parent, { take = 10, skip = 0, search }) {
},
{ wifiDevices: { some: { mac: { contains: search, mode } } } },
{ wifiDevices: { some: { ip: { contains: search, mode } } } },
{ displayName: { contains: search, mode } }, ,
{ sAMAccountName: { contains: search, mode } },
{ displayName: { contains: search, mode } },
,
{ sAMAccountName: { contains: search, mode } }
]
}
]
}
return {
data:
prisma.user.findMany({
where,
include: {
wifiDevices: true
},
orderBy: [
{ wifiDevices: { _count: 'desc' } },
{ displayName: 'asc' }],
take,
skip
}),
data: prisma.user.findMany({
where,
include: {
wifiDevices: true
},
orderBy: [{ wifiDevices: { _count: 'desc' } }, { displayName: 'asc' }],
take,
skip
}),
total: prisma.user.count({ where })
}

View File

@ -20,15 +20,15 @@ const User = {
sharedFolders: parent =>
parent.groups
? parent.groups
.filter(group => group.cn.includes('-Share-'))
.map(group => group.cn.split('-')[2])
.filter(group => group.cn.includes('-Share-'))
.map(group => group.cn.split('-')[2])
: [],
sharedPrinters: parent =>
parent.groups
? parent.groups
.filter(group => group.cn.includes('-Printer-'))
.map(group => group.cn.split('-')[2])
.filter(group => group.cn.includes('-Printer-'))
.map(group => group.cn.split('-')[2])
: [],
isSuperAdmin: parent => parent.roles.includes('superAdmin'),
@ -62,24 +62,22 @@ const User = {
return campus || '--'
},
onlineWifiDevicesCount: (parent, data, { auth }) => prisma.wifiDevice.count({
where: {
status: 'ONLINE',
user: { id: parent.id }
}
}),
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 }
}
})
offlineWifiDevicesCount: (parent, data, { auth }) =>
prisma.wifiDevice.count({
where: {
OR: [{ status: 'OFFLINE' }, { status: 'RECENT' }],
user: { id: parent.id }
}
})
}
export { User }

View File

@ -15,25 +15,22 @@ const WifiDevice = {
})
).user,
usage: parent => parent.usage < 0 ? parent.usage * -1 : parent.usage,
usage: parent => parent.usage.toString(),
accessPoint: async parent => {
try {
const ap = await prisma.accessPoint.findUnique({ where: { hostname: parent.apName } })
if (ap)
return ap
else
return null
}
catch (e) {
const ap = await prisma.accessPoint.findUnique({
where: { hostname: parent.apName }
})
if (ap) return ap
else return null
} catch (e) {
logError({
tags: ['wifiDevice', 'accessPoint'],
message: `Could not find access point ${parent.apName}`,
data: parent
})
}
}
}

View File

@ -25,7 +25,7 @@ class AuthDirective extends SchemaDirectiveVisitor {
context.auth = {
...user,
campus: user.extensionAttribute1?.split('-')[0] || '--',
campus: user.extensionAttribute1?.split('-')[0] || '--'
}
if (user.pwdLastSet.toISOString() === pwdLastSet) {

View File

@ -3,7 +3,7 @@ import { gql } from 'apollo-server'
const typeDefs = gql`
type Query {
"Returns only a few fields of a user"
basicUser(sAMAccountName: String!): User!
basicUser(sAMAccountName: String!): User!
"The authenticated user"
me: User! @auth
@ -15,37 +15,37 @@ const typeDefs = gql`
limit: Int = 15
"Should return only students?"
onlyStudents: Boolean = false
): [User!] @auth(roles: ["servant"])
): [User!] @auth(roles: ["servant"])
"A single user"
user(sAMAccountName: String!): User!
@auth(roles: ["superAdmin"])
user(sAMAccountName: String!): User! @auth(roles: ["superAdmin"])
"AD groups"
groups(where: GroupWhereInput!, limit: Int = 10): [Group!]!
@auth(roles: ["servant"])
@auth(roles: ["servant"])
"Current stats. Differs from the historical statistics."
stats: Stats!
"Users who has some device currently connected to Wi-Fi"
userPresence(search: String = "", onlyServants: Boolean = false): [UserPresence!] @auth(roles: ["watcher"])
userPresence(
search: String = ""
onlyServants: Boolean = false
): [UserPresence!] @auth(roles: ["watcher"])
"Devices that uses the Wi-Fi"
wifiDevices(
search: String = ""
search: String = ""
take: Int
skip: Int
sortBy: WifiDevicesResultSortBy = "signalStrength"
sortDesc: Boolean = false,
sortDesc: Boolean = false
onlineOnly: Boolean = false
): WifiDevicesResult! @auth(roles: ["superAdmin"])
"Users that uses the Wi-Fi"
wifiUsers(
search: String = ""
take: Int
skip: Int): WifiUsersResult! @auth(roles: ["superAdmin"])
wifiUsers(search: String = "", take: Int, skip: Int): WifiUsersResult!
@auth(roles: ["superAdmin"])
"Application Logs"
logs(
@ -92,7 +92,7 @@ const typeDefs = gql`
"Import all users from Active Directory"
importUsers: String! @auth(roles: ["superAdmin"])
"Add a PA host"
addPAHost(data: AddPAHostInput!): PAHost! @auth(roles: ["superAdmin"])
@ -239,7 +239,7 @@ const typeDefs = gql`
lastSeen: String
essid: String
ip: String
uptime: String
uptime: Int
apName: String
status: Status
accessPoint: AccessPoint
@ -247,7 +247,7 @@ const typeDefs = gql`
frequency: String
protocol: String
speed: Int
usage: Int
usage: String
}
"A user that is on the Wi-Fi network reach"
@ -322,6 +322,7 @@ const typeDefs = gql`
ip: String
clients: Int
subnetInfo: SubnetInfo
usage: String
createdAt: String
updatedAt: String
@ -345,14 +346,14 @@ const typeDefs = gql`
shortName: String!
name: String!
cidr: String!
networkAddress: String!
firstAddress: String!
lastAddress: String!
broadcastAddress: String!
networkAddress: String!
firstAddress: String!
lastAddress: String!
broadcastAddress: String!
subnetMask: String!
subnetMaskLength: String!
numHosts: String!
length: String!
subnetMaskLength: String!
numHosts: String!
length: String!
}
input LoginInput {
@ -405,7 +406,7 @@ const typeDefs = gql`
notes: String
}
enum WifiDevicesResultSortBy{
enum WifiDevicesResultSortBy {
mac
hostname
firstSeen
@ -420,7 +421,7 @@ const typeDefs = gql`
frequency
protocol
speed
usage
usage
}
`

View File

@ -1,4 +1,4 @@
String.prototype.capitalize = function(lower = true) {
String.prototype.capitalize = function (lower = true) {
return (lower ? this.toLowerCase() : this).replace(/(?:^|\s)\S/g, a =>
a.toUpperCase()
)

View File

@ -22,9 +22,9 @@
retrocycle, set, stringify, test
*/
if (typeof JSON.decycle !== "function") {
if (typeof JSON.decycle !== 'function') {
JSON.decycle = function decycle(object, replacer) {
"use strict";
'use strict'
// Make a deep copy of an object or array, assuring that there is at most
// one instance of each object or array in the resulting structure. The
@ -50,77 +50,73 @@ if (typeof JSON.decycle !== "function") {
// the object or array. [NUMBER] or [STRING] indicates a child element or
// property.
var objects = new WeakMap(); // object to path mappings
var objects = new WeakMap() // object to path mappings
return (function derez(value, path) {
// The derez function recurses through the object, producing the deep copy.
var old_path; // The path of an earlier occurance of value
var nu; // The new object or array
var old_path // The path of an earlier occurance of value
var nu // The new object or array
// If a replacer function was provided, then call it to get a replacement value.
if (replacer !== undefined) {
value = replacer(value);
value = replacer(value)
}
// typeof null === "object", so go on if this value is really an object but not
// one of the weird builtin objects.
if (
typeof value === "object"
&& value !== null
&& !(value instanceof Boolean)
&& !(value instanceof Date)
&& !(value instanceof Number)
&& !(value instanceof RegExp)
&& !(value instanceof String)
typeof value === 'object' &&
value !== null &&
!(value instanceof Boolean) &&
!(value instanceof Date) &&
!(value instanceof Number) &&
!(value instanceof RegExp) &&
!(value instanceof String)
) {
// If the value is an object or array, look to see if we have already
// encountered it. If so, return a {"$ref":PATH} object. This uses an
// ES6 WeakMap.
old_path = objects.get(value);
old_path = objects.get(value)
if (old_path !== undefined) {
return { $ref: old_path };
return { $ref: old_path }
}
// Otherwise, accumulate the unique value and its path.
objects.set(value, path);
objects.set(value, path)
// If it is an array, replicate the array.
if (Array.isArray(value)) {
nu = [];
nu = []
value.forEach(function (element, i) {
nu[i] = derez(element, path + "[" + i + "]");
});
nu[i] = derez(element, path + '[' + i + ']')
})
} else {
// If it is an object, replicate the object.
nu = {};
nu = {}
Object.keys(value).forEach(function (name) {
nu[name] = derez(
value[name],
path + "[" + JSON.stringify(name) + "]"
);
});
path + '[' + JSON.stringify(name) + ']'
)
})
}
return nu;
return nu
}
return value;
}(object, "$"));
};
return value
})(object, '$')
}
}
if (typeof JSON.retrocycle !== "function") {
if (typeof JSON.retrocycle !== 'function') {
JSON.retrocycle = function retrocycle($) {
"use strict";
'use strict'
// Restore an object that was reduced by decycle. Members whose values are
// objects of the form
@ -141,42 +137,42 @@ if (typeof JSON.retrocycle !== "function") {
// return JSON.retrocycle(JSON.parse(s));
// produces an array containing a single element which is the array itself.
var px = /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\(?:[\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*")\])*$/;
(function rez(value) {
var px =
/^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\(?:[\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*")\])*$/
;(function rez(value) {
// The rez function walks recursively through the object looking for $ref
// properties. When it finds one that has a value that is a path, then it
// replaces the $ref object with a reference to the value that is found by
// the path.
if (value && typeof value === "object") {
if (value && typeof value === 'object') {
if (Array.isArray(value)) {
value.forEach(function (element, i) {
if (typeof element === "object" && element !== null) {
var path = element.$ref;
if (typeof path === "string" && px.test(path)) {
value[i] = eval(path);
if (typeof element === 'object' && element !== null) {
var path = element.$ref
if (typeof path === 'string' && px.test(path)) {
value[i] = eval(path)
} else {
rez(element);
rez(element)
}
}
});
})
} else {
Object.keys(value).forEach(function (name) {
var item = value[name];
if (typeof item === "object" && item !== null) {
var path = item.$ref;
if (typeof path === "string" && px.test(path)) {
value[name] = eval(path);
var item = value[name]
if (typeof item === 'object' && item !== null) {
var path = item.$ref
if (typeof path === 'string' && px.test(path)) {
value[name] = eval(path)
} else {
rez(item);
rez(item)
}
}
});
})
}
}
}($));
return $;
};
}
})($)
return $
}
}

View File

@ -3,7 +3,7 @@ import fs from 'fs'
function saveJSONToFile(json, fileName = 'output.json') {
const jsonContent = JSON.stringify(json)
fs.writeFile(fileName, jsonContent, 'utf8', function(err) {
fs.writeFile(fileName, jsonContent, 'utf8', function (err) {
if (err) {
console.log('An error occured while writing JSON Object to File.')
return console.log(err)

View File

@ -5,47 +5,37 @@ const subNetsInfo = [
{
shortName: 'RT',
name: 'Reitoria',
cidr: '10.0.0.0/16',
cidr: '10.0.0.0/16'
},
{
shortName: 'RT',
name: 'Reitoria',
cidr: '10.1.0.0/16',
addresses: [
{ name: 'RNP', ip: '200.19.32.4' }
]
addresses: [{ name: 'RNP', ip: '200.19.32.4' }]
},
{
shortName: 'AQ',
name: 'Aquidauana',
cidr: '10.2.0.0/16',
addresses: [
{ name: 'RNP', ip: '200.19.32.253' }
]
addresses: [{ name: 'RNP', ip: '200.19.32.253' }]
},
{
shortName: 'CG',
name: 'Campo Grande',
cidr: '10.3.0.0/16',
addresses: [
{ name: 'RNP', ip: '200.19.32.120' }
]
addresses: [{ name: 'RNP', ip: '200.19.32.120' }]
},
{
shortName: 'CB',
name: 'Corumbá',
cidr: '10.4.0.0/16',
addresses: [
{ name: 'RNP', ip: '200.19.37.2' }
]
addresses: [{ name: 'RNP', ip: '200.19.37.2' }]
},
{
shortName: 'CX',
name: 'Coxim',
cidr: '10.5.0.0/16',
addresses: [
{ name: 'RNP', ip: '200.19.36.16' }
],
addresses: [{ name: 'RNP', ip: '200.19.36.16' }]
},
{
shortName: 'NA',
@ -57,17 +47,13 @@ const subNetsInfo = [
shortName: 'PP',
name: 'Ponta Porã',
cidr: '10.7.0.0/16',
addresses: [
{ name: 'RNP', ip: '200.19.34.254' }
]
addresses: [{ name: 'RNP', ip: '200.19.34.254' }]
},
{
shortName: 'TL',
name: 'Três Lagoas',
cidr: '10.8.0.0/16',
addresses: [
{ name: 'RNP', ip: '200.19.35.2' }
]
addresses: [{ name: 'RNP', ip: '200.19.35.2' }]
},
{
shortName: 'JD',
@ -80,13 +66,11 @@ const subNetsInfo = [
name: 'Naviraí',
cidr: '10.10.0.0/16',
addresses: []
},
{
shortName: 'DR',
name: 'Dourados',
cidr: '10.11.0.0/16'
,
cidr: '10.11.0.0/16',
addresses: []
}
]
@ -103,7 +87,6 @@ export function getSubnetInfo(ip) {
name: 'Sem rede'
}
const subnet = subNets.find(subnet => subnet.contains(ip))
if (!subnet)

View File

@ -6,5 +6,10 @@
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false,
"excludeFiles": "dist/**"
"overrides": [
{
"files": "*.js, *.vue, *.css, *.scss",
"excludeFiles": "**/dist/**, **/node_modules/**"
}
]
}

View File

@ -1,6 +1,7 @@
# Portal de TI - Web Client
## Desenvolvimento
```
npm install
npm get-schema
@ -8,11 +9,13 @@ npm run serve
```
### Compilar para produção
```
npm run build
```
### Lint
```
npm run lint
```
```

View File

@ -3,7 +3,10 @@
directive @auth(roles: [String!]) on FIELD_DEFINITION
directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
) on FIELD_DEFINITION | OBJECT | INTERFACE
type AuthPayload {
user: User!
@ -48,14 +51,23 @@ type Mutation {
}
type Query {
"""Returns only a few fields of User"""
"""
Returns only a few fields of User
"""
basicUser(sAMAccountName: String!): User!
me: User!
users(where: UserWhereInput!, limit: Int = 15, onlyStudents: Boolean = false): [User!]
users(
where: UserWhereInput!
limit: Int = 15
onlyStudents: Boolean = false
): [User!]
user(sAMAccountName: String!): User!
groups(where: GroupWhereInput!, limit: Int = 10): [Group!]!
stats: Stats!
wifiDevices(search: String = "", identifiedOnly: Boolean = true): [WifiDevice]!
wifiDevices(
search: String = ""
identifiedOnly: Boolean = true
): [WifiDevice]!
userPresence(search: String): [UserPresence!]
}
@ -91,10 +103,14 @@ input UpdatePasswordInput {
newPassword: String!
}
"""The `Upload` scalar type represents a file upload."""
"""
The `Upload` scalar type represents a file upload.
"""
scalar Upload
"""A mix between the database User and the Active Directory User"""
"""
A mix between the database User and the Active Directory User
"""
type User {
id: ID
wifiDevices: [WifiDevice!]
@ -175,7 +191,7 @@ type WifiDevice {
lastSeen: String
essid: String
ip: String
uptime: String
uptime: Int
apName: String
status: Status
}

View File

@ -1,41 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
"name": "App",
"icons": [
{
"src": "/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
]
}

View File

@ -102,7 +102,8 @@ export default {
</script>
<style scoped>
td {
td,
td * {
white-space: nowrap;
}
</style>

View File

@ -37,4 +37,9 @@ export default {
}
</script>
<style></style>
<style scoped>
td,
td * {
white-space: nowrap;
}
</style>

View File

@ -10,6 +10,8 @@
:items="items"
:headers="headers"
:search="search"
sort-by="status"
sort-desc
calculate-widths
>
<template #[`item.status`]="{ item }">
@ -108,7 +110,8 @@ export default {
</script>
<style scoped>
td {
td,
td * {
white-space: nowrap;
}
</style>

View File

@ -30,7 +30,7 @@
</template>
<template #[`item.uptime`]="{ item: { uptime } }">
<small> {{ uptime | durationFromSeconds }}</small>
<small class="text-no-wrap"> {{ uptime | durationFromSeconds }}</small>
</template>
<template #[`item.signalStrength`]="{ item: { signalStrength, status } }">
@ -131,7 +131,8 @@ export default {
</script>
<style scoped>
td {
td,
td * {
white-space: nowrap;
}
</style>

View File

@ -55,4 +55,9 @@ export default {
}
</script>
<style></style>
<style scoped>
td,
td * {
white-space: nowrap;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<span>
{{ amount }} <small>{{ unit }}</small>
<span class="text-no-wrap">
{{ amount }} <small class="font-weight-medium">{{ unit }}</small>
</span>
</template>
@ -10,8 +10,8 @@ import { destructedBytes } from '../../plugins/format-bytes'
export default {
props: {
value: {
type: Number,
default: 0
type: String,
default: '0'
}
},
computed: {

View File

@ -1,16 +1,19 @@
import Vue from 'vue'
export function destructedBytes(bytes, kib, maxUnit) {
if (bytes == 0)
return {
amount: '0',
unit: 'Bytes'
}
if (isNaN(parseFloat(bytes)) && !isFinite(bytes))
return { amount: '', unit: 'Err' }
const decimals = 1
kib = kib || false
if (bytes < 0) bytes = bytes * -1
if (bytes === 0) return '0 Bytes'
if (isNaN(parseFloat(bytes)) && !isFinite(bytes)) return ''
const k = kib ? 1024 : 1000
const sizes = kib

View File

@ -144,7 +144,7 @@
<template #[`item.clients`]="{ item }">
<div class="align-content-end">
<v-btn
:disabled="item.uptime === '-1'"
:disabled="item.uptime == -1"
block
small
rounded
@ -183,6 +183,9 @@
<template #[`item.notes`]="{ item }">
<small>{{ item.notes }}</small>
</template>
<template #[`item.usage`]="{ item }">
<small>{{ item.usage | bytes }}</small>
</template>
</v-data-table>
</v-container>
</template>
@ -206,6 +209,7 @@ export default {
'local',
'clients',
'uptime',
'usage',
'notes'
],
allHeaders: [
@ -214,7 +218,6 @@ export default {
{ text: 'IP', value: 'ip', width: 80 },
{ text: 'MAC', value: 'mac' },
{ text: 'Campus', value: 'campus', group: true },
{ text: 'Localização', value: 'local' },
{
text: 'Clientes',
@ -228,6 +231,7 @@ export default {
text: 'Controladora',
value: 'controller'
},
{ text: 'Uso', value: 'usage' },
{ text: 'Observações', value: 'notes' },
{ text: 'Última atualização', value: 'updatedAt', width: 160 }
]
@ -306,6 +310,7 @@ export default {
model
ip
clients
usage
createdAt
updatedAt
@ -332,6 +337,7 @@ export default {
model
ip
clients
usage
createdAt
updatedAt