Compare commits

..

No commits in common. "84286a69193fec91bdf5df5e5cfcd7ec4652ef66" and "0df0beea96a326041103ac9d7ea3534441438361" have entirely different histories.

19 changed files with 31 additions and 396 deletions

34
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "ifms-smart-cp", "name": "ifms-smart-cp",
"version": "1.0.0", "version": "0.0.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ifms-smart-cp", "name": "ifms-smart-cp",
"version": "1.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@mdi/font": "7.0.96", "@mdi/font": "7.0.96",
"@prisma/client": "^5.4.2", "@prisma/client": "^5.4.2",
@ -18,7 +18,6 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"ip": "^1.1.8",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"ldapts": "^7.0.5", "ldapts": "^7.0.5",
"roboto-fontface": "*", "roboto-fontface": "*",
@ -30,7 +29,6 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.21.4", "@babel/types": "^7.21.4",
"@types/ip": "^1.1.2",
"@types/jsonwebtoken": "^9.0.4", "@types/jsonwebtoken": "^9.0.4",
"@types/node": "^18.15.0", "@types/node": "^18.15.0",
"@types/webfontloader": "^1.6.35", "@types/webfontloader": "^1.6.35",
@ -657,15 +655,6 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==" "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
}, },
"node_modules/@types/ip": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.2.tgz",
"integrity": "sha512-WjV3/mz9YSlfU8E88m4ZwvRQmpTjWy4vH8K+cggBUAjmp91wvT1he132Ql8V3CsZz3SYkze3CC3AxhjaD4ZYug==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/jsonwebtoken": { "node_modules/@types/jsonwebtoken": {
"version": "9.0.4", "version": "9.0.4",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz",
@ -1745,11 +1734,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"node_modules/ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -3357,15 +3341,6 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==" "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
}, },
"@types/ip": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.2.tgz",
"integrity": "sha512-WjV3/mz9YSlfU8E88m4ZwvRQmpTjWy4vH8K+cggBUAjmp91wvT1he132Ql8V3CsZz3SYkze3CC3AxhjaD4ZYug==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/jsonwebtoken": { "@types/jsonwebtoken": {
"version": "9.0.4", "version": "9.0.4",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz",
@ -4221,11 +4196,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"ipaddr.js": { "ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "ifms-smart-cp", "name": "ifms-smart-cp",
"version": "1.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"clean": "rimraf ./dist", "clean": "rimraf ./dist",
"dev:web": "vite --host", "dev:web": "vite --host",
@ -24,7 +24,6 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"ip": "^1.1.8",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"ldapts": "^7.0.5", "ldapts": "^7.0.5",
"roboto-fontface": "*", "roboto-fontface": "*",
@ -36,7 +35,6 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.21.4", "@babel/types": "^7.21.4",
"@types/ip": "^1.1.2",
"@types/jsonwebtoken": "^9.0.4", "@types/jsonwebtoken": "^9.0.4",
"@types/node": "^18.15.0", "@types/node": "^18.15.0",
"@types/webfontloader": "^1.6.35", "@types/webfontloader": "^1.6.35",

View File

@ -19,8 +19,6 @@ export class LdapAuth implements Auth {
try { try {
await this.client.bind(`${this.domain}\\${username}`, password) await this.client.bind(`${this.domain}\\${username}`, password)
// const isAdmin = await this.isAdmin(username)
const search = await this.client.search(this.searchDN, { const search = await this.client.search(this.searchDN, {
scope: 'sub', scope: 'sub',
filter: `(sAMAccountName=${username})`, filter: `(sAMAccountName=${username})`,
@ -31,8 +29,7 @@ export class LdapAuth implements Auth {
username, username,
displayName: search.searchEntries[0].displayName as string, displayName: search.searchEntries[0].displayName as string,
domain: this.domain, domain: this.domain,
pwdLastSet: search.searchEntries[0].pwdLastSet as string, pwdLastSet: search.searchEntries[0].pwdLastSet as string
isAdmin: false
} }
} catch (error: any) { } catch (error: any) {
console.log('Error:', error) console.log('Error:', error)
@ -42,23 +39,4 @@ export class LdapAuth implements Auth {
await this.client.unbind() await this.client.unbind()
} }
} }
async isAdmin(username: string): Promise<boolean> {
const adminsGroupDN = (
await this.client.search(this.searchDN, {
filter: `(&(objectCategory=group)(cn=PP-PTI-Admins))`
})
).searchEntries[0]?.distinguishedName as string
const adminGroupMembers = await this.client.search(this.searchDN, {
filter: `(memberOf:1.2.840.113556.1.4.1941:=${adminsGroupDN})`,
attributes: ['sAMAccountName']
})
const isAdmin = adminGroupMembers.searchEntries.some(
entry => entry.sAMAccountName === username
)
return isAdmin
}
} }

View File

@ -2,10 +2,6 @@ import { server } from './server'
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
const [major] = process.versions.node.split('.').map(Number)
if (major < 21) throw new Error('Node version must be >= 21.0.0')
export const SERVER_PORT = process.env.PORT || 8080 export const SERVER_PORT = process.env.PORT || 8080
server.listen(SERVER_PORT, () => { server.listen(SERVER_PORT, () => {

View File

@ -1,6 +1,5 @@
import { PaFirewallService } from '../services/PaFirewallService' import { PaFirewallService } from '../services/PaFirewallService'
import { db } from '../prisma' import { db } from '../prisma'
import { LoginResult } from '../schemas/LoginResult'
export async function autoLogin({ export async function autoLogin({
username, username,
@ -10,20 +9,12 @@ export async function autoLogin({
username: string username: string
domain: string domain: string
ip: string ip: string
}): Promise<LoginResult> { }) {
console.log(`AutoLogin attempt: ${domain}\\${username} from ${ip}`) console.log('AutoLogin?', username, domain, ip)
const paHosts = await db.paHost.findMany() const paHosts = await db.paHost.findMany()
const pa = new PaFirewallService(paHosts[0].ip, paHosts[0].key) const pa = new PaFirewallService(paHosts[0].ip, paHosts[0].key)
await pa.login({ username, ip, domain }) return await pa.login({ username, ip, domain })
return {
username,
displayName: username,
domain,
pwdLastSet: '',
isAdmin: false
}
} }

View File

@ -26,7 +26,7 @@ export async function login(
await pa.login({ username, ip, domain: user.domain }) await pa.login({ username, ip, domain: user.domain })
const jwt = jwtService.generateToken({ const jwt = await jwtService.generateToken({
displayName: user.displayName, displayName: user.displayName,
username: user.username, username: user.username,
domain: user.domain, domain: user.domain,

View File

@ -1,103 +0,0 @@
import { z } from 'zod'
import ip from 'ip'
import { db } from '../prisma'
type Network = {
name: string
shortName: string
network: string
}
const networksInfo: Array<Network> = [
{
name: 'Reitoria',
shortName: 'RT',
network: '10.0.0.0/16'
},
{
name: 'Aquidauana',
shortName: 'AQ',
network: '10.2.0.0/16'
},
{
name: 'Campo Grande',
shortName: 'CG',
network: '10.3.0.0/16'
},
{
name: 'Corumbá',
shortName: 'CB',
network: '10.4.0.0/16'
},
{
name: 'Coxim',
shortName: 'CX',
network: '10.5.0.0/16'
},
{
name: 'Nova Andradina',
shortName: 'NA',
network: '10.6.0.0/16'
},
{
name: 'Ponta Porã',
shortName: 'PP',
network: '10.7.0.0/16'
},
{
name: 'Três Lagoas',
shortName: 'TL',
network: '10.8.0.0/16'
},
{
name: 'Jardim',
shortName: 'JD',
network: '10.9.0.0/16'
},
{
name: 'Naviraí',
shortName: 'NV',
network: '10.10.0.0/16'
},
{
name: 'Dourados',
shortName: 'DR',
network: '10.11.0.0/16'
},
{
name: 'Reitoria',
shortName: 'RT',
network: '10.1.0.0/16'
}
]
const networks = networksInfo.map(network => {
return {
...network,
...ip.cidrSubnet(network.network)
}
})
export async function getNetworkForIP(ip: string) {
z.string().ip().parse(ip)
const network = networks.find(network => network.contains(ip))
if (!network) {
return {
name: 'Rede desconhecida',
shortName: '--',
network: '?',
isSupported: false
}
}
const paHosts = await db.paHost.findMany()
const paHost = paHosts.find(paHost => network?.contains(paHost.ip))
return {
...network,
isSupported: !!paHost
}
}

View File

@ -2,6 +2,5 @@ export type AuthResult = {
username: string username: string
displayName: string displayName: string
domain: string domain: string
pwdLastSet?: string pwdLastSet: string
isAdmin?: boolean
} }

View File

@ -2,8 +2,7 @@ export type JwtPayload = {
username: string username: string
displayName: string displayName: string
domain: string domain: string
pwdLastSet?: string pwdLastSet: string
isAdmin?: boolean
iat?: Date // issued at iat?: Date // issued at
exp?: Date // expires at exp?: Date // expires at
} }

View File

@ -1,6 +1,6 @@
import { sign, verify } from 'jsonwebtoken' import { sign, verify } from 'jsonwebtoken'
import { JwtPayload } from '../schemas/JwtPayload' import { JwtPayload } from '../schemas/JwtPayload'
import { JWT_SECRET, JWT_TTL_IN_MINUTES } from '../env' import { JWT_SECRET, JWT_TTL_IN_MINUTES } from '../../common/env'
export class JwtService { export class JwtService {
constructor( constructor(

View File

@ -1,7 +1,7 @@
import { XMLParser } from 'fast-xml-parser' import { XMLParser } from 'fast-xml-parser'
import { db } from '../prisma' import { db } from '../prisma'
import { MAP_TIMEOUT_IN_MINUTES } from '../env' import { MAP_TIMEOUT_IN_MINUTES } from '../../common/env'
const xmlParser = new XMLParser({ const xmlParser = new XMLParser({
ignoreAttributes: false, ignoreAttributes: false,
@ -42,7 +42,7 @@ export class PaFirewallService {
throw new Error('Failed to map user ID to IP') throw new Error('Failed to map user ID to IP')
} }
console.log(`Mapped user ${domain}\\${username} to IP ${ip}`) console.log(`Mapped user ${username} to IP ${ip}`)
return true return true
} }

View File

@ -8,21 +8,13 @@ import { logout } from './lib/logout'
import { PaFirewallService } from './services/PaFirewallService' import { PaFirewallService } from './services/PaFirewallService'
import { jwtService } from './lib/jwt' import { jwtService } from './lib/jwt'
import { autoLogin } from './lib/autoLogin' import { autoLogin } from './lib/autoLogin'
import { JwtPayload } from './schemas/JwtPayload'
import { getNetworkForIP } from './lib/netInfo'
// Created for each request // Created for each request
function createContext({ req, res }: trpcExpress.CreateExpressContextOptions) { function createContext({ req, res }: trpcExpress.CreateExpressContextOptions) {
const ip = getIpFromRequest(req) const ip = getIpFromRequest(req)
const token = req.headers.authorization?.split(' ')[1] const token = req.headers.authorization?.split(' ')[1]
let jwtPayload: JwtPayload | null = null const jwtPayload = token ? jwtService.verifyToken(token) : null
try {
jwtPayload = token ? jwtService.verifyToken(token) : null
} catch (error) {
console.log(error)
}
return { ip, user: jwtPayload } return { ip, user: jwtPayload }
} }
@ -34,16 +26,9 @@ export const t = initTRPC.context<Context>().create()
const { query, mutation, input } = t.procedure const { query, mutation, input } = t.procedure
export const appRouter = t.router({ export const appRouter = t.router({
myIp: query(async ({ ctx }) => { myIp: query(({ ctx }) => {
if (!ctx.ip) throw new Error('Erro ao obter endereço IP') if (!ctx.ip) throw new Error('Erro ao obter endereço IP')
const network = await getNetworkForIP(ctx.ip) return ctx.ip
return {
name: network?.name,
shortName: network?.shortName,
ip: ctx.ip,
isSupported: network?.isSupported
}
}), }),
login: input( login: input(
@ -64,7 +49,7 @@ export const appRouter = t.router({
const { username, domain } = ctx.user const { username, domain } = ctx.user
return await autoLogin({ username, domain: domain, ip: ctx.ip }) return await autoLogin({ username, domain: ctx.ip, ip: domain })
}), }),
addFirewall: t.procedure addFirewall: t.procedure

View File

@ -3,42 +3,30 @@
<span v-if="!loadingIpAddress"> <span v-if="!loadingIpAddress">
Endereço IP deste dispositivo: Endereço IP deste dispositivo:
<strong> <strong>
<code> {{ netInfo.ip }} ({{ netInfo.name }}) </code> <code>
{{ ipAddress }}
</code>
</strong> </strong>
</span> </span>
<span v-else> <span v-else>
<v-progress-circular indeterminate class="mr-2" /> <v-progress-circular indeterminate class="mr-2" />
Obtendo seu endereço IP... Obtendo seu endereço IP...
</span> </span>
<v-alert
type="error"
v-if="!netInfo.isSupported && !loadingIpAddress"
prominent
class="mt-2"
>
A rede onde seu dispositivo se encontra não é suportada. Entre em contato
com o SERTI do seu câmpus
</v-alert>
</v-alert> </v-alert>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { trpc } from '../trpc' import { trpc } from '../trpc'
const netInfo = ref<{ const ipAddress = ref('')
ip: string
name?: string | undefined
shortName?: string | undefined
isSupported?: boolean
}>({ ip: '', name: '', shortName: '' })
const loadingIpAddress = ref(false) const loadingIpAddress = ref(false)
onMounted(async () => { onMounted(async () => {
try { try {
loadingIpAddress.value = true loadingIpAddress.value = true
netInfo.value = await trpc.myIp.query() ipAddress.value = await trpc.myIp.query()
} catch (error: any) { } catch (error: any) {
netInfo.value = error.message ipAddress.value = error.message
} finally { } finally {
loadingIpAddress.value = false loadingIpAddress.value = false
} }

View File

@ -43,7 +43,7 @@ import { PropType } from 'vue'
import { trpc } from '../trpc' import { trpc } from '../trpc'
import { LoginResult } from '@/server/schemas/LoginResult' import { LoginResult } from '@/server/schemas/Auth'
const props = defineProps({ const props = defineProps({
loginResult: { loginResult: {
@ -58,10 +58,10 @@ async function onLogout() {
'Deseja realmente sair? Você será desconectado e não poderá navegar na Internet até fazer login novamente.' 'Deseja realmente sair? Você será desconectado e não poderá navegar na Internet até fazer login novamente.'
) )
) { ) {
const success = await trpc.logout.mutate() const success = await trpc.logout.mutate({
username: props.loginResult.username,
localStorage.removeItem('token') domain: props.loginResult.domain
localStorage.removeItem('autoLogin') })
if (success) { if (success) {
alert('Você foi desconectado.') alert('Você foi desconectado.')

View File

@ -34,7 +34,7 @@
<v-switch <v-switch
v-model="autoLogin" v-model="autoLogin"
color="primary" color="primary"
label="Logar automaticamente neste dispositivo?" label="Logar automaticamente neste dispositivo"
hide-details hide-details
/> />
</v-card-text> </v-card-text>
@ -61,43 +61,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed } from 'vue'
import { trpc } from '../trpc' import { trpc } from '../trpc'
import { VForm } from 'vuetify/lib/components/index.mjs' import { VForm } from 'vuetify/lib/components/index.mjs'
import LoginErrorAlert from './LoginErrorAlert.vue' import LoginErrorAlert from './LoginErrorAlert.vue'
import { useToken } from '../composables/useToken'
const emit = defineEmits(['login']) const emit = defineEmits(['login'])
const { token } = useToken()
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
const autoLogin = ref(false) const autoLogin = ref(false)
watch(autoLogin, value => {
localStorage.setItem('autoLogin', value ? 'true' : 'false')
})
onMounted(async () => {
autoLogin.value = localStorage.getItem('autoLogin') === 'true'
if (autoLogin.value && token.value) {
try {
loading.value = true
const result = await trpc.autoLogin.mutate()
emit('login', result)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
})
const showPassword = ref(false) const showPassword = ref(false)
const loading = ref(false) const loading = ref(false)
@ -123,10 +97,6 @@ async function doLogin() {
emit('login', loginResult) emit('login', loginResult)
console.log(loginResult) console.log(loginResult)
if (loginResult.token && autoLogin.value) {
token.value = loginResult.token
}
} catch (e: any) { } catch (e: any) {
console.log(e.message) console.log(e.message)
error.value = e.message error.value = e.message

View File

@ -16,21 +16,6 @@ const routes = [
import(/* webpackChunkName: "home" */ '@/views/Login.vue') import(/* webpackChunkName: "home" */ '@/views/Login.vue')
} }
] ]
},
{
path: '/add-firewall',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'AddFirewall',
component: () =>
import(
/* webpackChunkName: "addFirewall" */ '@/views/AddFirewall.vue'
)
}
]
} }
] ]

View File

@ -8,11 +8,7 @@ const SERVER_URL = import.meta.env.VITE_SERVER_URL || ''
export const trpc = createTRPCProxyClient<AppRouter>({ export const trpc = createTRPCProxyClient<AppRouter>({
links: [ links: [
httpBatchLink({ httpBatchLink({
url: `${SERVER_URL}/trpc`, url: `${SERVER_URL}/trpc`
headers() {
const token = localStorage.getItem('token')
return token ? { Authorization: token } : {}
}
}) })
] ]
}) })

View File

@ -1,117 +0,0 @@
<template>
<v-container class="justify-center">
<v-row justify="center">
<v-col xl="5" lg="6" md="7" sm="10">
<v-alert type="info" class="mb-6">
É necessário um usuário local com permissões de escrita na API XML
para que o sistema possa logar os usuários. É possível usar o mesmo
usuário usado no sistema Portal de TI.
</v-alert>
<v-form ref="form" @submit.prevent="submit" lazy-validation>
<v-card title="Adicionar PA">
<v-card-text>
<v-text-field
class="mb-5"
v-model="ip"
label="Endereço IP"
:variant="'outlined'"
hint="Endereço de gestão do firewall"
prepend-inner-icon="mdi-ip"
required
validate-on="blur"
:rules="[v => validIP(v) || 'Endereço IP inválido']"
/>
<v-text-field
class="mb-5"
v-model="username"
label="Usuário local do firewall"
:variant="'outlined'"
hint="Usuário do firewall. Não usar usuário do Panorama"
prepend-inner-icon="mdi-account"
:rules="[v => !!v || 'Campo obrigatório']"
required
/>
<v-text-field
v-model="password"
label="Senha"
:type="showPassword ? 'text' : 'password'"
hint="Senha do usuário local do firewall"
:variant="'outlined'"
autocomplete="current-password"
prepend-inner-icon="mdi-key"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="showPassword = !showPassword"
:rules="[v => !!v || 'Campo obrigatório']"
required
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
class="px-6"
prepend-icon="mdi-plus"
:disabled="!valid"
size="x-large"
color="primary"
:variant="'flat'"
type="submit"
:loading="loading"
>
Adicionar
</v-btn>
</v-card-actions>
</v-card>
</v-form>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { VForm } from 'vuetify/lib/components/index.mjs'
import { z } from 'zod'
import { trpc } from '../trpc'
const form = ref<VForm>()
const valid = computed(() => {
return form.value?.isValid || false
})
const validIP = (v: string) => {
return z.string().ip().safeParse(v).success
}
const username = ref('')
const password = ref('')
const ip = ref('')
const showPassword = ref(false)
const loading = ref(false)
const router = useRouter()
async function submit() {
console.log('submit')
try {
loading.value = true
await trpc.addFirewall.mutate({
ip: ip.value,
username: username.value,
password: password.value
})
alert('Firewall adicionado com sucesso!')
router.push('/')
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
</script>