Version 2 initial commit
0
.gitignore
vendored
Normal file
4
server/.babelrc
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["env"],
|
||||||
|
"plugins": ["transform-object-rest-spread"]
|
||||||
|
}
|
37
server/.env.example
Executable file
|
@ -0,0 +1,37 @@
|
||||||
|
# Use este arquivo somente para desenvolvimento/testes
|
||||||
|
# Em produção, estas variáveis devem estar configuradas no ambiente
|
||||||
|
|
||||||
|
# Usuário com privilégios para modificações
|
||||||
|
AD_BIND_USER=ifms\usuario_para_bind
|
||||||
|
AD_BIND_PASSWORD=senha_do_usuario_para_bind
|
||||||
|
AD_URL=ldaps://ifms.edu.br
|
||||||
|
AD_BASE_DN=dc=ifms,dc=edu,dc=br
|
||||||
|
|
||||||
|
# Funções por grupo no AD
|
||||||
|
# Grupo para usuários Administradores do sistema (superAdmin)
|
||||||
|
SUPER_ADMIN_GROUP=PP-PTI-Admins
|
||||||
|
# Pode gerar tokens de redefinição de senha (tokenCreator)
|
||||||
|
TOKEN_CREATOR_GROUP=PP-PTI-TokenCreator
|
||||||
|
# Estudantes (student)
|
||||||
|
STUDENT_GROUP=Estudantes
|
||||||
|
# Servidores (Servant)
|
||||||
|
SERVANT_GROUP=G_SERVIDORES
|
||||||
|
# Presença Online (watcher)
|
||||||
|
WATCHER_GROUP=PP-PTI-Watchers
|
||||||
|
|
||||||
|
# Altere a Variável de ambiente abaixo
|
||||||
|
JWT_SECRET=VoceDeveAlterarEstaVariavelECYxhfBk6weKyJupHRaX2VtvZS4Fn7DdscMmrjWP9AbGqgQ3L8
|
||||||
|
JWT_EXPIRATION=720h
|
||||||
|
|
||||||
|
# Prisma
|
||||||
|
PRISMA_ENDPOINT=http://localhost:4466/
|
||||||
|
PRISMA_SECRET=my-super-secret
|
||||||
|
|
||||||
|
# Playground
|
||||||
|
PLAYGROUND=true
|
||||||
|
|
||||||
|
# UniFi
|
||||||
|
UNIFI_URL=10.7.0.6
|
||||||
|
UNIFI_PORT=8443
|
||||||
|
UNIFI_USER=unifi_admin
|
||||||
|
UNIFI_PASSWORD=senha_do_unifi_admin
|
7
server/.gitignore
vendored
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.env
|
||||||
|
config/
|
||||||
|
dist/
|
||||||
|
generated/
|
||||||
|
node_modules/
|
9
server/.prettierrc
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"excludeFiles": "dist/**"
|
||||||
|
}
|
14
server/Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
FROM node
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run prisma-generate
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
CMD npm run prisma-deploy && npm start
|
38
server/README.md
Executable file
|
@ -0,0 +1,38 @@
|
||||||
|
# IFMS - Portal de TI - Server
|
||||||
|
Servidor de API GraphQL para o Portal de TI do IFMS
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Node
|
||||||
|
- Docker
|
||||||
|
- 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 Datamodel
|
||||||
|
npm run dev
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## Compilar para produção
|
||||||
|
Altere a variável `secret:` em `prisma.yml`
|
||||||
|
~~~
|
||||||
|
npm run prisma-deploy
|
||||||
|
npm run build
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## 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ã
|
42
server/docker-compose.yml
Executable file
|
@ -0,0 +1,42 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
prisma:
|
||||||
|
image: 'prismagraphql/prisma:1.34'
|
||||||
|
restart: 'no'
|
||||||
|
ports:
|
||||||
|
- '4466:4466'
|
||||||
|
environment:
|
||||||
|
PRISMA_CONFIG: |
|
||||||
|
port: 4466
|
||||||
|
# uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
|
||||||
|
# managementApiSecret: my-secret
|
||||||
|
databases:
|
||||||
|
default:
|
||||||
|
connector: postgres
|
||||||
|
host: postgres
|
||||||
|
user: prisma
|
||||||
|
password: prisma
|
||||||
|
rawAccess: false
|
||||||
|
port: 5432
|
||||||
|
migrations: true
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
restart: 'no'
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: prisma
|
||||||
|
POSTGRES_PASSWORD: prisma
|
||||||
|
volumes:
|
||||||
|
- 'postgres:/var/lib/postgresql/data'
|
||||||
|
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4
|
||||||
|
restart: 'no'
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: 'admin@pg.com'
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: 'senhas'
|
||||||
|
ports:
|
||||||
|
- '4477:80'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres: null
|
9080
server/package-lock.json
generated
Executable file
51
server/package.json
Executable file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "ifms-gql-server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Servidor GraphQL para concentrar as informações do IFMS",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "nodemon --ext js,graphql src/index.js --exec babel-node",
|
||||||
|
"prisma-deploy": "prisma deploy",
|
||||||
|
"prisma-generate": "prisma generate",
|
||||||
|
"build": "babel src --out-dir dist --copy-files"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/douglasvbarone/ifms-gql-server.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ifms"
|
||||||
|
],
|
||||||
|
"author": "Douglas Barone",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/douglasvbarone/ifms-gql-server/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/douglasvbarone/ifms-gql-server#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.26.0",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
|
"babel-preset-env": "^1.7.0",
|
||||||
|
"nodemon": "^2.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/polyfill": "^7.8.3",
|
||||||
|
"activedirectory2": "^1.3.0",
|
||||||
|
"apollo-server": "^2.9.16",
|
||||||
|
"apollo-server-plugin-response-cache": "^0.4.1",
|
||||||
|
"bcrypt": "^3.0.7",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"graphql": "^14.6.0",
|
||||||
|
"graphql-tools": "^4.0.6",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"ldapjs": "^1.0.2",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"node-cron": "^2.0.3",
|
||||||
|
"node-unifi": "^1.2.2",
|
||||||
|
"prisma": "^1.34.10",
|
||||||
|
"prisma-binding": "^2.3.16",
|
||||||
|
"prisma-client-lib": "^1.34.10",
|
||||||
|
"uuid": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
10
server/prisma.yml
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
endpoint: ${env:PRISMA_ENDPOINT}
|
||||||
|
datamodel:
|
||||||
|
- src/datamodels/user.prisma
|
||||||
|
- src/datamodels/resetToken.prisma
|
||||||
|
- src/datamodels/wifiDevice.prisma
|
||||||
|
secret: ${env:PRISMA_SECRET}
|
||||||
|
|
||||||
|
generate:
|
||||||
|
- generator: javascript-client
|
||||||
|
output: ./src/generated/prisma-client/
|
96
server/src/classes/ResetToken.js
Executable file
|
@ -0,0 +1,96 @@
|
||||||
|
import { prisma } from '../generated/prisma-client'
|
||||||
|
import uuid from 'uuid/v4'
|
||||||
|
import { User } from './User'
|
||||||
|
import moment from 'moment'
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
import { replacePassword } from '../utils/activedirectory/passwordUtils'
|
||||||
|
|
||||||
|
class ResetToken {
|
||||||
|
/**
|
||||||
|
* Generates a reset token for a specific user
|
||||||
|
* @param username
|
||||||
|
* @param creatorUsername
|
||||||
|
* @returns {ResetTokenPromise}
|
||||||
|
*/
|
||||||
|
static async createToken(username, creatorUsername) {
|
||||||
|
const user = await new User(username).init()
|
||||||
|
|
||||||
|
if (!user.roles.includes('student'))
|
||||||
|
throw new Error('Apenas estudantes podem utilizar tokens')
|
||||||
|
|
||||||
|
const token = uuid()
|
||||||
|
const expiration = moment(new Date()).add(3, 'days')
|
||||||
|
|
||||||
|
const hashedToken = await prisma.createResetToken({
|
||||||
|
token: await bcrypt.hash(token, 2),
|
||||||
|
creator: {
|
||||||
|
connect: { sAMAccountName: creatorUsername }
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: { id: user.id }
|
||||||
|
},
|
||||||
|
expiration: expiration
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...hashedToken,
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a reset token exists and is valid
|
||||||
|
* @param token
|
||||||
|
* @returns {Promise<ResetToken>}
|
||||||
|
*/
|
||||||
|
static async checkToken(token) {
|
||||||
|
const hashedResetTokens = await prisma.resetTokens({
|
||||||
|
where: {
|
||||||
|
expiration_gt: new Date(),
|
||||||
|
usedAt: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hashedResetTokens.length === 0) return null
|
||||||
|
|
||||||
|
const checkedTokens = await Promise.all(
|
||||||
|
hashedResetTokens.map(async hashedToken => ({
|
||||||
|
...hashedToken,
|
||||||
|
valid: await bcrypt.compare(token, hashedToken.token)
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return checkedTokens.find(v => v.valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* @param newPassword
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
static async useToken(token, newPassword) {
|
||||||
|
const resetToken = await ResetToken.checkToken(token)
|
||||||
|
|
||||||
|
if (!resetToken) {
|
||||||
|
throw new Error('Token inválido, já usado ou expirado')
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.resetToken({ id: resetToken.id }).user()
|
||||||
|
|
||||||
|
await replacePassword(user.sAMAccountName, newPassword)
|
||||||
|
|
||||||
|
await prisma.updateResetToken({
|
||||||
|
where: {
|
||||||
|
id: resetToken.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
usedAt: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ResetToken }
|
328
server/src/classes/User.js
Executable file
|
@ -0,0 +1,328 @@
|
||||||
|
import { ad } from '../utils/activedirectory'
|
||||||
|
import { prisma } from '../generated/prisma-client'
|
||||||
|
import { Change, createClient } from 'ldapjs'
|
||||||
|
|
||||||
|
import { encodePassword } from '../utils/activedirectory/encodePassword'
|
||||||
|
import config from '../utils/activedirectory/config'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
|
class User {
|
||||||
|
constructor(username) {
|
||||||
|
this.username = username
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the object, since asynchronous tasks can't run in the constructor
|
||||||
|
* @return {Promise<User>}
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
let user = await prisma.user({ sAMAccountName: this.username })
|
||||||
|
|
||||||
|
if (!user || !user.roles || !user.groups)
|
||||||
|
user = await User.upsertUser(this.username)
|
||||||
|
else User.upsertUser(this.username)
|
||||||
|
|
||||||
|
Object.assign(this, user)
|
||||||
|
|
||||||
|
return this
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user is a Super Admin
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
static async isSuperAdmin(username) {
|
||||||
|
const superAdminGroup = process.env.SUPER_ADMIN_GROUP || 'PP-SERTI'
|
||||||
|
return ad.isUserMemberOf(username, superAdminGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user is a Token Generator
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
static async isTokenCreator(username) {
|
||||||
|
const tokenCreatorGroup = process.env.TOKEN_CREATOR_GROUP || 'CEREL'
|
||||||
|
return await ad.isUserMemberOf(username, tokenCreatorGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user is a Servant
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
static async isServant(username) {
|
||||||
|
const servantGroup = process.env.SERVANT_GROUP || 'G_SERVIDORES'
|
||||||
|
return ad.isUserMemberOf(username, servantGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user is a Student
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
static async isStudent(username) {
|
||||||
|
const studentGroup = process.env.STUDENT_GROUP || 'Estudantes'
|
||||||
|
return ad.isUserMemberOf(username, studentGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the a user role(s)
|
||||||
|
* @param username
|
||||||
|
* @returns {Promise<[String]>}
|
||||||
|
*/
|
||||||
|
static async getRoles(username, groups = null) {
|
||||||
|
const rolesGroups = [
|
||||||
|
{
|
||||||
|
role: 'superAdmin',
|
||||||
|
adGroup: process.env.SUPER_ADMIN_GROUP || 'PP-SERTI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'tokenCreator',
|
||||||
|
adGroup: process.env.TOKEN_CREATOR_GROUP || 'PTI-TokenGen'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'student',
|
||||||
|
adGroup: process.env.STUDENT_GROUP || 'Estudantes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'servant',
|
||||||
|
adGroup: process.env.SERVANT_GROUP || 'G_SERVIDORES'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'watcher',
|
||||||
|
adGroup: process.env.WATCHER_GROUP || 'PTI-Vigias'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const userGroups = groups || (await User.getGroupsMembership(username))
|
||||||
|
|
||||||
|
const userRoles = rolesGroups
|
||||||
|
.filter(roleGroup =>
|
||||||
|
userGroups.some(userGroup => userGroup.cn === roleGroup.adGroup)
|
||||||
|
)
|
||||||
|
.map(role => role.role)
|
||||||
|
|
||||||
|
if (userRoles.some(role => role === 'superAdmin'))
|
||||||
|
userRoles.push('tokenCreator', 'watcher')
|
||||||
|
|
||||||
|
return [...new Set(userRoles)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user has group membership
|
||||||
|
* @param {String} group
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async isMemberOf(group) {
|
||||||
|
return ad.isUserMemberOf(this.username, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the user password.
|
||||||
|
* @param oldPassword
|
||||||
|
* @param newPassword
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
updatePassword(oldPassword, newPassword) {
|
||||||
|
if (oldPassword === newPassword)
|
||||||
|
throw new Error('A nova senha não pode ser igual à antiga')
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = createClient(config)
|
||||||
|
|
||||||
|
client.on('error', err => {
|
||||||
|
client.unbind(() => {
|
||||||
|
console.log('Client unbinded due error.')
|
||||||
|
})
|
||||||
|
reject(new Error(err.message))
|
||||||
|
})
|
||||||
|
|
||||||
|
client.bind(`ifms\\${this.username}`, oldPassword, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
client.bind(config.username, config.password, (err, result) => {
|
||||||
|
if (err) reject(new Error('Problemas técnicos, tente mais tarde'))
|
||||||
|
else reject(new Error('Usuário ou senha antiga incorretos.'))
|
||||||
|
})
|
||||||
|
} else
|
||||||
|
client.search(
|
||||||
|
config.baseDN,
|
||||||
|
{
|
||||||
|
filter: `(sAMAccountName=${this.username})`,
|
||||||
|
attributes: 'dn',
|
||||||
|
scope: 'sub'
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
res.on('error', err => {
|
||||||
|
reject(new Error(err.message))
|
||||||
|
})
|
||||||
|
|
||||||
|
res.on('searchEntry', entry => {
|
||||||
|
const userDN = entry.object.dn
|
||||||
|
|
||||||
|
client.bind(userDN, oldPassword, () => {
|
||||||
|
client.modify(
|
||||||
|
userDN,
|
||||||
|
[
|
||||||
|
new Change({
|
||||||
|
operation: 'delete',
|
||||||
|
modification: {
|
||||||
|
unicodePwd: encodePassword(oldPassword)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Change({
|
||||||
|
operation: 'add',
|
||||||
|
modification: {
|
||||||
|
unicodePwd: encodePassword(newPassword)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
(err, result) => {
|
||||||
|
if (err) {
|
||||||
|
client.unbind(() => {
|
||||||
|
console.log('Client unbinded due error.')
|
||||||
|
})
|
||||||
|
reject(new Error(err.message))
|
||||||
|
} else {
|
||||||
|
User.upsertUser(this.username)
|
||||||
|
client.unbind(() => {
|
||||||
|
console.log('Client unbinded')
|
||||||
|
})
|
||||||
|
resolve(User.login(this.username, newPassword))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getGroupsMembership(username) {
|
||||||
|
return ad.getGroupMembershipForUser(
|
||||||
|
{ attributes: ['cn', 'dn', 'name'] },
|
||||||
|
username
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or updates a user in the database
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
static async upsertUser(username) {
|
||||||
|
const adUser = await ad.findUser(username)
|
||||||
|
if (!adUser) throw new Error('Usuário não encontrado')
|
||||||
|
|
||||||
|
const groups = await User.getGroupsMembership(username)
|
||||||
|
const roles = await User.getRoles(username, groups)
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
...adUser,
|
||||||
|
roles,
|
||||||
|
groups
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.upsertUser({
|
||||||
|
where: {
|
||||||
|
sAMAccountName: user.sAMAccountName
|
||||||
|
},
|
||||||
|
update: user,
|
||||||
|
create: user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attemps to login user and returns AuthPayload data
|
||||||
|
* @param username
|
||||||
|
* @param password
|
||||||
|
* @returns {Promise<{expiresIn: String, user: User, token: String}>}
|
||||||
|
*/
|
||||||
|
static async login(username, password) {
|
||||||
|
try {
|
||||||
|
await ad.authenticate('ifms\\' + username, password)
|
||||||
|
} catch (err) {
|
||||||
|
throw (await ad.checkBinding())
|
||||||
|
? new Error('Usuário ou senha inválidos.')
|
||||||
|
: new Error('Problemas técnicos ao autenticar. Tente mais tarde.')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await new User(username).init()
|
||||||
|
|
||||||
|
await prisma.updateUser({
|
||||||
|
where: { sAMAccountName: user.sAMAccountName },
|
||||||
|
data: {
|
||||||
|
lastLoginPrior: user.lastLogin || new Date(),
|
||||||
|
lastLogin: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
sAMAccountName: user.sAMAccountName,
|
||||||
|
pwdLastSet: user.pwdLastSet
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwtSecret = process.env.JWT_SECRET || 'a good secret'
|
||||||
|
|
||||||
|
//@TODO Add momentJs to calculate expiration
|
||||||
|
const options = { expiresIn: process.env.JWT_EXPIRATION || '72h' }
|
||||||
|
|
||||||
|
const token = jwt.sign(payload, jwtSecret, options)
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
expiresIn: options.expiresIn
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
'Erro ao atualizar dados do usuário. Informe o departamento de TI.'
|
||||||
|
) // is DB connection OK?!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importAllUsers() {
|
||||||
|
try {
|
||||||
|
console.log('Obtendo usuários do AD...')
|
||||||
|
|
||||||
|
const allAdUsers = await ad.findUsers({
|
||||||
|
paged: true,
|
||||||
|
filter: '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' // Only active users
|
||||||
|
})
|
||||||
|
|
||||||
|
// const allAdUsers = await ad.getUsersForGroup('Estudantes')
|
||||||
|
// const allAdUsers = [await ad.findUser('aluno.teste')]
|
||||||
|
|
||||||
|
const dbUsers = []
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
for (let user of allAdUsers) {
|
||||||
|
const dbUser = await prisma.upsertUser({
|
||||||
|
where: {
|
||||||
|
sAMAccountName: user.sAMAccountName
|
||||||
|
},
|
||||||
|
update: user,
|
||||||
|
create: user
|
||||||
|
})
|
||||||
|
|
||||||
|
dbUsers.push(dbUser)
|
||||||
|
i++
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
`\rImportando usuário ${i} de ${allAdUsers.length}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nOK!')
|
||||||
|
|
||||||
|
return dbUsers
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Erro ao importar usuários: ' + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { User }
|
35
server/src/cronTasks.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import cron from 'node-cron'
|
||||||
|
import { updateDBWithOnlineDevices } from './utils/wifiUtils'
|
||||||
|
import { User } from './classes/User'
|
||||||
|
|
||||||
|
console.log('Scheduling Tasks...')
|
||||||
|
|
||||||
|
cron.schedule(
|
||||||
|
'*/5 * * * *',
|
||||||
|
async () => {
|
||||||
|
console.log(
|
||||||
|
'cron exec: updateDBWithOnlineDevices started at' +
|
||||||
|
new Date().toTimeString()
|
||||||
|
)
|
||||||
|
await updateDBWithOnlineDevices()
|
||||||
|
console.log(
|
||||||
|
'cron exec: updateDBWithOnlineDevices finished at' +
|
||||||
|
new Date().toTimeString()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
cron.schedule(
|
||||||
|
'0 0 0 * * *',
|
||||||
|
async () => {
|
||||||
|
console.log(
|
||||||
|
'cron exec: User.importAllUsers started at ' + new Date().toTimeString()
|
||||||
|
)
|
||||||
|
await User.importAllUsers()
|
||||||
|
console.log(
|
||||||
|
'cron exec: User.importAllUsers finished at ' + new Date().toTimeString()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
12
server/src/datamodels/resetToken.prisma
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
type ResetToken {
|
||||||
|
id: ID! @id
|
||||||
|
user: User! @relation(name: "ResetTokenToUser", onDelete: SET_NULL)
|
||||||
|
creator: User! @relation(name: "ResetTokenToCreator", onDelete: SET_NULL)
|
||||||
|
token: String! @unique
|
||||||
|
expiration: DateTime!
|
||||||
|
|
||||||
|
usedAt: DateTime
|
||||||
|
|
||||||
|
createdAt: DateTime! @createdAt
|
||||||
|
updatedAt: DateTime! @updatedAt
|
||||||
|
}
|
54
server/src/datamodels/user.prisma
Executable file
|
@ -0,0 +1,54 @@
|
||||||
|
type User {
|
||||||
|
id: ID! @id
|
||||||
|
lastLogin: DateTime
|
||||||
|
lastLoginPrior: DateTime
|
||||||
|
roles: Json
|
||||||
|
groups: Json
|
||||||
|
|
||||||
|
resetTokens: [ResetToken!] @relation(name: "ResetTokenToUser", onDelete: CASCADE)
|
||||||
|
createdResetTokens: [ResetToken!] @relation(name: "ResetTokenToCreator", onDelete: CASCADE)
|
||||||
|
wifiDevices: [WifiDevice!] @relation(name: "WifiDeviceToUser")
|
||||||
|
|
||||||
|
createdAt: DateTime! @createdAt
|
||||||
|
updatedAt: DateTime! @updatedAt
|
||||||
|
|
||||||
|
#AD fields
|
||||||
|
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: String
|
||||||
|
sAMAccountName: String! @unique
|
||||||
|
sAMAccountType: String
|
||||||
|
sn: String
|
||||||
|
thumbnailPhoto: String
|
||||||
|
title: String
|
||||||
|
userAccountControl: String
|
||||||
|
userPrincipalName: String
|
||||||
|
whenChanged: String
|
||||||
|
whenCreated: String
|
||||||
|
}
|
22
server/src/datamodels/wifiDevice.prisma
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
type WifiDevice {
|
||||||
|
id: ID! @id
|
||||||
|
user: User @relation(name: "WifiDeviceToUser")
|
||||||
|
oui: String
|
||||||
|
mac: String @unique
|
||||||
|
hostname: String
|
||||||
|
firstSeen: String
|
||||||
|
lastSeen: String
|
||||||
|
essid: String
|
||||||
|
ip: String
|
||||||
|
uptime: String
|
||||||
|
apName: String
|
||||||
|
status: Status
|
||||||
|
|
||||||
|
createdAt: DateTime! @createdAt
|
||||||
|
updatedAt: DateTime! @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
ONLINE
|
||||||
|
OFFLINE
|
||||||
|
}
|
21
server/src/index.js
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
import {} from 'dotenv/config'
|
||||||
|
import '@babel/polyfill/noConflict'
|
||||||
|
import './utils/capitalize'
|
||||||
|
|
||||||
|
import { server } from './server'
|
||||||
|
|
||||||
|
import './cronTasks'
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? 'Running in production'
|
||||||
|
: 'Running in development'
|
||||||
|
)
|
||||||
|
|
||||||
|
server.listen().then(options => {
|
||||||
|
console.log(
|
||||||
|
`\n---\nServer ready!`,
|
||||||
|
`\nEndpoint: ${options.url}graphql:${options.port} `,
|
||||||
|
`\nWebSocket: ${options.subscriptionsUrl}\n---`
|
||||||
|
)
|
||||||
|
})
|
5
server/src/resolvers/Group.js
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
const Group = {
|
||||||
|
members: (_, args, { ad }) => ad.getUsersForGroup(_.cn)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Group }
|
35
server/src/resolvers/Mutation.js
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
import { replacePassword } from '../utils/activedirectory/passwordUtils'
|
||||||
|
import { User } from '../classes/User'
|
||||||
|
import { ResetToken } from '../classes/ResetToken'
|
||||||
|
|
||||||
|
const Mutation = {
|
||||||
|
async login(_, { data }) {
|
||||||
|
return User.login(data.username, data.password)
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePassword(_, { data }, { auth }) {
|
||||||
|
return auth.updatePassword(data.oldPassword, data.newPassword)
|
||||||
|
},
|
||||||
|
|
||||||
|
async replacePassword(_, { data }) {
|
||||||
|
return replacePassword(data.username, data.newPassword)
|
||||||
|
},
|
||||||
|
|
||||||
|
async createResetToken(_, { data }, { auth }) {
|
||||||
|
return ResetToken.createToken(data.username, auth.sAMAccountName)
|
||||||
|
},
|
||||||
|
|
||||||
|
async useResetToken(_, { data }) {
|
||||||
|
return ResetToken.useToken(data.token, data.newPassword)
|
||||||
|
},
|
||||||
|
|
||||||
|
async importUsers() {
|
||||||
|
User.importAllUsers()
|
||||||
|
.then(users => console.log('OK', users.length))
|
||||||
|
.catch(console.log)
|
||||||
|
|
||||||
|
return 'A importação está sendo feita. Isso pode demorar alguns minutos.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Mutation }
|
155
server/src/resolvers/Query.js
Executable file
|
@ -0,0 +1,155 @@
|
||||||
|
import { prisma } from '../generated/prisma-client'
|
||||||
|
import { updateDBWithOnlineDevices } from '../utils/wifiUtils'
|
||||||
|
import { User } from '../classes/User'
|
||||||
|
import { gql } from 'apollo-server'
|
||||||
|
|
||||||
|
const parseSAMAccountName = sAMAccountName =>
|
||||||
|
sAMAccountName ? sAMAccountName.replace('.', ' ') : ''
|
||||||
|
|
||||||
|
const Query = {
|
||||||
|
async me(_, args, { auth }) {
|
||||||
|
return auth
|
||||||
|
},
|
||||||
|
|
||||||
|
async basicUser(_, { sAMAccountName }) {
|
||||||
|
if (sAMAccountName === '') throw new Error('Argumento inválido')
|
||||||
|
|
||||||
|
const user = await new User(sAMAccountName).init()
|
||||||
|
|
||||||
|
if (!user) throw new Error('Usuário não encontrado.')
|
||||||
|
|
||||||
|
return {
|
||||||
|
sAMAccountName: user.sAMAccountName,
|
||||||
|
displayName: user.displayName,
|
||||||
|
thumbnailPhoto: user.thumbnailPhoto,
|
||||||
|
roles: user.roles
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async users(
|
||||||
|
_,
|
||||||
|
{ where: { cn, displayName, sAMAccountName }, limit, onlyStudents },
|
||||||
|
{ ad }
|
||||||
|
) {
|
||||||
|
const parsedSAMAccountName = parseSAMAccountName(sAMAccountName)
|
||||||
|
|
||||||
|
const studentGroup = process.env.STUDENT_GROUP || 'Estudantes'
|
||||||
|
|
||||||
|
const search = `|(cn=*${parsedSAMAccountName}*)(cn=*${cn}*)(displayName=*${displayName}*)`
|
||||||
|
|
||||||
|
const filter = onlyStudents
|
||||||
|
? `(&(memberOf=CN=${studentGroup},OU=Groups,DC=ifms,DC=edu,DC=br)(${search}))`
|
||||||
|
: `(${search})`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const users = await ad.findUsers({
|
||||||
|
filter,
|
||||||
|
sizeLimit: limit
|
||||||
|
})
|
||||||
|
|
||||||
|
return users.map(user => ({ ...user, roles: [] })) || []
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Não foi possível realizar a busca.')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async user(_, { sAMAccountName }) {
|
||||||
|
if (!sAMAccountName)
|
||||||
|
throw new Error('Busca vazia. Informe uma conta de usuário.')
|
||||||
|
try {
|
||||||
|
const user = await new User(sAMAccountName).init()
|
||||||
|
return user || new Error('Usuário não encontrado')
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async groups(_, { where, limit }, { ad }) {
|
||||||
|
const groups = await ad.findGroups({
|
||||||
|
filter: `(|(cn=*${where.cn}*)(name=*${where.name}*)(dn=*${where.dn}*))`,
|
||||||
|
sizeLimit: limit
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups || []
|
||||||
|
},
|
||||||
|
|
||||||
|
async stats() {
|
||||||
|
return {
|
||||||
|
tokenCountTotal: prisma.resetTokensConnection().aggregate().count(),
|
||||||
|
tokenCountUsed: prisma
|
||||||
|
.resetTokensConnection({ where: { usedAt_not: null } })
|
||||||
|
.aggregate()
|
||||||
|
.count(),
|
||||||
|
tokenCountExpired: prisma
|
||||||
|
.resetTokensConnection({ where: { expiration_lte: new Date() } })
|
||||||
|
.aggregate()
|
||||||
|
.count(),
|
||||||
|
tokenCountNotUsed: prisma
|
||||||
|
.resetTokensConnection({ where: { usedAt: null } })
|
||||||
|
.aggregate()
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
wifiDevices: async (_, { identifiedOnly }) => {
|
||||||
|
await updateDBWithOnlineDevices()
|
||||||
|
// updateDBWithOnlineDevices().then(success=>{
|
||||||
|
// if(success)
|
||||||
|
// console.log('PubSub') //TODO: PubSub
|
||||||
|
// })
|
||||||
|
|
||||||
|
return prisma.wifiDevices({
|
||||||
|
orderBy: 'lastSeen_DESC',
|
||||||
|
where: identifiedOnly ? { NOT: { user: null } } : {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
userPresence: async (_, { search }) => {
|
||||||
|
if (!search) {
|
||||||
|
await updateDBWithOnlineDevices()
|
||||||
|
search = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersWithWifiDevices = await prisma.users({
|
||||||
|
where: {
|
||||||
|
wifiDevices_some: {
|
||||||
|
id_not: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).$fragment(gql`
|
||||||
|
fragment HydrateUser on User {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
thumbnailPhoto
|
||||||
|
wifiDevices {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
lastSeen
|
||||||
|
apName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
const userPresences = usersWithWifiDevices
|
||||||
|
.filter(user =>
|
||||||
|
user.displayName.toLowerCase().includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
.map(user => ({
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
displayName: user.displayName,
|
||||||
|
thumbnailPhoto: user.thumbnailPhoto
|
||||||
|
},
|
||||||
|
wifiDevices: user.wifiDevices.sort((a, b) =>
|
||||||
|
a.lastSeen > b.lastSeen ? -1 : 1
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const sortedUserPresences = userPresences.sort((a, b) =>
|
||||||
|
a.wifiDevices[0].lastSeen > b.wifiDevices[0].lastSeen ? -1 : 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return sortedUserPresences.slice(0, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Query }
|
12
server/src/resolvers/ResetToken.js
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
import { prisma } from '../generated/prisma-client'
|
||||||
|
|
||||||
|
const ResetToken = {
|
||||||
|
creator(_) {
|
||||||
|
return prisma.resetToken({ id: _.id }).creator()
|
||||||
|
},
|
||||||
|
user(_) {
|
||||||
|
return prisma.resetToken({ id: _.id }).user()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ResetToken }
|
48
server/src/resolvers/User.js
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
import { prisma } from '../generated/prisma-client'
|
||||||
|
|
||||||
|
const User = {
|
||||||
|
firstName: _ => _.displayName.split(' ')[0],
|
||||||
|
displayName: _ => (_.displayName ? _.displayName.capitalize() : ''),
|
||||||
|
|
||||||
|
groups: async (_, args, { ad }) =>
|
||||||
|
_.groups ? _.groups : ad.getGroupMembershipForUser(_.sAMAccountName),
|
||||||
|
|
||||||
|
sharedFolders: _ =>
|
||||||
|
_.groups
|
||||||
|
? _.groups
|
||||||
|
.filter(group => group.cn.includes('-Share-'))
|
||||||
|
.map(group => group.cn.split('-')[2])
|
||||||
|
: [],
|
||||||
|
|
||||||
|
sharedPrinters: _ =>
|
||||||
|
_.groups
|
||||||
|
? _.groups
|
||||||
|
.filter(group => group.cn.includes('-Printer-'))
|
||||||
|
.map(group => group.cn.split('-')[2])
|
||||||
|
: [],
|
||||||
|
|
||||||
|
isSuperAdmin: _ => _.roles.includes('superAdmin'),
|
||||||
|
isTokenCreator: _ => _.roles.includes('tokenCreator'),
|
||||||
|
isServant: _ => _.roles.includes('servant'),
|
||||||
|
isStudent: _ => _.roles.includes('student'),
|
||||||
|
isWatcher: _ => _.roles.includes('watcher'),
|
||||||
|
|
||||||
|
wifiDevices: (_, data, { auth }) => {
|
||||||
|
if (
|
||||||
|
_.sAMAccountName !== auth.sAMAccountName &&
|
||||||
|
!auth.roles.includes('superAdmin')
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
return prisma.wifiDevices({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
sAMAccountName: _.sAMAccountName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: 'lastSeen_DESC'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { User }
|
7
server/src/resolvers/WifiDevice.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { prisma } from '../generated/prisma-client'
|
||||||
|
|
||||||
|
const WifiDevice = {
|
||||||
|
user: _ => prisma.wifiDevice({ id: _.id }).user()
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WifiDevice }
|
18
server/src/resolvers/index.js
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Query } from './Query'
|
||||||
|
import { Mutation } from './Mutation'
|
||||||
|
|
||||||
|
import { User } from './User'
|
||||||
|
import { Group } from './Group'
|
||||||
|
import { ResetToken } from './ResetToken'
|
||||||
|
import { WifiDevice } from './WifiDevice'
|
||||||
|
|
||||||
|
const resolvers = {
|
||||||
|
Query,
|
||||||
|
Mutation,
|
||||||
|
User,
|
||||||
|
Group,
|
||||||
|
ResetToken,
|
||||||
|
WifiDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
export { resolvers }
|
47
server/src/schemaDirectives/AuthDirective.js
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
import { SchemaDirectiveVisitor } from 'apollo-server'
|
||||||
|
import { defaultFieldResolver } from 'graphql'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
|
import { User } from '../classes/User'
|
||||||
|
|
||||||
|
class AuthDirective extends SchemaDirectiveVisitor {
|
||||||
|
visitFieldDefinition(field, details) {
|
||||||
|
const { resolve = defaultFieldResolver } = field
|
||||||
|
|
||||||
|
field.resolve = async (...args) => {
|
||||||
|
const [, , context] = args
|
||||||
|
|
||||||
|
const { roles: expectedRoles = [] } = this.args
|
||||||
|
|
||||||
|
const authorizationHeader = context.req
|
||||||
|
? context.req.headers.authorization
|
||||||
|
: context.connection.context.Authorization // TODO: check if work with subscriptions
|
||||||
|
|
||||||
|
if (authorizationHeader) {
|
||||||
|
const token = authorizationHeader.replace('Bearer ', '')
|
||||||
|
const { sAMAccountName, pwdLastSet } = jwt.verify(
|
||||||
|
token,
|
||||||
|
process.env.JWT_SECRET
|
||||||
|
)
|
||||||
|
|
||||||
|
const user = await new User(sAMAccountName).init()
|
||||||
|
|
||||||
|
context.auth = user
|
||||||
|
|
||||||
|
if (user.pwdLastSet === pwdLastSet) {
|
||||||
|
if (
|
||||||
|
expectedRoles.length === 0 ||
|
||||||
|
expectedRoles.some(role => user.roles.includes(role))
|
||||||
|
)
|
||||||
|
return resolve.apply(this, args)
|
||||||
|
|
||||||
|
throw new Error('Você não tem permissão para fazer isso')
|
||||||
|
} else throw new Error('O token utilizado foi invalidado')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('É necessário fazer login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AuthDirective }
|
7
server/src/schemaDirectives/index.js
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
import { AuthDirective } from './AuthDirective'
|
||||||
|
|
||||||
|
const schemaDirectives = {
|
||||||
|
auth: AuthDirective
|
||||||
|
}
|
||||||
|
|
||||||
|
export { schemaDirectives }
|
36
server/src/server.js
Executable file
|
@ -0,0 +1,36 @@
|
||||||
|
import { ApolloServer } from 'apollo-server'
|
||||||
|
import responseCachePlugin from 'apollo-server-plugin-response-cache'
|
||||||
|
|
||||||
|
import { prisma as db } from './generated/prisma-client'
|
||||||
|
import { ad } from './utils/activedirectory'
|
||||||
|
|
||||||
|
import { typeDefs } from './typeDefs'
|
||||||
|
import { resolvers } from './resolvers'
|
||||||
|
import { schemaDirectives } from './schemaDirectives'
|
||||||
|
|
||||||
|
const server = new ApolloServer({
|
||||||
|
cors:
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? {
|
||||||
|
origin: ['http://ti.pp.ifms.edu.br']
|
||||||
|
}
|
||||||
|
: true,
|
||||||
|
typeDefs,
|
||||||
|
resolvers,
|
||||||
|
schemaDirectives,
|
||||||
|
context: ({ req }) => {
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
ad,
|
||||||
|
req
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
responseCachePlugin({
|
||||||
|
sessionId: requestContext =>
|
||||||
|
requestContext.request.http.headers.get('Authorization') || null
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export { server }
|
202
server/src/typeDefs.js
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import { gql } from 'apollo-server'
|
||||||
|
|
||||||
|
const typeDefs = gql`
|
||||||
|
type Query {
|
||||||
|
"Returns only a few fields of User"
|
||||||
|
basicUser(sAMAccountName: String!): User! @cacheControl(maxAge: 350)
|
||||||
|
|
||||||
|
me: User! @auth @cacheControl(maxAge: 30, scope: PRIVATE)
|
||||||
|
|
||||||
|
users(
|
||||||
|
where: UserWhereInput!
|
||||||
|
limit: Int = 15
|
||||||
|
onlyStudents: Boolean = false
|
||||||
|
): [User!] @auth(roles: ["servant"]) @cacheControl(maxAge: 350)
|
||||||
|
|
||||||
|
user(sAMAccountName: String!): User!
|
||||||
|
@auth(roles: ["superAdmin"])
|
||||||
|
@cacheControl(maxAge: 350)
|
||||||
|
|
||||||
|
groups(where: GroupWhereInput!, limit: Int = 10): [Group!]!
|
||||||
|
@auth(roles: ["servant"])
|
||||||
|
@cacheControl(maxAge: 350)
|
||||||
|
|
||||||
|
stats: Stats!
|
||||||
|
|
||||||
|
wifiDevices(
|
||||||
|
search: String = ""
|
||||||
|
identifiedOnly: Boolean = true
|
||||||
|
): [WifiDevice]!
|
||||||
|
|
||||||
|
userPresence(search: String): [UserPresence!] @auth(roles: ["watcher"])
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
login(data: LoginInput!): AuthPayload!
|
||||||
|
updatePassword(data: UpdatePasswordInput!): AuthPayload! @auth
|
||||||
|
replacePassword(data: ReplacePasswordInput!): String!
|
||||||
|
@auth(roles: ["superAdmin"])
|
||||||
|
createResetToken(data: CreateResetTokenInput!): ResetToken!
|
||||||
|
@auth(roles: ["superAdmin", "tokenCreator"])
|
||||||
|
useResetToken(data: UseResetTokenInput!): Boolean!
|
||||||
|
importUsers: String! @auth(roles: ["superAdmin"])
|
||||||
|
}
|
||||||
|
|
||||||
|
directive @auth(roles: [String!]) on FIELD_DEFINITION
|
||||||
|
|
||||||
|
"A mix between the database User and the Active Directory User"
|
||||||
|
type User @cacheControl(maxAge: 350, scope: PRIVATE) {
|
||||||
|
id: ID
|
||||||
|
|
||||||
|
wifiDevices: [WifiDevice!]
|
||||||
|
|
||||||
|
lastLogin: String
|
||||||
|
lastLoginPrior: String
|
||||||
|
roles: [String!]
|
||||||
|
groups: [Group!]
|
||||||
|
sharedFolders: [String!]
|
||||||
|
sharedPrinters: [String!]
|
||||||
|
|
||||||
|
firstName: String
|
||||||
|
|
||||||
|
isSuperAdmin: Boolean!
|
||||||
|
isTokenCreator: Boolean!
|
||||||
|
isServant: Boolean!
|
||||||
|
isStudent: Boolean!
|
||||||
|
isWatcher: Boolean!
|
||||||
|
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
|
||||||
|
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
|
||||||
|
lastLogon: String
|
||||||
|
lastLogonTimestamp: String
|
||||||
|
lockoutTime: String
|
||||||
|
logonCount: String
|
||||||
|
mail: String
|
||||||
|
name: String
|
||||||
|
objectCategory: String
|
||||||
|
objectGUID: String
|
||||||
|
objectSid: String
|
||||||
|
primaryGroupID: String
|
||||||
|
pwdLastSet: String
|
||||||
|
sAMAccountName: String!
|
||||||
|
sAMAccountType: String
|
||||||
|
sn: String
|
||||||
|
thumbnailPhoto: String
|
||||||
|
title: String
|
||||||
|
userAccountControl: String
|
||||||
|
userPrincipalName: String
|
||||||
|
whenChanged: String
|
||||||
|
whenCreated: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group {
|
||||||
|
cn: String!
|
||||||
|
dn: String!
|
||||||
|
name: String
|
||||||
|
members: [User!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthPayload {
|
||||||
|
user: User!
|
||||||
|
token: String!
|
||||||
|
expiresIn: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetToken {
|
||||||
|
id: ID!
|
||||||
|
user: User!
|
||||||
|
creator: User!
|
||||||
|
token: String!
|
||||||
|
expiration: String!
|
||||||
|
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stats {
|
||||||
|
tokenCountTotal: Int!
|
||||||
|
tokenCountUsed: Int!
|
||||||
|
tokenCountExpired: Int!
|
||||||
|
tokenCountNotUsed: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type WifiDevice {
|
||||||
|
user: User
|
||||||
|
id: String
|
||||||
|
oui: String
|
||||||
|
mac: String
|
||||||
|
hostname: String
|
||||||
|
firstSeen: String
|
||||||
|
lastSeen: String
|
||||||
|
essid: String
|
||||||
|
ip: String
|
||||||
|
uptime: String
|
||||||
|
apName: String
|
||||||
|
status: Status
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserPresence {
|
||||||
|
user: User!
|
||||||
|
wifiDevices: [WifiDevice!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
ONLINE
|
||||||
|
OFFLINE
|
||||||
|
}
|
||||||
|
|
||||||
|
input LoginInput {
|
||||||
|
username: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
input UpdatePasswordInput {
|
||||||
|
oldPassword: String!
|
||||||
|
newPassword: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ReplacePasswordInput {
|
||||||
|
username: String!
|
||||||
|
newPassword: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UserWhereInput {
|
||||||
|
cn: String
|
||||||
|
displayName: String
|
||||||
|
sAMAccountName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input GroupWhereInput {
|
||||||
|
cn: String
|
||||||
|
dn: String
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateResetTokenInput {
|
||||||
|
username: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UseResetTokenInput {
|
||||||
|
token: String!
|
||||||
|
newPassword: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export { typeDefs }
|
44
server/src/utils/activedirectory/attributes.js
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
const attributes = {
|
||||||
|
user: [
|
||||||
|
'accountExpires',
|
||||||
|
'badPasswordTime',
|
||||||
|
'badPwdCount',
|
||||||
|
'cn',
|
||||||
|
'department',
|
||||||
|
'description',
|
||||||
|
'displayName',
|
||||||
|
'distinguishedName',
|
||||||
|
'dn',
|
||||||
|
'extensionAttribute1',
|
||||||
|
'extensionAttribute10',
|
||||||
|
'extensionAttribute2',
|
||||||
|
'extensionAttribute6',
|
||||||
|
'extensionAttribute7',
|
||||||
|
'givenName',
|
||||||
|
'homeDirectory',
|
||||||
|
'homeDrive',
|
||||||
|
// 'lastLogoff',
|
||||||
|
'lastLogon',
|
||||||
|
'lastLogonTimestamp',
|
||||||
|
'lockoutTime',
|
||||||
|
'logonCount',
|
||||||
|
'mail',
|
||||||
|
// 'memberOf',
|
||||||
|
'name',
|
||||||
|
'objectCategory',
|
||||||
|
'primaryGroupID',
|
||||||
|
'pwdLastSet',
|
||||||
|
'sAMAccountName',
|
||||||
|
'sAMAccountType',
|
||||||
|
'sn',
|
||||||
|
'thumbnailPhoto',
|
||||||
|
'title',
|
||||||
|
'userAccountControl',
|
||||||
|
'userPrincipalName',
|
||||||
|
'whenChanged',
|
||||||
|
'whenCreated'
|
||||||
|
],
|
||||||
|
group: ['*']
|
||||||
|
}
|
||||||
|
|
||||||
|
export { attributes }
|
14
server/src/utils/activedirectory/config.js
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
import { attributes } from './attributes'
|
||||||
|
import { entryParser } from './entryParser'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
url: process.env.AD_URL || 'ldap://ifms.edu.br',
|
||||||
|
baseDN: process.env.AD_BASE_DN || 'dc=ifms,dc=edu,dc=br',
|
||||||
|
username: process.env.AD_BIND_USER || 'ifms\\user',
|
||||||
|
password: process.env.AD_BIND_PASSWORD || 'password',
|
||||||
|
tlsOptions: { rejectUnauthorized: false },
|
||||||
|
attributes,
|
||||||
|
entryParser
|
||||||
|
}
|
||||||
|
|
||||||
|
export { config as default }
|
19
server/src/utils/activedirectory/encodePassword.js
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Encodes a password
|
||||||
|
* @param password
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const encodePassword = password => {
|
||||||
|
let encodedPassword = ''
|
||||||
|
password = '"' + password + '"'
|
||||||
|
|
||||||
|
for (let i = 0; i < password.length; i++)
|
||||||
|
encodedPassword += String.fromCharCode(
|
||||||
|
password.charCodeAt(i) & 0xff,
|
||||||
|
(password.charCodeAt(i) >>> 8) & 0xff
|
||||||
|
)
|
||||||
|
|
||||||
|
return encodedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
export { encodePassword }
|
29
server/src/utils/activedirectory/entryParser.js
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
const entryParser = (entry, raw, callback) => {
|
||||||
|
if (raw.hasOwnProperty('thumbnailPhoto'))
|
||||||
|
entry.thumbnailPhoto = `data:image/png;base64,${raw.thumbnailPhoto.toString(
|
||||||
|
'base64'
|
||||||
|
)}`
|
||||||
|
|
||||||
|
if (raw.hasOwnProperty('pwdLastSet'))
|
||||||
|
entry.pwdLastSet = ldapTimeToJSTime(raw.pwdLastSet)
|
||||||
|
|
||||||
|
if (raw.hasOwnProperty('badPasswordTime'))
|
||||||
|
entry.badPasswordTime = ldapTimeToJSTime(raw.badPasswordTime)
|
||||||
|
|
||||||
|
if (raw.hasOwnProperty('lastLogon'))
|
||||||
|
entry.lastLogon = ldapTimeToJSTime(raw.lastLogon)
|
||||||
|
|
||||||
|
if (raw.hasOwnProperty('lastLogonTimestamp'))
|
||||||
|
entry.lastLogonTimestamp = ldapTimeToJSTime(raw.lastLogonTimestamp)
|
||||||
|
|
||||||
|
if (raw.hasOwnProperty('lockoutTime') && entry.lockoutTime !== '0')
|
||||||
|
entry.lockoutTime = ldapTimeToJSTime(raw.lockoutTime)
|
||||||
|
|
||||||
|
callback(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ldapTimeToJSTime(ldapTime) {
|
||||||
|
return new Date(ldapTime / 1e4 - 1.16444736e13)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { entryParser }
|
18
server/src/utils/activedirectory/index.js
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
import { promiseWrapper as AD } from 'activedirectory2'
|
||||||
|
import config from './config'
|
||||||
|
|
||||||
|
const ad = new AD(config)
|
||||||
|
|
||||||
|
ad.checkBinding = async () => {
|
||||||
|
try {
|
||||||
|
await ad.authenticate(
|
||||||
|
process.env.AD_BIND_USER,
|
||||||
|
process.env.AD_BIND_PASSWORD
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ad }
|
77
server/src/utils/activedirectory/passwordUtils.js
Executable file
|
@ -0,0 +1,77 @@
|
||||||
|
import config from './config'
|
||||||
|
import { createClient, Change } from 'ldapjs'
|
||||||
|
import { User } from '../../classes/User'
|
||||||
|
import { encodePassword } from './encodePassword'
|
||||||
|
|
||||||
|
const replacePassword = (username, newPassword) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = createClient(config)
|
||||||
|
|
||||||
|
client.on('error', err => {
|
||||||
|
reject(new Error(err))
|
||||||
|
})
|
||||||
|
|
||||||
|
client.bind(config.username, config.password, (err, result) => {
|
||||||
|
if (!err) {
|
||||||
|
client.search(
|
||||||
|
config.baseDN,
|
||||||
|
{
|
||||||
|
filter: `(sAMAccountName=${username})`,
|
||||||
|
attributes: 'dn',
|
||||||
|
scope: 'sub'
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
res.on('error', err => {
|
||||||
|
reject(new Error(err.message))
|
||||||
|
})
|
||||||
|
|
||||||
|
let theEntry = null
|
||||||
|
|
||||||
|
res.on('searchEntry', entry => {
|
||||||
|
theEntry = entry
|
||||||
|
|
||||||
|
const userDN = entry.object.dn
|
||||||
|
|
||||||
|
client.modify(
|
||||||
|
userDN,
|
||||||
|
[
|
||||||
|
new Change({
|
||||||
|
operation: 'replace',
|
||||||
|
modification: {
|
||||||
|
unicodePwd: encodePassword(newPassword)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
(err, result) => {
|
||||||
|
if (err) {
|
||||||
|
client.unbind(() => {
|
||||||
|
console.log('Client unbinded due error.')
|
||||||
|
})
|
||||||
|
reject(new Error(err.message))
|
||||||
|
} else {
|
||||||
|
User.upsertUser(username)
|
||||||
|
resolve('Senha alterada com sucesso!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
//@TODO Disparar erro para usuário não encontrado. Solução temporária abaixo
|
||||||
|
res.on('end', result => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!theEntry)
|
||||||
|
reject(
|
||||||
|
new Error('Timeout. Provavelmente o usuário não existe')
|
||||||
|
)
|
||||||
|
}, 10000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
reject('Problemas técnicos. Tente mais tarde.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { replacePassword }
|
5
server/src/utils/capitalize.js
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
String.prototype.capitalize = function(lower = true) {
|
||||||
|
return (lower ? this.toLowerCase() : this).replace(/(?:^|\s)\S/g, a =>
|
||||||
|
a.toUpperCase()
|
||||||
|
)
|
||||||
|
}
|
16
server/src/utils/saveJSONToFile.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
function saveJSONToFile(json, fileName = 'output.json') {
|
||||||
|
const jsonContent = JSON.stringify(json)
|
||||||
|
|
||||||
|
fs.writeFile(fileName, jsonContent, 'utf8', function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.log('An error occured while writing JSON Object to File.')
|
||||||
|
return console.log(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('JSON file has been saved.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { saveJSONToFile }
|
96
server/src/utils/unifiController.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import unifi from 'node-unifi'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
const unifiController = new unifi.Controller(
|
||||||
|
process.env.UNIFI_URL || 'unifi.pp.ifms.edu.br',
|
||||||
|
process.env.UNIFI_PORT || 8443
|
||||||
|
)
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'authorizeGuest',
|
||||||
|
'unauthorizeGuest',
|
||||||
|
'reconnectClient',
|
||||||
|
'blockClient',
|
||||||
|
'unblockClient',
|
||||||
|
'setClientNote',
|
||||||
|
'setClientName',
|
||||||
|
'getDailySiteStats',
|
||||||
|
'getHourlySiteStats',
|
||||||
|
'getHourlyApStats',
|
||||||
|
'getDailyApStats',
|
||||||
|
'getSessions',
|
||||||
|
'getLatestSessions',
|
||||||
|
'getAllAuthorizations',
|
||||||
|
'getAllUsers',
|
||||||
|
'getBlockedUsers',
|
||||||
|
'getGuests',
|
||||||
|
'getClientDevices',
|
||||||
|
'getClientDevice',
|
||||||
|
'getUserGroups',
|
||||||
|
'setUserGroup',
|
||||||
|
'editUserGroup',
|
||||||
|
'addUserGroup',
|
||||||
|
'deleteUserGroup',
|
||||||
|
'getHealth',
|
||||||
|
'getDashboard',
|
||||||
|
'getUsers',
|
||||||
|
'getAccessDevices',
|
||||||
|
'getRogueAccessPoints',
|
||||||
|
'getSites',
|
||||||
|
'getSitesStats',
|
||||||
|
'addSite',
|
||||||
|
'deleteSite',
|
||||||
|
'listAdmins',
|
||||||
|
'getWLanGroups',
|
||||||
|
'getSiteSysinfo',
|
||||||
|
'getSelf',
|
||||||
|
'getNetworkConf',
|
||||||
|
'getVouchers',
|
||||||
|
'getPayments',
|
||||||
|
'createHotspotOperator',
|
||||||
|
'getHotspotOperators',
|
||||||
|
'createVouchers',
|
||||||
|
'revokeVoucher',
|
||||||
|
'extendGuestValidity',
|
||||||
|
'getPortForwardingStats',
|
||||||
|
'getDPIStats',
|
||||||
|
'getCurrentChannels',
|
||||||
|
'getPortForwarding',
|
||||||
|
'getDynamicDNS',
|
||||||
|
'getPortConfig',
|
||||||
|
'getVoipExtensions',
|
||||||
|
'getSiteSettings',
|
||||||
|
'adoptDevice',
|
||||||
|
'rebootAccessPoint',
|
||||||
|
'disableAccessPoint',
|
||||||
|
'setLEDOverride',
|
||||||
|
'setLocateAccessPoint',
|
||||||
|
'setSiteLEDs',
|
||||||
|
'setAccessPointRadioSettings',
|
||||||
|
'setGuestLoginSettings',
|
||||||
|
'renameAccessPoint',
|
||||||
|
'createWLan',
|
||||||
|
'deleteWLan',
|
||||||
|
'setWLanSettings',
|
||||||
|
'disableWLan',
|
||||||
|
'getEvents',
|
||||||
|
'getWLanSettings',
|
||||||
|
'getAlarms',
|
||||||
|
'upgradeDevice',
|
||||||
|
'upgradeDeviceExternal',
|
||||||
|
'runSpectrumScan',
|
||||||
|
'getSpectrumScanState',
|
||||||
|
'listRadiusAccounts',
|
||||||
|
'createBackup',
|
||||||
|
'upgradeExternalFirmware'
|
||||||
|
]
|
||||||
|
|
||||||
|
const controller = {}
|
||||||
|
|
||||||
|
for (let key of keys) {
|
||||||
|
controller[key] = promisify(unifiController[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
export { controller }
|
76
server/src/utils/wifiUtils.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { controller } from './unifiController'
|
||||||
|
import { prisma } from '../generated/prisma-client'
|
||||||
|
|
||||||
|
const getOnlineWifiDevices = async () => {
|
||||||
|
try {
|
||||||
|
await controller.login(process.env.UNIFI_USER, process.env.UNIFI_PASSWORD)
|
||||||
|
|
||||||
|
const accessPointsPromise = controller.getAccessDevices('default')
|
||||||
|
const onlineDevicesPromise = controller.getClientDevices('default')
|
||||||
|
|
||||||
|
const [accessPoints, onlineDevices] = await Promise.all([
|
||||||
|
accessPointsPromise,
|
||||||
|
onlineDevicesPromise
|
||||||
|
])
|
||||||
|
|
||||||
|
const onlineDevicesWithUser = onlineDevices[0].map(async client => ({
|
||||||
|
user: client['1x_identity'] || null,
|
||||||
|
oui: client.oui,
|
||||||
|
mac: client.mac,
|
||||||
|
hostname: client.hostname,
|
||||||
|
firstSeen: new Date(client.first_seen * 1000).toISOString(),
|
||||||
|
lastSeen: new Date(client.last_seen * 1000).toISOString(),
|
||||||
|
essid: client.essid,
|
||||||
|
ip: client.ip,
|
||||||
|
uptime: client.uptime.toString(),
|
||||||
|
apName: accessPoints[0].find(ap => ap.mac === client.ap_mac).name,
|
||||||
|
status: 'ONLINE'
|
||||||
|
}))
|
||||||
|
|
||||||
|
controller.logout()
|
||||||
|
|
||||||
|
return Promise.all(onlineDevicesWithUser)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Erro ao listar dispositivos. ' + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDBWithOnlineDevices = async () => {
|
||||||
|
let onlineDevices = await getOnlineWifiDevices()
|
||||||
|
|
||||||
|
await prisma.updateManyWifiDevices({
|
||||||
|
data: {
|
||||||
|
status: 'OFFLINE'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let device of onlineDevices) {
|
||||||
|
|
||||||
|
|
||||||
|
const newDevice = {
|
||||||
|
...device,
|
||||||
|
user: device.user
|
||||||
|
? {
|
||||||
|
connect: {
|
||||||
|
sAMAccountName: device.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.upsertWifiDevice(
|
||||||
|
{
|
||||||
|
where: { mac: device.mac },
|
||||||
|
create: newDevice,
|
||||||
|
update: newDevice
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getOnlineWifiDevices, updateDBWithOnlineDevices }
|
2
web/.browserslistrc
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
2
web/.env.development
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
VUE_APP_GRAPHQL_HTTP="http://192.168.0.9:4000/graphql"
|
||||||
|
VUE_APP_GRAPHQL_WS="ws://192.168.0.9:4000/graphql"
|
2
web/.env.production
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
VUE_APP_GRAPHQL_HTTP="http://ifms-pti-server.pp.ifms.edu.br/graphql"
|
||||||
|
VUE_APP_GRAPHQL_WS="ws://ifms-pti-server.pp.ifms.edu.br/graphql"
|
18
web/.eslintrc.js
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-debugger': 'off'
|
||||||
|
},
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint'
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: ['plugin:vue/recommended', '@vue/prettier']
|
||||||
|
}
|
21
web/.gitignore
vendored
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
12
web/.graphqlconfig
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"ifms-pti": {
|
||||||
|
"schemaPath": "./generated/schema.graphql",
|
||||||
|
"extensions": {
|
||||||
|
"endpoints": {
|
||||||
|
"dev": "http://localhost:4000/graphql"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
web/.prettierrc
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"excludeFiles": "dist/**"
|
||||||
|
}
|
18
web/Dockerfile
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# build stage
|
||||||
|
FROM node as build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# production stage
|
||||||
|
FROM nginx:stable-alpine as production-stage
|
||||||
|
|
||||||
|
RUN rm -rf /etc/nginx/conf.d
|
||||||
|
RUN mkdir -p /etc/nginx/conf.d
|
||||||
|
COPY ./default.conf /etc/nginx/conf.d/
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
25
web/README.md
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
# Portal de TI
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm get-schema
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
3
web/babel.config.js
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: ['@vue/cli-plugin-babel/preset']
|
||||||
|
}
|
12
web/default.conf
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
181
web/generated/schema.graphql
Executable file
|
@ -0,0 +1,181 @@
|
||||||
|
# source: http://localhost:4000/graphql
|
||||||
|
# timestamp: Thu May 07 2020 16:24:38 GMT-0400 (Amazon Standard Time)
|
||||||
|
|
||||||
|
directive @auth(roles: [String!]) on FIELD_DEFINITION
|
||||||
|
|
||||||
|
directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE
|
||||||
|
|
||||||
|
type AuthPayload {
|
||||||
|
user: User!
|
||||||
|
token: String!
|
||||||
|
expiresIn: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CacheControlScope {
|
||||||
|
PUBLIC
|
||||||
|
PRIVATE
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateResetTokenInput {
|
||||||
|
username: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group {
|
||||||
|
cn: String!
|
||||||
|
dn: String!
|
||||||
|
name: String
|
||||||
|
members: [User!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input GroupWhereInput {
|
||||||
|
cn: String
|
||||||
|
dn: String
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input LoginInput {
|
||||||
|
username: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
login(data: LoginInput!): AuthPayload!
|
||||||
|
updatePassword(data: UpdatePasswordInput!): AuthPayload!
|
||||||
|
replacePassword(data: ReplacePasswordInput!): String!
|
||||||
|
createResetToken(data: CreateResetTokenInput!): ResetToken!
|
||||||
|
useResetToken(data: UseResetTokenInput!): Boolean!
|
||||||
|
importUsers: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
"""Returns only a few fields of User"""
|
||||||
|
basicUser(sAMAccountName: String!): User!
|
||||||
|
me: 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]!
|
||||||
|
userPresence(search: String): [UserPresence!]
|
||||||
|
}
|
||||||
|
|
||||||
|
input ReplacePasswordInput {
|
||||||
|
username: String!
|
||||||
|
newPassword: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetToken {
|
||||||
|
id: ID!
|
||||||
|
user: User!
|
||||||
|
creator: User!
|
||||||
|
token: String!
|
||||||
|
expiration: String!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stats {
|
||||||
|
tokenCountTotal: Int!
|
||||||
|
tokenCountUsed: Int!
|
||||||
|
tokenCountExpired: Int!
|
||||||
|
tokenCountNotUsed: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
ONLINE
|
||||||
|
OFFLINE
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdatePasswordInput {
|
||||||
|
oldPassword: String!
|
||||||
|
newPassword: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""The `Upload` scalar type represents a file upload."""
|
||||||
|
scalar Upload
|
||||||
|
|
||||||
|
"""A mix between the database User and the Active Directory User"""
|
||||||
|
type User {
|
||||||
|
id: ID
|
||||||
|
wifiDevices: [WifiDevice!]
|
||||||
|
lastLogin: String
|
||||||
|
lastLoginPrior: String
|
||||||
|
roles: [String!]
|
||||||
|
firstName: String
|
||||||
|
groups: [Group!]
|
||||||
|
isSuperAdmin: Boolean!
|
||||||
|
isTokenCreator: Boolean!
|
||||||
|
isServant: Boolean!
|
||||||
|
isStudent: Boolean!
|
||||||
|
isWatcher: Boolean!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
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
|
||||||
|
lastLogon: String
|
||||||
|
lastLogonTimestamp: String
|
||||||
|
lockoutTime: String
|
||||||
|
logonCount: String
|
||||||
|
mail: String
|
||||||
|
name: String
|
||||||
|
objectCategory: String
|
||||||
|
objectGUID: String
|
||||||
|
objectSid: String
|
||||||
|
primaryGroupID: String
|
||||||
|
pwdLastSet: String
|
||||||
|
sAMAccountName: String!
|
||||||
|
sAMAccountType: String
|
||||||
|
sn: String
|
||||||
|
thumbnailPhoto: String
|
||||||
|
title: String
|
||||||
|
userAccountControl: String
|
||||||
|
userPrincipalName: String
|
||||||
|
whenChanged: String
|
||||||
|
whenCreated: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input UseResetTokenInput {
|
||||||
|
token: String!
|
||||||
|
newPassword: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserPresence {
|
||||||
|
user: User!
|
||||||
|
wifiDevices: [WifiDevice!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UserWhereInput {
|
||||||
|
cn: String
|
||||||
|
displayName: String
|
||||||
|
sAMAccountName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type WifiDevice {
|
||||||
|
user: User
|
||||||
|
id: String
|
||||||
|
oui: String
|
||||||
|
mac: String
|
||||||
|
hostname: String
|
||||||
|
firstSeen: String
|
||||||
|
lastSeen: String
|
||||||
|
essid: String
|
||||||
|
ip: String
|
||||||
|
uptime: String
|
||||||
|
apName: String
|
||||||
|
status: Status
|
||||||
|
}
|
21202
web/package-lock.json
generated
Executable file
47
web/package.json
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "ifms-pti",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint",
|
||||||
|
"get-schema": "graphql get-schema"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "^5.6.55",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"qrcode.vue": "^1.7.0",
|
||||||
|
"roboto-fontface": "*",
|
||||||
|
"validator": "^13.1.17",
|
||||||
|
"vue": "^2.6.12",
|
||||||
|
"vue-apollo": "^3.0.4",
|
||||||
|
"vue-json-pretty": "^1.7.0",
|
||||||
|
"vue-moment": "^4.1.0",
|
||||||
|
"vue-router": "^3.4.6",
|
||||||
|
"vue-the-mask": "^0.11.1",
|
||||||
|
"vuetify": "^2.3.13",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "^4.5.7",
|
||||||
|
"@vue/cli-plugin-eslint": "^4.5.7",
|
||||||
|
"@vue/cli-plugin-router": "^4.5.7",
|
||||||
|
"@vue/cli-service": "^4.5.7",
|
||||||
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^7.10.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"eslint-plugin-vue": "^7.0.1",
|
||||||
|
"graphql-cli": "^4.1.0",
|
||||||
|
"graphql-tag": "^2.11.0",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"sass": "^1.26.11",
|
||||||
|
"sass-loader": "^10.0.2",
|
||||||
|
"vue-cli-plugin-apollo": "^0.22.2",
|
||||||
|
"vue-cli-plugin-vuetify": "^2.0.7",
|
||||||
|
"vue-template-compiler": "^2.6.12",
|
||||||
|
"vuetify-loader": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
web/public/android-icon-144x144.png
Executable file
After Width: | Height: | Size: 6.3 KiB |
BIN
web/public/android-icon-192x192.png
Executable file
After Width: | Height: | Size: 8.1 KiB |
BIN
web/public/android-icon-36x36.png
Executable file
After Width: | Height: | Size: 1.8 KiB |
BIN
web/public/android-icon-48x48.png
Executable file
After Width: | Height: | Size: 2.1 KiB |
BIN
web/public/android-icon-72x72.png
Executable file
After Width: | Height: | Size: 3.0 KiB |
BIN
web/public/android-icon-96x96.png
Executable file
After Width: | Height: | Size: 3.9 KiB |
BIN
web/public/apple-icon-114x114.png
Executable file
After Width: | Height: | Size: 4.7 KiB |
BIN
web/public/apple-icon-120x120.png
Executable file
After Width: | Height: | Size: 4.8 KiB |
BIN
web/public/apple-icon-144x144.png
Executable file
After Width: | Height: | Size: 6.3 KiB |
BIN
web/public/apple-icon-152x152.png
Executable file
After Width: | Height: | Size: 6.7 KiB |
BIN
web/public/apple-icon-180x180.png
Executable file
After Width: | Height: | Size: 8.3 KiB |
BIN
web/public/apple-icon-57x57.png
Executable file
After Width: | Height: | Size: 2.4 KiB |
BIN
web/public/apple-icon-60x60.png
Executable file
After Width: | Height: | Size: 2.5 KiB |
BIN
web/public/apple-icon-72x72.png
Executable file
After Width: | Height: | Size: 3.0 KiB |
BIN
web/public/apple-icon-76x76.png
Executable file
After Width: | Height: | Size: 3.1 KiB |
BIN
web/public/apple-icon-precomposed.png
Executable file
After Width: | Height: | Size: 8.6 KiB |
BIN
web/public/apple-icon.png
Executable file
After Width: | Height: | Size: 8.6 KiB |
2
web/public/browserconfig.xml
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
BIN
web/public/favicon-16x16.png
Executable file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/favicon-32x32.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
web/public/favicon-96x96.png
Executable file
After Width: | Height: | Size: 3.9 KiB |
BIN
web/public/favicon.ico
Executable file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/favicon.png
Executable file
After Width: | Height: | Size: 21 KiB |
64
web/public/index.html
Executable file
|
@ -0,0 +1,64 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico">-->
|
||||||
|
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png" />
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="114x114"
|
||||||
|
href="/apple-icon-114x114.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="120x120"
|
||||||
|
href="/apple-icon-120x120.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="144x144"
|
||||||
|
href="/apple-icon-144x144.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="152x152"
|
||||||
|
href="/apple-icon-152x152.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/apple-icon-180x180.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="192x192"
|
||||||
|
href="/android-icon-192x192.png"
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||||
|
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<title>Portal de TI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong
|
||||||
|
>We're sorry but ifms-pti doesn't work properly without JavaScript
|
||||||
|
enabled. Please enable it to continue.</strong
|
||||||
|
>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
41
web/public/manifest.json
Executable file
|
@ -0,0 +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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
web/public/ms-icon-144x144.png
Executable file
After Width: | Height: | Size: 6.3 KiB |
BIN
web/public/ms-icon-150x150.png
Executable file
After Width: | Height: | Size: 6.6 KiB |
BIN
web/public/ms-icon-310x310.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
web/public/ms-icon-70x70.png
Executable file
After Width: | Height: | Size: 2.9 KiB |
95
web/src/App.vue
Executable file
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-overlay :value="$apollo.queries.me.loading" z-index="9">
|
||||||
|
<v-progress-circular color="primary lighten-4" indeterminate size="96" />
|
||||||
|
</v-overlay>
|
||||||
|
<component :is="layout" />
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar"
|
||||||
|
color="success"
|
||||||
|
multi-line
|
||||||
|
:timeout="10000"
|
||||||
|
top
|
||||||
|
>
|
||||||
|
{{ message }}
|
||||||
|
<v-btn text icon @click="snackbar = false">
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-snackbar>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Default from './layouts/Default'
|
||||||
|
import Simple from './layouts/Simple'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
Default,
|
||||||
|
Simple
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
snackbar: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
apollo: {
|
||||||
|
me: gql`
|
||||||
|
{
|
||||||
|
me {
|
||||||
|
sAMAccountName
|
||||||
|
displayName
|
||||||
|
department
|
||||||
|
roles
|
||||||
|
groups {
|
||||||
|
cn
|
||||||
|
}
|
||||||
|
thumbnailPhoto
|
||||||
|
firstName
|
||||||
|
pwdLastSet
|
||||||
|
lastLogin
|
||||||
|
lastLoginPrior
|
||||||
|
accountExpires
|
||||||
|
badPwdCount
|
||||||
|
description
|
||||||
|
extensionAttribute1
|
||||||
|
extensionAttribute10
|
||||||
|
extensionAttribute2
|
||||||
|
extensionAttribute6
|
||||||
|
extensionAttribute7
|
||||||
|
mail
|
||||||
|
title
|
||||||
|
isSuperAdmin
|
||||||
|
isStudent
|
||||||
|
isTokenCreator
|
||||||
|
isServant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
layout() {
|
||||||
|
return this.$route.meta.layout || 'Default'
|
||||||
|
},
|
||||||
|
message() {
|
||||||
|
return this.$route.params.message
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
message(message) {
|
||||||
|
this.snackbar = !!message
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
) {
|
||||||
|
this.$vuetify.theme.dark = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
BIN
web/src/assets/bg.jpeg
Executable file
After Width: | Height: | Size: 1.7 MiB |
BIN
web/src/assets/bg.jpg
Executable file
After Width: | Height: | Size: 59 KiB |
BIN
web/src/assets/ifms.png
Normal file
After Width: | Height: | Size: 24 KiB |
101
web/src/assets/logoTI.svg
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
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"
|
||||||
|
width="93.870148mm"
|
||||||
|
height="92.686058mm"
|
||||||
|
viewBox="0 0 93.870148 92.686059"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-48.497227,-75.955887)">
|
||||||
|
<circle
|
||||||
|
style="fill:#cf3034;fill-opacity:1;stroke-width:2.04116;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="path847"
|
||||||
|
cx="131.27765"
|
||||||
|
cy="110.05767"
|
||||||
|
r="11.089727" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect851"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="48.497227"
|
||||||
|
y="75.955887"
|
||||||
|
rx="1.9841913" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect897"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="72.563675"
|
||||||
|
y="75.955887"
|
||||||
|
rx="1.9841913" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect899"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="96.809151"
|
||||||
|
y="75.955887"
|
||||||
|
rx="1.9841913" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect901"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="72.563675"
|
||||||
|
y="100.06919"
|
||||||
|
rx="1.9841913" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect903"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="72.563675"
|
||||||
|
y="124.25967"
|
||||||
|
rx="1.9841913" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect905"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="72.563675"
|
||||||
|
y="148.49739"
|
||||||
|
rx="1.9841913" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect907"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="121.06866"
|
||||||
|
y="148.49739"
|
||||||
|
rx="1.9841913" />
|
||||||
|
<rect
|
||||||
|
style="fill:#3fa14c;fill-opacity:1;stroke-width:1.9842;stroke-linecap:round;paint-order:stroke fill markers"
|
||||||
|
id="rect909"
|
||||||
|
width="20.144552"
|
||||||
|
height="20.144552"
|
||||||
|
x="121.06866"
|
||||||
|
y="124.25967"
|
||||||
|
rx="1.9841913" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
BIN
web/src/assets/serti-dark.png
Executable file
After Width: | Height: | Size: 65 KiB |
BIN
web/src/assets/serti.png
Executable file
After Width: | Height: | Size: 78 KiB |
BIN
web/src/assets/ti.png
Executable file
After Width: | Height: | Size: 21 KiB |
124
web/src/components/AboutCard.vue
Executable file
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<v-card flat>
|
||||||
|
<v-card-title
|
||||||
|
class="font-weight-light"
|
||||||
|
:class="{ 'display-2': $vuetify.breakpoint.mdAndUp }"
|
||||||
|
>
|
||||||
|
Sobre esta aplicação
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Desenvolvida pelo SERTI de Ponta Porã. Feedback
|
||||||
|
<a href="mailto:serti.pp@ifms.edu.br">serti.pp@ifms.edu.br</a>.
|
||||||
|
</v-card-text>
|
||||||
|
<v-img class="my-5" src="../assets/logoTI.svg" height="256" contain />
|
||||||
|
<v-card-text>
|
||||||
|
<span class="headline">Principais tecnologias utilizadas:</span>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row>
|
||||||
|
<v-col v-for="group in appTechs" :key="group.subtitle">
|
||||||
|
<v-list two-line>
|
||||||
|
<v-subheader>
|
||||||
|
{{ group.subtitle }}
|
||||||
|
</v-subheader>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(tech, index) in group.techs"
|
||||||
|
:key="index"
|
||||||
|
two-line
|
||||||
|
>
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-icon>{{ tech.icon }}</v-icon>
|
||||||
|
</v-list-item-avatar>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ tech.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<a target="_blank" :href="tech.link">{{ tech.link }}</a>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn large color="primary" :to="{ name: 'home' }" exact>
|
||||||
|
<v-icon left>mdi-home</v-icon>Início
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AboutCard',
|
||||||
|
data: () => ({
|
||||||
|
appTechs: [
|
||||||
|
{
|
||||||
|
subtitle: 'Front-end',
|
||||||
|
techs: [
|
||||||
|
{
|
||||||
|
title: 'VueJS',
|
||||||
|
icon: 'mdi-vuejs',
|
||||||
|
link: 'https://vuejs.org/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'VuetifyJS',
|
||||||
|
icon: 'mdi-vuetify',
|
||||||
|
link: 'https://vuetifyjs.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'GraphQL',
|
||||||
|
icon: 'mdi-graphql',
|
||||||
|
link: 'https://graphql.org/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Apollo Client',
|
||||||
|
icon: 'mdi-alpha-a-circle-outline',
|
||||||
|
link: 'https://www.apollographql.com/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subtitle: 'Back-end',
|
||||||
|
techs: [
|
||||||
|
{
|
||||||
|
title: 'GraphQL',
|
||||||
|
icon: 'mdi-graphql',
|
||||||
|
link: 'https://graphql.org/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Apollo Server',
|
||||||
|
icon: 'mdi-alpha-a-circle-outline',
|
||||||
|
link: 'https://www.apollographql.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Prisma',
|
||||||
|
icon: 'mdi-database-search',
|
||||||
|
link: 'https://www.prisma.io/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PostgreSQL',
|
||||||
|
icon: 'mdi-database',
|
||||||
|
link: 'https://www.postgresql.org/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Active Directory',
|
||||||
|
icon: 'mdi-microsoft-windows',
|
||||||
|
link: 'https://www.microsoft.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LDAP JS',
|
||||||
|
icon: 'mdi-file-tree-outline',
|
||||||
|
link: 'http://ldapjs.org/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
49
web/src/components/Avatar.vue
Executable file
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<v-avatar :size="size" class="avatar" :class="{ left, right }">
|
||||||
|
<v-img
|
||||||
|
:src="
|
||||||
|
src
|
||||||
|
? src
|
||||||
|
: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKUUlEQVR4nMWbe2xb9RXHv+d3r68ftF2SJqVJSkuTrRGtEU2ctKBWgamMiraTioQpHR2EDd88JkCwIjEhoUiwobKNssHaxlYLJX1HUTceg7Gq0JZISYlDEmijtA1aEDSBNA9IGtux7/3tDztZ0vh573X3kSzL9u9877nH93F+53cuIc3IsmzinJcR0a0AigAs45znENEcAHMAMABjkdcQgItE1A3g/Pj4eHN9ff3VdPpH6RCtqalZGAwGtxDRegDlAG7QKBUkomZVVU8AOOLxeC4Y52UYwwLgdDqFjIyMzUT0GIB7EP5nDYWImgG8YbPZ6nfu3OkzRFOvQOQQf4iInkX4EL8efAvgz6Io7t61a9eYHiFdAXC5XHcS0S4Ay/Xo6KAfwG/dbvdhAFyLgKYAVFRUZEiS9FcAv9RibzSc849FUfz17t27v0zVNuUAVFVVrVJV9SiAm1O1TTPfc84f9Xg8x1MxSikALpfLRUR/A2BKybXryx/z8vKera2tVZMZnGwAqLKy8jnO+Qs6HIPJZILdbkdxcTFyc3Mxf/58SJKEoaEhXLlyBd3d3WhpacHIyIiezQDAAQC/crvdwUQDkwkAVVZWvsw5367VG8YY7r77bmzYsAFWqzXuWM45Ojo6cOzYMQwODmrdJDjn/xwZGbmvoaFhIt64hAGQZfk5AC9qdSQ3NxeyLCMvLy8lu4mJCRw/fhwnT57Uumlwzg/n5+dvi3c6CPEEIuf8Tq0OFBQU4KmnnsL8+fNTthUEAXa7HVarFV1dXZq2T0S3jo2NZXi93g9ibifWDy6Xq4yIGuONiceiRYuwffv2hId8IgoKCmC1WnHu3DmtErc7HI6LXq/382g/Rk1XKyoqMojoGDRe7c1mM2RZhiRJWsxnsW7dOhQXF+uRqKuuro6apUYNQCTJuVnr1u6//37ceOONWs2j8vDDD2PevHlazecoinLA6XTOOppnBcDlct0JHRleTk4O1q5dq9U8JjabDevXr9cjUZqZmSlf++WMAMiybIrk9prZuHEjGDN8IggAKC8vx9y5c/VIvFRVVbVg+hczPOWcPwQdExuTyQSHw6HVPCGSJGHVqlV6JH7EOX9m+hdTAXA6nQIR/U6P+vLlyw278MXCbrfrsuecV8uynD35eSoAGRkZmwEs0yO+YsUKPeZJsWzZMr1BvoFz/uTkh6kARCo5uliwYEHiQToRRRHZ2dmJB8aBiB6dvCMwIFzDQ7iMpQu9jiVLZmamXon8rKysu4BIAILB4BborOHl5eUhKytLr2NJYUSgIxf88E5HqreaMZvNePrppyEImrLmlNm0aRNsNptemXsAEHM6nRLCpWvNFBYW6r0/p8S8efNQWFioVya/srLyxywjI6MU2uv2AACitCwvxIVzTTXQa/kpi6zY6KKnpweKohjhUFIoioJLly7p1lFV1c5gQC1/bGwMZ1tadDuULE1NTRgb07UcAAAgoiIGnckPAAhMwP79+/HDDz/odioRV69exaGDByEYM98oYpzzHL0qxAg+nw91e/YY4VRcGhsb4fP7QcYEIIdFVml1Y7ZY0N7egaamJiPkohIIBHDq1CmYzWajJG0M4SVq3RARLFYL/vVBzPKbbs6ePQtVVQ296zAYuIoriiJ6e3vx5Zcpr1Alxb8//BAmk7FrMpPNCYYhSRKOHT1qpCSA8L//n95ew3MOwwPABAFfnDuH9vZ2wzQVRcHRI0fSUWvwM4TbUgzFbDZj3969GB8fN0SvoaEBAwMD6ZhrDDEAF41WFUURIyPfY8/u3bq1Ojs78c7bb8NssRjg2Uw45xdYpCHJcCxWC1q9Xhw6dEizRm9vL15/7TWYJCkthVYi6mYAzhuuHBaH1WrFu++8g8bGxpTte3p68OILL8DvDxh5358B57yLjY+PNwNIuIysBVEUYbZYMDI8nLLtRydPwu/3w2JJz84DgCAIp1l9ff3VSPdVWpAkCaVlZSnblTgcsNpsRqW80RheuHBhBwOASB9eWigoKMBtt92Wsl1xcTGWLl2aBo+m+Ki2tladDO+RdGzBYrFg69atmmyJCNu2bUvb+U9Eh4FIGuzxeC4YfRqYzWY8/vjjWLx4sWaNm266CU888QQsxt8CRwKBwLvAtLX/0tJSAcDPjVC/5ZZbUFVVhSVLlujWysrKwsqVK9Hf348rV64Y4B0A4I29e/f+A5gWgPLy8vPBYPAxaJwdMsZQVFSEBx54AJs3bza0SDp37lzccccdWLx4MUZHRzE0NKSnJqgIgrC1tbV1GLimR0iW5WcAvJys0uROOxwOrFy58rpVhkdHR9He3g6v14vu7m6oalIdcQAAzvlbHo/nkcnPMwJQU1MzJxQKXQSwMJYAEaGwsBBlZWVwOBzXtRwejdHRUXi9Xnz66afo6elJdGQEiWhFXV3dVPo/a24py/IvAByMpVBSUgJZlv8vpfB4cM7hdrvR1tYWb9gf3G73c9O/mDW98nq9X5SUlNxJRDdHU+jr68Ply5dRXFyctkaIVFEUBR6PJ9HOf+Xz+R7s7OyckfVGnV+uXr36DOe8AsCs+08oFMKlixfR2dGBFcuXY84cQypqmvm2vx87duxA+2efxcsZVMbYffv27Zs1840agNbW1uHS0tILALZc+xtjDIwxDHz3HU6dPg2bzYalS5de91NCURScOHECf3n1VQwMDMBitcasFxDR83V1dW9F+y1mhcHr9XY5HI4bAKyZZSQIEEURAb8fn7W1oeXsWeRkZyM3N1fr/iQN5xxtbW3Y+corOHPmDFRVhdVmgyiKsUzeGx4e/s358+ejXh3j/m21tbXs8uXL+wFsi+VMIBBAcCLcjpu/aBHuvfderFmzxvAUNhAIoOmTT/D+++/jm2++AQCYJAlmszne0dfi8/nWxXvwKpleYRPn/O9EtCHWGFVVEfD7EQqFAITT4JKSEqy+/XbY7XbNS9k+nw9ffP45mpub0dbWhkAgAOB/0+wEF+EuAOVutztu+pjUiet0OqXMzMz9AB6MN05VVUxMTCAUDE7dj4kIS5YsQVFREfLz87EwNxc52dmw2mxTgfH5fPD5fBgYGEBfXx++/vprXOjuRm9v7wwd0WSClFx1qAXApkQ7D6TwwERtbS3r6+t7ZXqDUSw45wiFQggFgwgpCqA1bSWCKAgQTSaIopjshfY9n8+3JdnnDVO+dLtcroeIqA4p9BQoigJVVafeuaqCc4DzcApLjIEi74wxCIIw9Z4CKoDn8/LyXkr2aRFA40NT1dXVRYqiHABQqsU+DXzFGHtkz549H6dqqKnQ3traOlhQULDParX2A1iLKAnTdSIIYAeALdPz+1TQnb1UVVUt4Jw/wzmvAaC7cylJFAAHiOj3Wnd8EsPSN1mWsznnTxLRowDyjdK9hhEAhwVB+JOWZwSjYXj+6nQ6hUgT4jbO+c+gPxjDAD4iosOBQODdN99806/byWmkO4Enl8v1E8bYXaqq2omoCOGepBzMPl38AIY45xeIqJtz3iUIwunBwcHOhoaGtHVg/Rdv+XFYpvXd7gAAAABJRU5ErkJggg=='
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Avatar',
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: '40px'
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.avatar .v-image {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
75
web/src/components/CreateTokenForm.vue
Executable file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<v-card :loading="loading" flat>
|
||||||
|
<v-card-title class="font-weight-light display-1 mb-2">
|
||||||
|
Criar token
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<UserSelect v-model="user" students />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn to="/" text>Cancelar</v-btn>
|
||||||
|
<v-btn :disabled="!user" color="primary" large @click="onCreateToken">
|
||||||
|
Criar token
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import UserSelect from './UserSelect'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GenerateTokenForm',
|
||||||
|
components: { UserSelect },
|
||||||
|
data: () => ({
|
||||||
|
user: null,
|
||||||
|
loading: false
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
async onCreateToken() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.$apollo.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation($username: String!) {
|
||||||
|
createResetToken(data: { username: $username }) {
|
||||||
|
id
|
||||||
|
token
|
||||||
|
createdAt
|
||||||
|
expiration
|
||||||
|
user {
|
||||||
|
displayName
|
||||||
|
sAMAccountName
|
||||||
|
}
|
||||||
|
creator {
|
||||||
|
displayName
|
||||||
|
sAMAccountName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
username: this.user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenInfo = result.data.createResetToken
|
||||||
|
|
||||||
|
await this.$router.push({
|
||||||
|
name: 'print-token',
|
||||||
|
params: {
|
||||||
|
...tokenInfo
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
210
web/src/components/LoginForm.vue
Executable file
|
@ -0,0 +1,210 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-card :loading="loading" :elevation="24">
|
||||||
|
<v-scroll-x-reverse-transition mode="out-in">
|
||||||
|
<div v-if="step === 1" key="1">
|
||||||
|
<v-card-title class="justify-center pt-12">
|
||||||
|
<LogoSerti height="96px" />
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-title class="font-weight-light justify-center mb-6">
|
||||||
|
Para continuar, digite seu usuário
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="username"
|
||||||
|
:class="{ 'mx-6': $vuetify.breakpoint.mdAndUp }"
|
||||||
|
outlined
|
||||||
|
autofocus
|
||||||
|
rounded
|
||||||
|
name="username"
|
||||||
|
label="Usuário"
|
||||||
|
hint="Servidores: SIAPE. Alunos e colaboradores: CPF."
|
||||||
|
:error-messages="errors"
|
||||||
|
:error="!!errors.length"
|
||||||
|
@keyup.enter="goToPassword"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions class="mb-12">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
:disabled="!username"
|
||||||
|
:loading="loading"
|
||||||
|
color="primary"
|
||||||
|
depressed
|
||||||
|
x-large
|
||||||
|
@click="goToPassword"
|
||||||
|
>
|
||||||
|
Próximo
|
||||||
|
<v-icon right>mdi-chevron-right</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
</v-card-actions>
|
||||||
|
<v-divider />
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text color="primary" to="use-token" large>
|
||||||
|
<v-icon left>mdi-key-variant</v-icon>
|
||||||
|
Tenho um token
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="step === 2" key="2">
|
||||||
|
<v-card-title
|
||||||
|
class="font-weight-light justify-center display-1 pa-12"
|
||||||
|
>
|
||||||
|
<Avatar class="mr-4" :src="basicUser.thumbnailPhoto" size="75px" />
|
||||||
|
|
||||||
|
<div>Olá, {{ basicUser.firstName }}!</div>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
:class="{ 'mx-10': $vuetify.breakpoint.mdAndUp }"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
outlined
|
||||||
|
autofocus
|
||||||
|
rounded
|
||||||
|
name="password"
|
||||||
|
label="Senha"
|
||||||
|
:append-icon="
|
||||||
|
showPassword ? 'mdi-eye-outline' : 'mdi-eye-off-outline'
|
||||||
|
"
|
||||||
|
:hint="
|
||||||
|
basicUser.isServant
|
||||||
|
? 'Utilize a senha do SUAP'
|
||||||
|
: 'Caso ainda não tenha uma senha, dirija-se à CEREL e solicite um token.'
|
||||||
|
"
|
||||||
|
:error-messages="errors"
|
||||||
|
:error="!!errors.length"
|
||||||
|
@click:append="showPassword = !showPassword"
|
||||||
|
@keyup.enter="doLogin"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions class="mb-12">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
:disabled="!password"
|
||||||
|
:loading="loading"
|
||||||
|
color="primary"
|
||||||
|
class="px-6"
|
||||||
|
x-large
|
||||||
|
depressed
|
||||||
|
@click="doLogin"
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
</v-card-actions>
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text large color="secondary" @click="reset">Voltar</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</div>
|
||||||
|
</v-scroll-x-reverse-transition>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- <v-btn class="float-right mt-4" text color="secondary">Ajuda</v-btn>-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onLogin, onLogout } from '../plugins/vue-apollo'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import Avatar from './Avatar'
|
||||||
|
import LogoSerti from './LogoSerti'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoginForm',
|
||||||
|
components: { LogoSerti, Avatar },
|
||||||
|
data: () => ({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
showPassword: false,
|
||||||
|
basicUser: undefined,
|
||||||
|
errors: [],
|
||||||
|
step: 1,
|
||||||
|
loading: false
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
reset() {
|
||||||
|
this.username = ''
|
||||||
|
this.password = ''
|
||||||
|
this.showPassword = false
|
||||||
|
this.basicUser = undefined
|
||||||
|
this.errors = []
|
||||||
|
this.step = 1
|
||||||
|
this.loading = false
|
||||||
|
},
|
||||||
|
async goToPassword() {
|
||||||
|
this.loading = true
|
||||||
|
this.errors = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.$apollo.query({
|
||||||
|
query: gql`
|
||||||
|
query($username: String!) {
|
||||||
|
basicUser(sAMAccountName: $username) {
|
||||||
|
displayName
|
||||||
|
firstName
|
||||||
|
thumbnailPhoto
|
||||||
|
isServant
|
||||||
|
isStudent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
username: this.username
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.basicUser = response.data.basicUser
|
||||||
|
this.step = 2
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = e.graphQLErrors.map(e => e.message)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async doLogin() {
|
||||||
|
this.loading = true
|
||||||
|
this.showPassword = false
|
||||||
|
|
||||||
|
this.errors = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.$apollo.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation($username: String!, $password: String!) {
|
||||||
|
login(data: { username: $username, password: $password }) {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const token = response.data.login.token
|
||||||
|
|
||||||
|
await onLogin(this.$apollo.getClient(), token)
|
||||||
|
|
||||||
|
const next = this.$route.params.next
|
||||||
|
await this.$router.push(next ? next : '/')
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = e.graphQLErrors.map(e => e.message)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async doLogout() {
|
||||||
|
await onLogout(this.$apollo.getClient())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
25
web/src/components/LogoSerti.vue
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
v-if="this.$vuetify.theme.dark || dark"
|
||||||
|
src="@/assets/serti-dark.png"
|
||||||
|
:height="height"
|
||||||
|
/>
|
||||||
|
<img v-else src="@/assets/serti.png" :height="height" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'LogoSerti',
|
||||||
|
props: {
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '48px'
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
type: Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
69
web/src/components/MainDrawer.vue
Executable file
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-if="me"
|
||||||
|
v-model="drawer"
|
||||||
|
:clipped="$vuetify.breakpoint.mdAndDown"
|
||||||
|
:mini-variant="mini"
|
||||||
|
:mini-variant-width="64"
|
||||||
|
:fixed="$vuetify.breakpoint.mdAndDown"
|
||||||
|
:app="$vuetify.breakpoint.mdAndDown"
|
||||||
|
right
|
||||||
|
@input="$emit('input', drawer)"
|
||||||
|
>
|
||||||
|
<template slot="prepend">
|
||||||
|
<v-img class="ma-4" src="../assets/logoTI.svg" height="48px" contain />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<NavList :mini="mini" />
|
||||||
|
|
||||||
|
<template slot="append">
|
||||||
|
<v-list nav>
|
||||||
|
<v-list-item @click="mini = !mini">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon v-if="mini">mdi-chevron-left</v-icon>
|
||||||
|
<v-icon v-else>mdi-chevron-right</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NavList from './NavList'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MainDrawer',
|
||||||
|
components: { NavList },
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
drawer: true,
|
||||||
|
mini: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value(e) {
|
||||||
|
this.drawer = e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
apollo: {
|
||||||
|
me: {
|
||||||
|
query: gql`
|
||||||
|
{
|
||||||
|
me {
|
||||||
|
sAMAccountName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
44
web/src/components/NavList.vue
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="me && me.roles">
|
||||||
|
<v-list
|
||||||
|
v-for="(group, groupIndex) in filteredNavItems"
|
||||||
|
:key="groupIndex"
|
||||||
|
nav
|
||||||
|
rounded
|
||||||
|
>
|
||||||
|
<v-subheader v-if="group.groupTitle && !mini">
|
||||||
|
{{ group.groupTitle }}
|
||||||
|
</v-subheader>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-for="(item, itemIndex) in group.items"
|
||||||
|
:key="itemIndex"
|
||||||
|
:to="item.route"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
color="primary"
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-icon>{{ item.icon }}</v-icon>
|
||||||
|
</v-list-item-avatar>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ item.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Nav from '@/mixins/Nav'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NavList',
|
||||||
|
mixins: [Nav],
|
||||||
|
props: {
|
||||||
|
mini: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
106
web/src/components/NewPasswordFields.vue
Executable file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="newPassword"
|
||||||
|
class="mt-6"
|
||||||
|
:rules="[
|
||||||
|
() =>
|
||||||
|
!newPassword.length ||
|
||||||
|
passwordStrength.score >= minStrength ||
|
||||||
|
'Esta senha é muito simples!'
|
||||||
|
]"
|
||||||
|
:type="show ? 'text' : 'password'"
|
||||||
|
outlined
|
||||||
|
rounded
|
||||||
|
name="password"
|
||||||
|
label="Nova senha"
|
||||||
|
:hint="strengthTips ? passwordStrength.message : ''"
|
||||||
|
:append-icon="show ? 'mdi-eye-outline' : 'mdi-eye-off-outline'"
|
||||||
|
validate-on-blur
|
||||||
|
@click:append="show = !show"
|
||||||
|
@keyup="onInput"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-expand-transition>
|
||||||
|
<PasswordStrengthMeter
|
||||||
|
v-if="newPassword && strengthTips"
|
||||||
|
:password-strength="passwordStrength.score"
|
||||||
|
/>
|
||||||
|
</v-expand-transition>
|
||||||
|
|
||||||
|
<v-expand-transition>
|
||||||
|
<v-text-field
|
||||||
|
v-show="!show"
|
||||||
|
v-model="passwordConfirmation"
|
||||||
|
:rules="[
|
||||||
|
() =>
|
||||||
|
newPassword === passwordConfirmation || 'As senhas não coincidem.'
|
||||||
|
]"
|
||||||
|
class="mt-6"
|
||||||
|
type="password"
|
||||||
|
outlined
|
||||||
|
rounded
|
||||||
|
name="passwordConfirmation"
|
||||||
|
label="Confirme a nova senha"
|
||||||
|
validate-on-blur
|
||||||
|
@keyup="onInput"
|
||||||
|
/>
|
||||||
|
</v-expand-transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import PasswordStrengthMeter from './PasswordStrengthMeter'
|
||||||
|
import { passwordStrengthEvaluator } from '../utils/passwordStrengthEvaluator'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NewPasswordFields',
|
||||||
|
components: {
|
||||||
|
PasswordStrengthMeter
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
suggestion: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
newPassword: '',
|
||||||
|
passwordConfirmation: '',
|
||||||
|
show: false,
|
||||||
|
minStrength: 2,
|
||||||
|
strengthTips: true
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
passwordStrength() {
|
||||||
|
return passwordStrengthEvaluator(this.newPassword)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
this.onInput()
|
||||||
|
},
|
||||||
|
suggestion(v) {
|
||||||
|
this.strengthTips = false
|
||||||
|
this.newPassword = v
|
||||||
|
this.show = true
|
||||||
|
this.$emit('input', this.newPassword)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.newPassword = this.suggestion
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onInput() {
|
||||||
|
this.strengthTips = true
|
||||||
|
|
||||||
|
if (
|
||||||
|
(this.show || this.newPassword === this.passwordConfirmation) &&
|
||||||
|
this.passwordStrength.score >= this.minStrength
|
||||||
|
)
|
||||||
|
this.$emit('input', this.newPassword)
|
||||||
|
else this.$emit('input', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
30
web/src/components/PasswordStrengthMeter.vue
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="px-1">
|
||||||
|
<v-progress-linear
|
||||||
|
class="font-weight-light pa-0 ma-0"
|
||||||
|
style="font-size: 0.7rem; top: -2.6em"
|
||||||
|
:value="(passwordStrength + 1) / 0.05"
|
||||||
|
:color="strengthColor"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'PasswordStrengthMeter',
|
||||||
|
props: {
|
||||||
|
passwordStrength: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
strengthColor() {
|
||||||
|
return ['red', 'orange', 'yellow darken-1', 'teal', 'green'][
|
||||||
|
this.passwordStrength
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
99
web/src/components/ReplacePasswordForm.vue
Executable file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<v-card :loading="loading" flat>
|
||||||
|
<v-card-title class="font-weight-light display-1 mb-2">
|
||||||
|
Alterar uma senha
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<UserSelect v-model="username" />
|
||||||
|
|
||||||
|
<NewPasswordFields v-model="newPassword" :suggestion="suggestion" />
|
||||||
|
|
||||||
|
<v-btn text color="info" @click="generatePassword"
|
||||||
|
><v-icon left>mdi-refresh</v-icon>Gerar</v-btn
|
||||||
|
>
|
||||||
|
<v-btn :disabled="!username" text color="info" @click="defaultPassword"
|
||||||
|
><v-icon left>mdi-account-outline</v-icon> Padrão</v-btn
|
||||||
|
>
|
||||||
|
|
||||||
|
<v-alert color="error" :value="!!errors.length" icon="mdi-alert-circle">
|
||||||
|
<p v-for="(error, index) in errors" :key="index">{{ error }}</p>
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
<v-divider />
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text large :to="{ name: 'home' }" exact>Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
:disabled="!username || !newPassword"
|
||||||
|
color="primary"
|
||||||
|
large
|
||||||
|
@click="onReplacePassword"
|
||||||
|
>
|
||||||
|
Trocar a senha
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import UserSelect from './UserSelect'
|
||||||
|
import NewPasswordFields from './NewPasswordFields'
|
||||||
|
export default {
|
||||||
|
name: 'ReplacePasswordForm',
|
||||||
|
components: {
|
||||||
|
NewPasswordFields,
|
||||||
|
UserSelect
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
username: null,
|
||||||
|
newPassword: '',
|
||||||
|
suggestion: '',
|
||||||
|
loading: false,
|
||||||
|
errors: []
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
generatePassword() {
|
||||||
|
this.suggestion = Math.random().toString(36).slice(-10)
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultPassword() {
|
||||||
|
this.suggestion = `ifms.${this.username}`
|
||||||
|
},
|
||||||
|
|
||||||
|
async onReplacePassword() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation($username: String!, $newPassword: String!) {
|
||||||
|
replacePassword(
|
||||||
|
data: { username: $username, newPassword: $newPassword }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
username: this.username,
|
||||||
|
newPassword: this.newPassword
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.$router.push({
|
||||||
|
name: 'home',
|
||||||
|
params: {
|
||||||
|
message: `A senha do usuário ${this.username} foi alterada!`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = e.graphQLErrors.map(e => e.message)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|