diff --git a/.env b/.env index 805fade..f9aa333 100755 --- a/.env +++ b/.env @@ -1,6 +1,8 @@ DATABASE_URL=postgres://postgres:123QWEasd@localhost:5432/evstation PORT=4000 +NODE_ENV=production + JWT_SECRET=umaChaveMuitoForteESegura123! MQTT_URL=mqtt://localhost:1883 diff --git a/app.js b/app.js deleted file mode 100644 index f1873b4..0000000 --- a/app.js +++ /dev/null @@ -1,49 +0,0 @@ -// app.js -require('dotenv').config(); - -// Enforce presence of JWT_SECRET -if (!process.env.JWT_SECRET) { - console.error('Error: JWT_SECRET is not defined in environment.'); - process.exit(1); -} - -const express = require('express'); - -// Route modules -const usersRoutes = require('./routes/users'); -const chargerRoutes = require('./routes/chargers'); -const sessionsRoutes = require('./routes/charger_sessions'); -const pushRoutes = require('./routes/push'); // ✅ ADICIONA ISTO - -const app = express(); - -// Global middlewares -app.use(express.json()); - -// Public routes (no auth) -app.use('/api/users', usersRoutes); - -// Protected routes -// ✅ NOTA: chargers/sessions/push já têm router.use(verifyToken) internamente -// portanto não precisas passar verifyToken aqui. -app.use('/api/chargers', chargerRoutes); -app.use('/api/charger_sessions', sessionsRoutes); -app.use('/api/push', pushRoutes); // ✅ ISTO resolve /api/push/vapid-public-key - -// Health check opcional (bom para produção) -app.get('/api/health', (req, res) => { - res.json({ success: true, ok: true }); -}); - -// Error handler (catch-all) -app.use((err, req, res, next) => { - console.error('Unhandled error:', err); - res - .status(err.status || 500) - .json({ - success: false, - message: err.message || 'Internal Server Error', - }); -}); - -module.exports = app; diff --git a/db.js b/db.js deleted file mode 100755 index 432a055..0000000 --- a/db.js +++ /dev/null @@ -1,15 +0,0 @@ -// ./db.js -require('dotenv').config(); -const knex = require('knex'); -const knexfile = require('./knexfile'); - -// Pega o ambiente (default pra 'development') -const environment = process.env.NODE_ENV || 'development'; - -// Recupera só as configurações daquele ambiente -const config = knexfile[environment]; - -// Inicializa o Knex com esse config -const db = knex(config); - -module.exports = db; diff --git a/knexfile.js b/knexfile.js index f69a463..5f680b8 100755 --- a/knexfile.js +++ b/knexfile.js @@ -1,11 +1,55 @@ +// knexfile.js require('dotenv').config(); +function must(name) { + const v = process.env[name]; + if (!v) throw new Error(`${name} não definido no .env`); + return v; +} + +const shared = { + client: 'pg', + migrations: { + directory: './src/db/migrations', + }, +}; + +function buildConnectionFromEnv() { + // Se houver DATABASE_URL, usa-o. + // Se PGSSL=true, aplica ssl no formato esperado pelo driver pg (dentro de connection). + const ssl = + process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined; + + if (process.env.DATABASE_URL) { + // knex aceita string, mas o ssl precisa estar no objeto: + return ssl + ? { connectionString: process.env.DATABASE_URL, ssl } + : process.env.DATABASE_URL; + } + + // fallback para vars soltas + return { + host: process.env.PGHOST || '127.0.0.1', + port: Number(process.env.PGPORT || 5432), + user: process.env.PGUSER || 'postgres', + password: process.env.PGPASSWORD || 'postgres', + database: process.env.PGDATABASE || 'evse', + ...(ssl ? { ssl } : {}), + }; +} + module.exports = { development: { - client: 'pg', - connection: process.env.DATABASE_URL, - migrations: { - directory: './migrations', - }, + ...shared, + connection: buildConnectionFromEnv(), + }, + + production: { + ...shared, + // Em produção normalmente queres obrigar DATABASE_URL (se for o teu caso): + // connection: must('DATABASE_URL'), + // Mas mantendo compatível com vars soltas: + connection: buildConnectionFromEnv(), + pool: { min: 2, max: 10 }, }, }; diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js deleted file mode 100755 index 6100719..0000000 --- a/middleware/verifyToken.js +++ /dev/null @@ -1,41 +0,0 @@ -const jwt = require('jsonwebtoken'); - -if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET não definido no .env'); -} - -function verifyToken(req, res, next) { - const authHeader = - req.headers['authorization'] || req.headers['Authorization']; - - if (!authHeader) { - return res.status(403).json({ error: 'Token não fornecido' }); - } - - const match = authHeader.match(/^Bearer\s+(.+)$/i); - if (!match) { - return res - .status(403) - .json({ error: 'Token malformado. Use "Bearer "' }); - } - - const token = match[1]; - - jwt.verify(token, process.env.JWT_SECRET, (err, payload) => { - if (err) { - if (err.name === 'TokenExpiredError') { - return res.status(403).json({ error: 'Sessão expirada' }); - } - return res.status(403).json({ error: 'Token inválido' }); - } - - if (!payload?.id) { - return res.status(403).json({ error: 'Token inválido' }); - } - - req.user = payload; - next(); - }); -} - -module.exports = verifyToken; diff --git a/migrate_structure.sh b/migrate_structure.sh new file mode 100755 index 0000000..25f645c --- /dev/null +++ b/migrate_structure.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "==> 0) Criar branch (opcional)" +# git checkout -b refactor/estrutura-src + +echo "==> 1) Criar pastas da nova estrutura" +mkdir -p src/{config,db,repositories,services,domain/normalize,mqtt/handlers,middleware,routes} +mkdir -p src/db/migrations + +echo "==> 2) Mover ficheiros principais" +# app/server +mv app.js src/app.js +mv server.js src/server.js + +# db +mv db.js src/db/knex.js + +# knexfile continua na raiz (ok) + +echo "==> 3) Mover routes" +mv routes/chargers.js src/routes/chargers.routes.js +mv routes/charger_sessions.js src/routes/sessions.routes.js +mv routes/push.js src/routes/push.routes.js +mv routes/users.js src/routes/users.routes.js + +echo "==> 4) Mover middleware" +mv middleware/verifyToken.js src/middleware/auth.js + +echo "==> 5) Mover MQTT" +mv mqtt/client.js src/mqtt/index.js + +echo "==> 6) Mover pushService (utils -> services)" +mv utils/pushService.js src/services/push.service.js + +echo "==> 7) Mover migrations" +mv migrations/* src/db/migrations/ + +echo "==> 8) Limpar pastas antigas (se vazias)" +rmdir routes 2>/dev/null || true +rmdir middleware 2>/dev/null || true +rmdir mqtt 2>/dev/null || true +rmdir utils 2>/dev/null || true +rmdir migrations 2>/dev/null || true + +echo "==> 9) Criar placeholders úteis (opcional)" +touch src/config/index.js +touch src/services/{chargers.service.js,configs.service.js,sessions.service.js} +touch src/repositories/{chargers.repo.js,sessions.repo.js,push.repo.js} +touch src/domain/normalize/{chargingStatus.js,chargingConfig.js} +touch src/middleware/{validate.js,errorHandler.js} +touch src/mqtt/publishers.js +touch src/mqtt/handlers/{evse.handler.js,meter.handler.js,legacy.handler.js} + +echo "==> 10) Mostrar nova árvore (até 4 níveis)" +tree -a -L 4 src || true + +echo "✅ Estrutura criada e ficheiros movidos." +echo "⚠️ Próximo passo: corrigir os imports/paths (vai quebrar até ajustar)." diff --git a/migrations/20250619_create_tables.js b/migrations/20250619_create_tables.js deleted file mode 100755 index 75b9355..0000000 --- a/migrations/20250619_create_tables.js +++ /dev/null @@ -1,74 +0,0 @@ -// migrations/20250619_create_tables.js -exports.up = async function(knex) { - // Create 'users' table - await knex.schema.createTable('users', (table) => { - table.increments('id').primary(); - table.string('username', 255).notNullable().unique(); - table.string('password', 255).notNullable(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - }); - - // Create 'chargers' table with new fields - await knex.schema.createTable('chargers', (table) => { - table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); - table.integer('user_id').unsigned().notNullable() - .references('id').inTable('users').onDelete('CASCADE'); - table.string('location', 255).notNullable(); - table.string('status', 50).notNullable().defaultTo('offline'); - table.integer('charging_current').notNullable().defaultTo(32); - table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()); - table.string('mqtt_user', 255).notNullable(); - table.string('mqtt_pass', 255).notNullable(); - table.string('mqtt_topic', 255).notNullable().unique(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - // Add power and current for 3 phases, voltage and other new fields - table.integer('charging_time').notNullable().defaultTo(0); // Total charging time - table.decimal('consumption', 8, 2).notNullable().defaultTo(0); // Consumption (kWh) - - // Power for 3 phases (L1, L2, L3) - table.decimal('power_l1', 8, 2).notNullable().defaultTo(0); - table.decimal('power_l2', 8, 2).notNullable().defaultTo(0); - table.decimal('power_l3', 8, 2).notNullable().defaultTo(0); - - // Voltage for 3 phases (L1, L2, L3) - table.decimal('voltage_l1', 8, 2).notNullable().defaultTo(0); - table.decimal('voltage_l2', 8, 2).notNullable().defaultTo(0); - table.decimal('voltage_l3', 8, 2).notNullable().defaultTo(0); - - // Current for 3 phases (L1, L2, L3) - table.decimal('current_l1', 8, 2).notNullable().defaultTo(0); - table.decimal('current_l2', 8, 2).notNullable().defaultTo(0); - table.decimal('current_l3', 8, 2).notNullable().defaultTo(0); - }); - - // Create 'charger_configs' table - await knex.schema.createTable('charger_configs', (table) => { - table.uuid('charger_id').primary() - .references('id').inTable('chargers').onDelete('CASCADE'); - table.integer('max_charging_current').notNullable().defaultTo(32); - table.boolean('require_auth').notNullable().defaultTo(false); - table.boolean('rcm_enabled').notNullable().defaultTo(false); - table.integer('temperature_limit').notNullable().defaultTo(60); - table.timestamp('config_received_at').notNullable().defaultTo(knex.fn.now()); - }); - - // Create 'charger_sessions' table - await knex.schema.createTable('charger_sessions', (table) => { - table.increments('id').primary(); - table.uuid('charger_id').notNullable() - .references('id').inTable('chargers').onDelete('CASCADE'); - table.timestamp('started_at').notNullable(); - table.timestamp('ended_at'); - table.decimal('kwh', 8, 2).notNullable().defaultTo(0); - table.decimal('cost', 10, 2); - table.timestamp('created_at').defaultTo(knex.fn.now()); - }); -}; - -exports.down = async function(knex) { - await knex.schema.dropTableIfExists('charger_sessions'); - await knex.schema.dropTableIfExists('charger_configs'); - await knex.schema.dropTableIfExists('chargers'); - await knex.schema.dropTableIfExists('users'); -}; diff --git a/mqtt/client.js b/mqtt/client.js deleted file mode 100755 index a7cb1fb..0000000 --- a/mqtt/client.js +++ /dev/null @@ -1,436 +0,0 @@ -// mqtt/client.js -const mqtt = require('mqtt'); -const EventEmitter = require('events'); -const db = require('../db'); -const { sendPushToUser } = require('../utils/pushService'); -require('dotenv').config(); - -const emitter = new EventEmitter(); - -const MQTT_URL = process.env.MQTT_URL || 'mqtt://localhost:1883'; -const mqttUser = process.env.MQTT_USER || 'admin'; -const mqttPass = process.env.MQTT_PASS || '123QWEasd'; - -const client = mqtt.connect(MQTT_URL, { - username: mqttUser, - password: mqttPass, - reconnectPeriod: 2000, -}); - -// -------------------- -// Helpers -// -------------------- -const lastEnabled = {}; // por chargerId - -function getStatusFromStateCode(code) { - const map = { - A1: '🔌 Not Conn.', - B1: '🟡 Unauth.', - B2: '🟢 Ready', - C1: '⚡ Wait', - C2: '⚡ Charging', - D1: '💨 Vent (req)', - D2: '💨 Vent', - E: '❌ CP Error', - F: '⚠️ Fault', - }; - return map[code] || '❓ Unknown'; -} - -function getTriple(arr) { - return Array.isArray(arr) - ? arr.slice(0, 3).map((n) => Math.round((Number(n) || 0) * 10) / 10) - : [0, 0, 0]; -} - -function toOneDecimal(v) { - const n = Number(v); - if (!Number.isFinite(n)) return 0; - return Math.round(n * 10) / 10; -} - -function shallowEqual(a, b) { - if (a === b) return true; - if (!a || !b) return false; - const ak = Object.keys(a); - const bk = Object.keys(b); - if (ak.length !== bk.length) return false; - for (const k of ak) { - const av = a[k]; - const bv = b[k]; - if (Array.isArray(av) || Array.isArray(bv)) { - if (!Array.isArray(av) || !Array.isArray(bv)) return false; - if (av.length !== bv.length) return false; - for (let i = 0; i < av.length; i++) { - if (av[i] !== bv[i]) return false; - } - } else if (av !== bv) { - return false; - } - } - return true; -} - -// -------------------- -// Caches -// -------------------- - -// último estado normalizado por chargerId -const lastStateByChargerId = new Map(); // chargerId -> normalizedState - -// cache charger por mqttTopic (evita SELECT a cada msg) -const chargerCache = new Map(); // mqttTopic -> { charger, fetchedAt } -const CHARGER_CACHE_TTL_MS = Number(process.env.CHARGER_CACHE_TTL_MS || 30000); - -async function getChargerByMqttTopic(mqttTopic) { - const cached = chargerCache.get(mqttTopic); - const now = Date.now(); - - if (cached && now - cached.fetchedAt < CHARGER_CACHE_TTL_MS) { - return cached.charger; - } - - const charger = await db('chargers').where({ mqtt_topic: mqttTopic }).first(); - if (charger) { - chargerCache.set(mqttTopic, { charger, fetchedAt: now }); - } - return charger; -} - -// -------------------- -// Subscribe -// -------------------- -client.on('connect', () => { - console.log('[MQTT] Conectado ao broker:', MQTT_URL); - - // ✅ tópicos fixos que TU pediste manter - const fixedTopics = ['+/state', '+/response/config/evse']; - - // opcional via env (mas não substitui os fixos) - const envTopics = (process.env.MQTT_SUB_TOPICS || '') - .split(',') - .map((s) => s.trim()) - .filter(Boolean); - - const topicsToSub = [...new Set([...fixedTopics, ...envTopics])]; - - topicsToSub.forEach((t) => { - client.subscribe(t, { qos: 0 }, (err, granted) => { - if (err) { - console.error('[MQTT] Falha ao subscrever', t, err.message); - } else { - console.log('[MQTT] Subscrito:', granted?.map((g) => g.topic).join(', ') || t); - } - }); - }); -}); - -// -------------------- -// Messages -// -------------------- -client.on('message', async (topic, message) => { - // LOG para garantir que está a consumir - console.log('[MQTT] msg recebida em:', topic); - - const parts = topic.split('/'); - const mqttTopic = parts[0]; // ex: bf92842c365a - const subtopic = parts.slice(1).join('/'); // ex: state ou response/config/evse - - // -------------------- - // STATE - // -------------------- - if (subtopic === 'state') { - try { - const payload = JSON.parse(message.toString()); - - const charger = await getChargerByMqttTopic(mqttTopic); - if (!charger) { - console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); - return; - } - - const chargerId = charger.id; - const now = new Date(); - - const stateCode = String(payload.state || '').split(' ')[0]; - if (!stateCode) { - console.warn(`[MQTT] Estado ausente/inválido para charger ID ${chargerId}`); - return; - } - - const status = getStatusFromStateCode(stateCode); - - const [p1, p2, p3] = getTriple(payload.power); - const [v1, v2, v3] = getTriple(payload.voltage); - const [c1, c2, c3] = getTriple(payload.current); - - const consumption = toOneDecimal(payload.consumption); - const chargingTime = toOneDecimal( - payload.chargingTime ?? payload.sessionTime - ); - - const normalizedState = { - status, - charging_current: c1, - consumption, - charging_time: chargingTime, - - power_l1: p1, - power_l2: p2, - power_l3: p3, - - voltage_l1: v1, - voltage_l2: v2, - voltage_l3: v3, - - current_l1: c1, - current_l2: c2, - current_l3: c3, - }; - - const prevState = lastStateByChargerId.get(chargerId); - const changed = !prevState || !shallowEqual(prevState, normalizedState); - - if (changed) { - await db('chargers') - .where({ id: chargerId }) - .update({ - ...normalizedState, - updated_at: now.toISOString(), - }); - - lastStateByChargerId.set(chargerId, normalizedState); - console.log(`[DB] Estado atualizado para charger ID ${chargerId}`); - } else { - // só para debug — podes remover depois - console.log(`[MQTT] Estado repetido, sem write (charger ${chargerId})`); - } - - // sessões start/stop (mantido) - const previouslyEnabled = lastEnabled[chargerId] || false; - const currentlyEnabled = stateCode === 'C2'; - - if (!previouslyEnabled && currentlyEnabled) { - const activeSession = await db('charger_sessions') - .where({ charger_id: chargerId }) - .whereNull('ended_at') - .first(); - - if (!activeSession) { - await db('charger_sessions').insert({ - charger_id: chargerId, - started_at: now, - kwh: consumption, - }); - console.log(`[DB] Sessão iniciada para charger ID ${chargerId}`); - } - } - - if (previouslyEnabled && !currentlyEnabled) { - const session = await db('charger_sessions') - .where({ charger_id: chargerId }) - .whereNull('ended_at') - .first(); - - if (session) { - await db('charger_sessions') - .where({ id: session.id }) - .update({ ended_at: now, kwh: consumption }); - - console.log(`[DB] Sessão finalizada para charger ID ${chargerId}`); - } - } - - lastEnabled[chargerId] = currentlyEnabled; - - // emit para socket - emitter.emit('charging-status', { - charger_id: chargerId, - mqtt_topic: mqttTopic, - status, - stateCode, - consumption, - chargingTime, - power: [p1, p2, p3], - voltage: [v1, v2, v3], - current: [c1, c2, c3], - raw: payload, - }); - - // push (mantido) - if (status === '⚠️ Fault' || status === '❌ CP Error') { - await sendPushToUser(charger.user_id, { - title: '⚠️ Erro no carregador', - body: `${charger.location || 'Carregador'} entrou em falha.`, - url: `/charger/${charger.id}`, - }); - } - - if (previouslyEnabled && !currentlyEnabled) { - await sendPushToUser(charger.user_id, { - title: '✅ Carregamento concluído', - body: `${charger.location || 'Carregador'} terminou o carregamento.`, - url: `/history`, - }); - } - } catch (err) { - console.error(`[MQTT] Erro ao processar state '${mqttTopic}':`, err); - console.error('Payload recebido:', message.toString()); - } - } - - // -------------------- - // CONFIG RESPONSE - // -------------------- - else if (subtopic === 'response/config/evse') { - try { - const payload = JSON.parse(message.toString()); - - const charger = await getChargerByMqttTopic(mqttTopic); - if (!charger) { - console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); - return; - } - - const configData = { - charger_id: charger.id, - max_charging_current: payload.maxChargingCurrent || 32, - require_auth: !!payload.requireAuth, - rcm_enabled: !!payload.rcm, - temperature_limit: payload.temperatureThreshold || 60, - config_received_at: new Date().toISOString(), - }; - - const existingConfig = await db('charger_configs') - .where({ charger_id: charger.id }) - .first(); - - const prevCfgNorm = existingConfig - ? { - max_charging_current: Number(existingConfig.max_charging_current) || 32, - require_auth: !!existingConfig.require_auth, - rcm_enabled: !!existingConfig.rcm_enabled, - temperature_limit: Number(existingConfig.temperature_limit) || 60, - } - : null; - - const nextCfgNorm = { - max_charging_current: Number(configData.max_charging_current) || 32, - require_auth: !!configData.require_auth, - rcm_enabled: !!configData.rcm_enabled, - temperature_limit: Number(configData.temperature_limit) || 60, - }; - - const cfgChanged = !prevCfgNorm || !shallowEqual(prevCfgNorm, nextCfgNorm); - - if (cfgChanged) { - if (existingConfig) { - await db('charger_configs') - .where({ charger_id: charger.id }) - .update(configData); - console.log(`[DB] Configuração atualizada para charger ID ${charger.id}`); - } else { - await db('charger_configs').insert(configData); - console.log(`[DB] Nova configuração inserida para charger ID ${charger.id}`); - } - } else { - console.log(`[MQTT] Config repetida, sem write (charger ${charger.id})`); - } - - emitter.emit('charging-config', { - ...configData, - mqtt_topic: mqttTopic, - raw: payload, - }); - } catch (err) { - console.error(`[MQTT] Erro ao processar config '${mqttTopic}':`, err); - console.error('Payload recebido:', message.toString()); - } - } -}); - -// -------------------- -// Broker offline / checker (igual ao teu) -// -------------------- -client.on('offline', async () => { - console.warn('[MQTT] Broker offline'); - - try { - const chargers = await db('chargers').select('id', 'user_id', 'location'); - const uniqueUsers = [...new Set(chargers.map((c) => c.user_id))]; - - await Promise.allSettled( - uniqueUsers.map((userId) => - sendPushToUser(userId, { - title: '📡 Broker MQTT offline', - body: 'O sistema perdeu ligação ao broker. Alguns estados podem estar desatualizados.', - url: '/', - }) - ) - ); - } catch (err) { - console.error('[MQTT] erro offline push:', err.message); - } -}); - -setInterval(async () => { - try { - const timeoutMinutes = Number(process.env.CHARGER_OFFLINE_MINUTES || 5); - const limitDate = new Date(Date.now() - timeoutMinutes * 60 * 1000); - - const offlineChargers = await db('chargers') - .where('updated_at', '<', limitDate.toISOString()) - .andWhereNot({ status: 'offline' }) - .select('*'); - - for (const ch of offlineChargers) { - await db('chargers') - .where({ id: ch.id }) - .update({ status: 'offline' }); - - lastStateByChargerId.delete(ch.id); - lastEnabled[ch.id] = false; - - await sendPushToUser(ch.user_id, { - title: '🔌 Carregador offline', - body: `${ch.location || 'Carregador'} está offline há mais de ${timeoutMinutes} min.`, - url: `/charger/${ch.id}`, - }); - } - } catch (err) { - console.error('[MQTT] offline checker erro:', err.message); - } -}, 60 * 1000); - -// -------------------- -// API pública -// -------------------- -function on(event, handler) { - emitter.on(event, handler); -} - -function sendConfig(chargerTopic, property, value) { - const payload = { [property]: value }; - client.publish(`${chargerTopic}/set/config/evse`, JSON.stringify(payload), { - qos: 1, - }); -} - -function sendEnable(chargerTopic, enable) { - client.publish( - `${chargerTopic}/enable`, - JSON.stringify({ enable: !!enable }), - { qos: 1 } - ); -} - -function requestConfig(chargerTopic) { - client.publish(`${chargerTopic}/request/config/evse`, null, { qos: 1 }); -} - -module.exports = { - on, - sendConfig, - sendEnable, - requestConfig, -}; diff --git a/node_modules/@socket.io/component-emitter/LICENSE b/node_modules/@socket.io/component-emitter/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/Readme.md b/node_modules/@socket.io/component-emitter/Readme.md old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts b/node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/lib/cjs/index.js b/node_modules/@socket.io/component-emitter/lib/cjs/index.js old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/lib/cjs/package.json b/node_modules/@socket.io/component-emitter/lib/cjs/package.json old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/lib/esm/index.d.ts b/node_modules/@socket.io/component-emitter/lib/esm/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/lib/esm/index.js b/node_modules/@socket.io/component-emitter/lib/esm/index.js old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/lib/esm/package.json b/node_modules/@socket.io/component-emitter/lib/esm/package.json old mode 100644 new mode 100755 diff --git a/node_modules/@socket.io/component-emitter/package.json b/node_modules/@socket.io/component-emitter/package.json old mode 100644 new mode 100755 diff --git a/node_modules/@types/cors/LICENSE b/node_modules/@types/cors/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/@types/cors/README.md b/node_modules/@types/cors/README.md old mode 100644 new mode 100755 diff --git a/node_modules/@types/cors/index.d.ts b/node_modules/@types/cors/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/@types/cors/package.json b/node_modules/@types/cors/package.json old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/LICENSE b/node_modules/agent-base/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/README.md b/node_modules/agent-base/README.md old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/helpers.d.ts b/node_modules/agent-base/dist/helpers.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/helpers.d.ts.map b/node_modules/agent-base/dist/helpers.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/helpers.js b/node_modules/agent-base/dist/helpers.js old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/helpers.js.map b/node_modules/agent-base/dist/helpers.js.map old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/index.d.ts b/node_modules/agent-base/dist/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/index.d.ts.map b/node_modules/agent-base/dist/index.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/index.js b/node_modules/agent-base/dist/index.js old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/dist/index.js.map b/node_modules/agent-base/dist/index.js.map old mode 100644 new mode 100755 diff --git a/node_modules/agent-base/package.json b/node_modules/agent-base/package.json old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/.eslintrc.js b/node_modules/asn1.js/.eslintrc.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/LICENSE b/node_modules/asn1.js/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/README.md b/node_modules/asn1.js/README.md old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1.js b/node_modules/asn1.js/lib/asn1.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/api.js b/node_modules/asn1.js/lib/asn1/api.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/base/buffer.js b/node_modules/asn1.js/lib/asn1/base/buffer.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/base/index.js b/node_modules/asn1.js/lib/asn1/base/index.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/base/node.js b/node_modules/asn1.js/lib/asn1/base/node.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/base/reporter.js b/node_modules/asn1.js/lib/asn1/base/reporter.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/constants/der.js b/node_modules/asn1.js/lib/asn1/constants/der.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/constants/index.js b/node_modules/asn1.js/lib/asn1/constants/index.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/decoders/der.js b/node_modules/asn1.js/lib/asn1/decoders/der.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/decoders/index.js b/node_modules/asn1.js/lib/asn1/decoders/index.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/decoders/pem.js b/node_modules/asn1.js/lib/asn1/decoders/pem.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/encoders/der.js b/node_modules/asn1.js/lib/asn1/encoders/der.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/encoders/index.js b/node_modules/asn1.js/lib/asn1/encoders/index.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/lib/asn1/encoders/pem.js b/node_modules/asn1.js/lib/asn1/encoders/pem.js old mode 100644 new mode 100755 diff --git a/node_modules/asn1.js/package.json b/node_modules/asn1.js/package.json old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/LICENSE b/node_modules/asynckit/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/README.md b/node_modules/asynckit/README.md old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/bench.js b/node_modules/asynckit/bench.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/index.js b/node_modules/asynckit/index.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/abort.js b/node_modules/asynckit/lib/abort.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/async.js b/node_modules/asynckit/lib/async.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/defer.js b/node_modules/asynckit/lib/defer.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/iterate.js b/node_modules/asynckit/lib/iterate.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/readable_asynckit.js b/node_modules/asynckit/lib/readable_asynckit.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/readable_parallel.js b/node_modules/asynckit/lib/readable_parallel.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/readable_serial.js b/node_modules/asynckit/lib/readable_serial.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/readable_serial_ordered.js b/node_modules/asynckit/lib/readable_serial_ordered.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/state.js b/node_modules/asynckit/lib/state.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/streamify.js b/node_modules/asynckit/lib/streamify.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/lib/terminator.js b/node_modules/asynckit/lib/terminator.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/package.json b/node_modules/asynckit/package.json old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/parallel.js b/node_modules/asynckit/parallel.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/serial.js b/node_modules/asynckit/serial.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/serialOrdered.js b/node_modules/asynckit/serialOrdered.js old mode 100644 new mode 100755 diff --git a/node_modules/asynckit/stream.js b/node_modules/asynckit/stream.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/CHANGELOG.md b/node_modules/axios/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/node_modules/axios/LICENSE b/node_modules/axios/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/axios/MIGRATION_GUIDE.md b/node_modules/axios/MIGRATION_GUIDE.md old mode 100644 new mode 100755 diff --git a/node_modules/axios/README.md b/node_modules/axios/README.md old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/axios.js b/node_modules/axios/dist/axios.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/axios.js.map b/node_modules/axios/dist/axios.js.map old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/axios.min.js b/node_modules/axios/dist/axios.min.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/axios.min.js.map b/node_modules/axios/dist/axios.min.js.map old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/browser/axios.cjs b/node_modules/axios/dist/browser/axios.cjs old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/browser/axios.cjs.map b/node_modules/axios/dist/browser/axios.cjs.map old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/esm/axios.js b/node_modules/axios/dist/esm/axios.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/esm/axios.js.map b/node_modules/axios/dist/esm/axios.js.map old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/esm/axios.min.js b/node_modules/axios/dist/esm/axios.min.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/esm/axios.min.js.map b/node_modules/axios/dist/esm/axios.min.js.map old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/node/axios.cjs b/node_modules/axios/dist/node/axios.cjs old mode 100644 new mode 100755 diff --git a/node_modules/axios/dist/node/axios.cjs.map b/node_modules/axios/dist/node/axios.cjs.map old mode 100644 new mode 100755 diff --git a/node_modules/axios/index.d.cts b/node_modules/axios/index.d.cts old mode 100644 new mode 100755 diff --git a/node_modules/axios/index.d.ts b/node_modules/axios/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/axios/index.js b/node_modules/axios/index.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/adapters/README.md b/node_modules/axios/lib/adapters/README.md old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/adapters/adapters.js b/node_modules/axios/lib/adapters/adapters.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/adapters/fetch.js b/node_modules/axios/lib/adapters/fetch.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/adapters/xhr.js b/node_modules/axios/lib/adapters/xhr.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/axios.js b/node_modules/axios/lib/axios.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/cancel/CancelToken.js b/node_modules/axios/lib/cancel/CancelToken.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/cancel/CanceledError.js b/node_modules/axios/lib/cancel/CanceledError.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/cancel/isCancel.js b/node_modules/axios/lib/cancel/isCancel.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/Axios.js b/node_modules/axios/lib/core/Axios.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/AxiosError.js b/node_modules/axios/lib/core/AxiosError.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/AxiosHeaders.js b/node_modules/axios/lib/core/AxiosHeaders.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/InterceptorManager.js b/node_modules/axios/lib/core/InterceptorManager.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/README.md b/node_modules/axios/lib/core/README.md old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/buildFullPath.js b/node_modules/axios/lib/core/buildFullPath.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/dispatchRequest.js b/node_modules/axios/lib/core/dispatchRequest.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/mergeConfig.js b/node_modules/axios/lib/core/mergeConfig.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/settle.js b/node_modules/axios/lib/core/settle.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/core/transformData.js b/node_modules/axios/lib/core/transformData.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/defaults/index.js b/node_modules/axios/lib/defaults/index.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/defaults/transitional.js b/node_modules/axios/lib/defaults/transitional.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/env/README.md b/node_modules/axios/lib/env/README.md old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/env/classes/FormData.js b/node_modules/axios/lib/env/classes/FormData.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/env/data.js b/node_modules/axios/lib/env/data.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/AxiosTransformStream.js b/node_modules/axios/lib/helpers/AxiosTransformStream.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/AxiosURLSearchParams.js b/node_modules/axios/lib/helpers/AxiosURLSearchParams.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/HttpStatusCode.js b/node_modules/axios/lib/helpers/HttpStatusCode.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/README.md b/node_modules/axios/lib/helpers/README.md old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/ZlibHeaderTransformStream.js b/node_modules/axios/lib/helpers/ZlibHeaderTransformStream.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/bind.js b/node_modules/axios/lib/helpers/bind.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/buildURL.js b/node_modules/axios/lib/helpers/buildURL.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/callbackify.js b/node_modules/axios/lib/helpers/callbackify.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/combineURLs.js b/node_modules/axios/lib/helpers/combineURLs.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/composeSignals.js b/node_modules/axios/lib/helpers/composeSignals.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/cookies.js b/node_modules/axios/lib/helpers/cookies.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/deprecatedMethod.js b/node_modules/axios/lib/helpers/deprecatedMethod.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js b/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/formDataToJSON.js b/node_modules/axios/lib/helpers/formDataToJSON.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/formDataToStream.js b/node_modules/axios/lib/helpers/formDataToStream.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/fromDataURI.js b/node_modules/axios/lib/helpers/fromDataURI.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/isAbsoluteURL.js b/node_modules/axios/lib/helpers/isAbsoluteURL.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/isAxiosError.js b/node_modules/axios/lib/helpers/isAxiosError.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/isURLSameOrigin.js b/node_modules/axios/lib/helpers/isURLSameOrigin.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/null.js b/node_modules/axios/lib/helpers/null.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/parseHeaders.js b/node_modules/axios/lib/helpers/parseHeaders.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/parseProtocol.js b/node_modules/axios/lib/helpers/parseProtocol.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/progressEventReducer.js b/node_modules/axios/lib/helpers/progressEventReducer.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/readBlob.js b/node_modules/axios/lib/helpers/readBlob.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/resolveConfig.js b/node_modules/axios/lib/helpers/resolveConfig.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/speedometer.js b/node_modules/axios/lib/helpers/speedometer.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/spread.js b/node_modules/axios/lib/helpers/spread.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/throttle.js b/node_modules/axios/lib/helpers/throttle.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/toFormData.js b/node_modules/axios/lib/helpers/toFormData.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/toURLEncodedForm.js b/node_modules/axios/lib/helpers/toURLEncodedForm.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/trackStream.js b/node_modules/axios/lib/helpers/trackStream.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/helpers/validator.js b/node_modules/axios/lib/helpers/validator.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/browser/classes/Blob.js b/node_modules/axios/lib/platform/browser/classes/Blob.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/browser/classes/FormData.js b/node_modules/axios/lib/platform/browser/classes/FormData.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/browser/classes/URLSearchParams.js b/node_modules/axios/lib/platform/browser/classes/URLSearchParams.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/browser/index.js b/node_modules/axios/lib/platform/browser/index.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/common/utils.js b/node_modules/axios/lib/platform/common/utils.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/index.js b/node_modules/axios/lib/platform/index.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/node/classes/FormData.js b/node_modules/axios/lib/platform/node/classes/FormData.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/node/classes/URLSearchParams.js b/node_modules/axios/lib/platform/node/classes/URLSearchParams.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/platform/node/index.js b/node_modules/axios/lib/platform/node/index.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/lib/utils.js b/node_modules/axios/lib/utils.js old mode 100644 new mode 100755 diff --git a/node_modules/axios/package.json b/node_modules/axios/package.json old mode 100644 new mode 100755 diff --git a/node_modules/base64id/CHANGELOG.md b/node_modules/base64id/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/node_modules/base64id/LICENSE b/node_modules/base64id/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/base64id/README.md b/node_modules/base64id/README.md old mode 100644 new mode 100755 diff --git a/node_modules/base64id/lib/base64id.js b/node_modules/base64id/lib/base64id.js old mode 100644 new mode 100755 diff --git a/node_modules/base64id/package.json b/node_modules/base64id/package.json old mode 100644 new mode 100755 diff --git a/node_modules/bn.js/LICENSE b/node_modules/bn.js/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/bn.js/README.md b/node_modules/bn.js/README.md old mode 100644 new mode 100755 diff --git a/node_modules/bn.js/lib/bn.js b/node_modules/bn.js/lib/bn.js old mode 100644 new mode 100755 diff --git a/node_modules/bn.js/package.json b/node_modules/bn.js/package.json old mode 100644 new mode 100755 diff --git a/node_modules/bn.js/util/genCombMulTo.js b/node_modules/bn.js/util/genCombMulTo.js old mode 100644 new mode 100755 diff --git a/node_modules/bn.js/util/genCombMulTo10.js b/node_modules/bn.js/util/genCombMulTo10.js old mode 100644 new mode 100755 diff --git a/node_modules/combined-stream/License b/node_modules/combined-stream/License old mode 100644 new mode 100755 diff --git a/node_modules/combined-stream/Readme.md b/node_modules/combined-stream/Readme.md old mode 100644 new mode 100755 diff --git a/node_modules/combined-stream/lib/combined_stream.js b/node_modules/combined-stream/lib/combined_stream.js old mode 100644 new mode 100755 diff --git a/node_modules/combined-stream/package.json b/node_modules/combined-stream/package.json old mode 100644 new mode 100755 diff --git a/node_modules/combined-stream/yarn.lock b/node_modules/combined-stream/yarn.lock old mode 100644 new mode 100755 diff --git a/node_modules/cors/CONTRIBUTING.md b/node_modules/cors/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/node_modules/cors/HISTORY.md b/node_modules/cors/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/cors/LICENSE b/node_modules/cors/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/cors/README.md b/node_modules/cors/README.md old mode 100644 new mode 100755 diff --git a/node_modules/cors/lib/index.js b/node_modules/cors/lib/index.js old mode 100644 new mode 100755 diff --git a/node_modules/cors/package.json b/node_modules/cors/package.json old mode 100644 new mode 100755 diff --git a/node_modules/delayed-stream/.npmignore b/node_modules/delayed-stream/.npmignore old mode 100644 new mode 100755 diff --git a/node_modules/delayed-stream/License b/node_modules/delayed-stream/License old mode 100644 new mode 100755 diff --git a/node_modules/delayed-stream/Makefile b/node_modules/delayed-stream/Makefile old mode 100644 new mode 100755 diff --git a/node_modules/delayed-stream/Readme.md b/node_modules/delayed-stream/Readme.md old mode 100644 new mode 100755 diff --git a/node_modules/delayed-stream/lib/delayed_stream.js b/node_modules/delayed-stream/lib/delayed_stream.js old mode 100644 new mode 100755 diff --git a/node_modules/delayed-stream/package.json b/node_modules/delayed-stream/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/LICENSE b/node_modules/engine.io-parser/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/Readme.md b/node_modules/engine.io-parser/Readme.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/commons.d.ts b/node_modules/engine.io-parser/build/cjs/commons.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/commons.js b/node_modules/engine.io-parser/build/cjs/commons.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/contrib/base64-arraybuffer.d.ts b/node_modules/engine.io-parser/build/cjs/contrib/base64-arraybuffer.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/contrib/base64-arraybuffer.js b/node_modules/engine.io-parser/build/cjs/contrib/base64-arraybuffer.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/decodePacket.browser.d.ts b/node_modules/engine.io-parser/build/cjs/decodePacket.browser.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/decodePacket.browser.js b/node_modules/engine.io-parser/build/cjs/decodePacket.browser.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/decodePacket.d.ts b/node_modules/engine.io-parser/build/cjs/decodePacket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/decodePacket.js b/node_modules/engine.io-parser/build/cjs/decodePacket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/encodePacket.browser.d.ts b/node_modules/engine.io-parser/build/cjs/encodePacket.browser.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/encodePacket.browser.js b/node_modules/engine.io-parser/build/cjs/encodePacket.browser.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/encodePacket.d.ts b/node_modules/engine.io-parser/build/cjs/encodePacket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/encodePacket.js b/node_modules/engine.io-parser/build/cjs/encodePacket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/index.d.ts b/node_modules/engine.io-parser/build/cjs/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/index.js b/node_modules/engine.io-parser/build/cjs/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/cjs/package.json b/node_modules/engine.io-parser/build/cjs/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/commons.d.ts b/node_modules/engine.io-parser/build/esm/commons.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/commons.js b/node_modules/engine.io-parser/build/esm/commons.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.d.ts b/node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.js b/node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/decodePacket.browser.d.ts b/node_modules/engine.io-parser/build/esm/decodePacket.browser.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/decodePacket.browser.js b/node_modules/engine.io-parser/build/esm/decodePacket.browser.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/decodePacket.d.ts b/node_modules/engine.io-parser/build/esm/decodePacket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/decodePacket.js b/node_modules/engine.io-parser/build/esm/decodePacket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/encodePacket.browser.d.ts b/node_modules/engine.io-parser/build/esm/encodePacket.browser.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/encodePacket.browser.js b/node_modules/engine.io-parser/build/esm/encodePacket.browser.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/encodePacket.d.ts b/node_modules/engine.io-parser/build/esm/encodePacket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/encodePacket.js b/node_modules/engine.io-parser/build/esm/encodePacket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/index.d.ts b/node_modules/engine.io-parser/build/esm/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/index.js b/node_modules/engine.io-parser/build/esm/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/build/esm/package.json b/node_modules/engine.io-parser/build/esm/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io-parser/package.json b/node_modules/engine.io-parser/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/LICENSE b/node_modules/engine.io/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/README.md b/node_modules/engine.io/README.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/contrib/types.cookie.d.ts b/node_modules/engine.io/build/contrib/types.cookie.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/contrib/types.cookie.js b/node_modules/engine.io/build/contrib/types.cookie.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/engine.io.d.ts b/node_modules/engine.io/build/engine.io.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/engine.io.js b/node_modules/engine.io/build/engine.io.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/parser-v3/index.d.ts b/node_modules/engine.io/build/parser-v3/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/parser-v3/index.js b/node_modules/engine.io/build/parser-v3/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/parser-v3/utf8.d.ts b/node_modules/engine.io/build/parser-v3/utf8.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/parser-v3/utf8.js b/node_modules/engine.io/build/parser-v3/utf8.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/server.d.ts b/node_modules/engine.io/build/server.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/server.js b/node_modules/engine.io/build/server.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/socket.d.ts b/node_modules/engine.io/build/socket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/socket.js b/node_modules/engine.io/build/socket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transport.d.ts b/node_modules/engine.io/build/transport.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transport.js b/node_modules/engine.io/build/transport.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports-uws/index.d.ts b/node_modules/engine.io/build/transports-uws/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports-uws/index.js b/node_modules/engine.io/build/transports-uws/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports-uws/polling.d.ts b/node_modules/engine.io/build/transports-uws/polling.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports-uws/polling.js b/node_modules/engine.io/build/transports-uws/polling.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports-uws/websocket.d.ts b/node_modules/engine.io/build/transports-uws/websocket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports-uws/websocket.js b/node_modules/engine.io/build/transports-uws/websocket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/index.d.ts b/node_modules/engine.io/build/transports/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/index.js b/node_modules/engine.io/build/transports/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/polling-jsonp.d.ts b/node_modules/engine.io/build/transports/polling-jsonp.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/polling-jsonp.js b/node_modules/engine.io/build/transports/polling-jsonp.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/polling.d.ts b/node_modules/engine.io/build/transports/polling.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/polling.js b/node_modules/engine.io/build/transports/polling.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/websocket.d.ts b/node_modules/engine.io/build/transports/websocket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/websocket.js b/node_modules/engine.io/build/transports/websocket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/webtransport.d.ts b/node_modules/engine.io/build/transports/webtransport.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/transports/webtransport.js b/node_modules/engine.io/build/transports/webtransport.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/userver.d.ts b/node_modules/engine.io/build/userver.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/build/userver.js b/node_modules/engine.io/build/userver.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/accepts/HISTORY.md b/node_modules/engine.io/node_modules/accepts/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/accepts/LICENSE b/node_modules/engine.io/node_modules/accepts/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/accepts/README.md b/node_modules/engine.io/node_modules/accepts/README.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/accepts/index.js b/node_modules/engine.io/node_modules/accepts/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/accepts/package.json b/node_modules/engine.io/node_modules/accepts/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/debug/LICENSE b/node_modules/engine.io/node_modules/debug/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/debug/README.md b/node_modules/engine.io/node_modules/debug/README.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/debug/package.json b/node_modules/engine.io/node_modules/debug/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/debug/src/browser.js b/node_modules/engine.io/node_modules/debug/src/browser.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/debug/src/common.js b/node_modules/engine.io/node_modules/debug/src/common.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/debug/src/index.js b/node_modules/engine.io/node_modules/debug/src/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/debug/src/node.js b/node_modules/engine.io/node_modules/debug/src/node.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-db/HISTORY.md b/node_modules/engine.io/node_modules/mime-db/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-db/LICENSE b/node_modules/engine.io/node_modules/mime-db/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-db/README.md b/node_modules/engine.io/node_modules/mime-db/README.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-db/db.json b/node_modules/engine.io/node_modules/mime-db/db.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-db/index.js b/node_modules/engine.io/node_modules/mime-db/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-db/package.json b/node_modules/engine.io/node_modules/mime-db/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-types/HISTORY.md b/node_modules/engine.io/node_modules/mime-types/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-types/LICENSE b/node_modules/engine.io/node_modules/mime-types/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-types/README.md b/node_modules/engine.io/node_modules/mime-types/README.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-types/index.js b/node_modules/engine.io/node_modules/mime-types/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/mime-types/package.json b/node_modules/engine.io/node_modules/mime-types/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/HISTORY.md b/node_modules/engine.io/node_modules/negotiator/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/LICENSE b/node_modules/engine.io/node_modules/negotiator/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/README.md b/node_modules/engine.io/node_modules/negotiator/README.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/index.js b/node_modules/engine.io/node_modules/negotiator/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/lib/charset.js b/node_modules/engine.io/node_modules/negotiator/lib/charset.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/lib/encoding.js b/node_modules/engine.io/node_modules/negotiator/lib/encoding.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/lib/language.js b/node_modules/engine.io/node_modules/negotiator/lib/language.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/lib/mediaType.js b/node_modules/engine.io/node_modules/negotiator/lib/mediaType.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/negotiator/package.json b/node_modules/engine.io/node_modules/negotiator/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/LICENSE b/node_modules/engine.io/node_modules/ws/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/README.md b/node_modules/engine.io/node_modules/ws/README.md old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/browser.js b/node_modules/engine.io/node_modules/ws/browser.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/index.js b/node_modules/engine.io/node_modules/ws/index.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/buffer-util.js b/node_modules/engine.io/node_modules/ws/lib/buffer-util.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/constants.js b/node_modules/engine.io/node_modules/ws/lib/constants.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/event-target.js b/node_modules/engine.io/node_modules/ws/lib/event-target.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/extension.js b/node_modules/engine.io/node_modules/ws/lib/extension.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/limiter.js b/node_modules/engine.io/node_modules/ws/lib/limiter.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/permessage-deflate.js b/node_modules/engine.io/node_modules/ws/lib/permessage-deflate.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/receiver.js b/node_modules/engine.io/node_modules/ws/lib/receiver.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/sender.js b/node_modules/engine.io/node_modules/ws/lib/sender.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/stream.js b/node_modules/engine.io/node_modules/ws/lib/stream.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/subprotocol.js b/node_modules/engine.io/node_modules/ws/lib/subprotocol.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/validation.js b/node_modules/engine.io/node_modules/ws/lib/validation.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/websocket-server.js b/node_modules/engine.io/node_modules/ws/lib/websocket-server.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/lib/websocket.js b/node_modules/engine.io/node_modules/ws/lib/websocket.js old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/package.json b/node_modules/engine.io/node_modules/ws/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/node_modules/ws/wrapper.mjs b/node_modules/engine.io/node_modules/ws/wrapper.mjs old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/package.json b/node_modules/engine.io/package.json old mode 100644 new mode 100755 diff --git a/node_modules/engine.io/wrapper.mjs b/node_modules/engine.io/wrapper.mjs old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/.eslintrc b/node_modules/es-set-tostringtag/.eslintrc old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/.nycrc b/node_modules/es-set-tostringtag/.nycrc old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/CHANGELOG.md b/node_modules/es-set-tostringtag/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/LICENSE b/node_modules/es-set-tostringtag/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/README.md b/node_modules/es-set-tostringtag/README.md old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/index.d.ts b/node_modules/es-set-tostringtag/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/index.js b/node_modules/es-set-tostringtag/index.js old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/package.json b/node_modules/es-set-tostringtag/package.json old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/test/index.js b/node_modules/es-set-tostringtag/test/index.js old mode 100644 new mode 100755 diff --git a/node_modules/es-set-tostringtag/tsconfig.json b/node_modules/es-set-tostringtag/tsconfig.json old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/dist/index.cjs b/node_modules/express-rate-limit/dist/index.cjs old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/dist/index.d.cts b/node_modules/express-rate-limit/dist/index.d.cts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/dist/index.d.mts b/node_modules/express-rate-limit/dist/index.d.mts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/dist/index.d.ts b/node_modules/express-rate-limit/dist/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/dist/index.mjs b/node_modules/express-rate-limit/dist/index.mjs old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/license.md b/node_modules/express-rate-limit/license.md old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/LICENSE b/node_modules/express-rate-limit/node_modules/ip-address/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/README.md b/node_modules/express-rate-limit/node_modules/ip-address/README.md old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/address-error.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/common.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/common.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/common.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/common.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/common.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/common.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/common.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/common.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/ip-address.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv4.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/ipv6.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v4/constants.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/constants.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/helpers.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.d.ts b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.d.ts.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.js b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.js old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.js.map b/node_modules/express-rate-limit/node_modules/ip-address/dist/v6/regular-expressions.js.map old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/package.json b/node_modules/express-rate-limit/node_modules/ip-address/package.json old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/address-error.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/address-error.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/common.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/common.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/ip-address.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/ip-address.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/ipv4.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/ipv4.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/ipv6.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/ipv6.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/v4/constants.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/v4/constants.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/v6/constants.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/v6/constants.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/v6/helpers.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/v6/helpers.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/node_modules/ip-address/src/v6/regular-expressions.ts b/node_modules/express-rate-limit/node_modules/ip-address/src/v6/regular-expressions.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/package.json b/node_modules/express-rate-limit/package.json old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/readme.md b/node_modules/express-rate-limit/readme.md old mode 100644 new mode 100755 diff --git a/node_modules/express-rate-limit/tsconfig.json b/node_modules/express-rate-limit/tsconfig.json old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/LICENSE b/node_modules/express-validator/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/README.md b/node_modules/express-validator/README.md old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/base.d.ts b/node_modules/express-validator/lib/base.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/base.js b/node_modules/express-validator/lib/base.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-handler-impl.d.ts b/node_modules/express-validator/lib/chain/context-handler-impl.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-handler-impl.js b/node_modules/express-validator/lib/chain/context-handler-impl.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-handler.d.ts b/node_modules/express-validator/lib/chain/context-handler.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-handler.js b/node_modules/express-validator/lib/chain/context-handler.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-runner-impl.d.ts b/node_modules/express-validator/lib/chain/context-runner-impl.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-runner-impl.js b/node_modules/express-validator/lib/chain/context-runner-impl.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-runner.d.ts b/node_modules/express-validator/lib/chain/context-runner.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/context-runner.js b/node_modules/express-validator/lib/chain/context-runner.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/index.d.ts b/node_modules/express-validator/lib/chain/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/index.js b/node_modules/express-validator/lib/chain/index.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/sanitizers-impl.d.ts b/node_modules/express-validator/lib/chain/sanitizers-impl.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/sanitizers-impl.js b/node_modules/express-validator/lib/chain/sanitizers-impl.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/sanitizers.d.ts b/node_modules/express-validator/lib/chain/sanitizers.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/sanitizers.js b/node_modules/express-validator/lib/chain/sanitizers.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/validation-chain.d.ts b/node_modules/express-validator/lib/chain/validation-chain.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/validation-chain.js b/node_modules/express-validator/lib/chain/validation-chain.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/validators-impl.d.ts b/node_modules/express-validator/lib/chain/validators-impl.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/validators-impl.js b/node_modules/express-validator/lib/chain/validators-impl.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/validators.d.ts b/node_modules/express-validator/lib/chain/validators.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/chain/validators.js b/node_modules/express-validator/lib/chain/validators.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-builder.d.ts b/node_modules/express-validator/lib/context-builder.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-builder.js b/node_modules/express-validator/lib/context-builder.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/bail.d.ts b/node_modules/express-validator/lib/context-items/bail.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/bail.js b/node_modules/express-validator/lib/context-items/bail.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/chain-condition.d.ts b/node_modules/express-validator/lib/context-items/chain-condition.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/chain-condition.js b/node_modules/express-validator/lib/context-items/chain-condition.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/context-item.d.ts b/node_modules/express-validator/lib/context-items/context-item.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/context-item.js b/node_modules/express-validator/lib/context-items/context-item.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/custom-condition.d.ts b/node_modules/express-validator/lib/context-items/custom-condition.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/custom-condition.js b/node_modules/express-validator/lib/context-items/custom-condition.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/custom-validation.d.ts b/node_modules/express-validator/lib/context-items/custom-validation.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/custom-validation.js b/node_modules/express-validator/lib/context-items/custom-validation.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/index.d.ts b/node_modules/express-validator/lib/context-items/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/index.js b/node_modules/express-validator/lib/context-items/index.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/sanitization.d.ts b/node_modules/express-validator/lib/context-items/sanitization.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/sanitization.js b/node_modules/express-validator/lib/context-items/sanitization.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/standard-validation.d.ts b/node_modules/express-validator/lib/context-items/standard-validation.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context-items/standard-validation.js b/node_modules/express-validator/lib/context-items/standard-validation.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context.d.ts b/node_modules/express-validator/lib/context.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/context.js b/node_modules/express-validator/lib/context.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/express-validator.d.ts b/node_modules/express-validator/lib/express-validator.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/express-validator.js b/node_modules/express-validator/lib/express-validator.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/field-selection.d.ts b/node_modules/express-validator/lib/field-selection.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/field-selection.js b/node_modules/express-validator/lib/field-selection.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/index.d.ts b/node_modules/express-validator/lib/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/index.js b/node_modules/express-validator/lib/index.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/matched-data.d.ts b/node_modules/express-validator/lib/matched-data.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/matched-data.js b/node_modules/express-validator/lib/matched-data.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/check.d.ts b/node_modules/express-validator/lib/middlewares/check.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/check.js b/node_modules/express-validator/lib/middlewares/check.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/exact.d.ts b/node_modules/express-validator/lib/middlewares/exact.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/exact.js b/node_modules/express-validator/lib/middlewares/exact.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/one-of.d.ts b/node_modules/express-validator/lib/middlewares/one-of.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/one-of.js b/node_modules/express-validator/lib/middlewares/one-of.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/schema.d.ts b/node_modules/express-validator/lib/middlewares/schema.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/schema.js b/node_modules/express-validator/lib/middlewares/schema.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/validation-chain-builders.d.ts b/node_modules/express-validator/lib/middlewares/validation-chain-builders.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/middlewares/validation-chain-builders.js b/node_modules/express-validator/lib/middlewares/validation-chain-builders.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/options.d.ts b/node_modules/express-validator/lib/options.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/options.js b/node_modules/express-validator/lib/options.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/utils.d.ts b/node_modules/express-validator/lib/utils.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/utils.js b/node_modules/express-validator/lib/utils.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/validation-result.d.ts b/node_modules/express-validator/lib/validation-result.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/lib/validation-result.js b/node_modules/express-validator/lib/validation-result.js old mode 100644 new mode 100755 diff --git a/node_modules/express-validator/package.json b/node_modules/express-validator/package.json old mode 100644 new mode 100755 diff --git a/node_modules/follow-redirects/LICENSE b/node_modules/follow-redirects/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/follow-redirects/README.md b/node_modules/follow-redirects/README.md old mode 100644 new mode 100755 diff --git a/node_modules/follow-redirects/debug.js b/node_modules/follow-redirects/debug.js old mode 100644 new mode 100755 diff --git a/node_modules/follow-redirects/http.js b/node_modules/follow-redirects/http.js old mode 100644 new mode 100755 diff --git a/node_modules/follow-redirects/https.js b/node_modules/follow-redirects/https.js old mode 100644 new mode 100755 diff --git a/node_modules/follow-redirects/index.js b/node_modules/follow-redirects/index.js old mode 100644 new mode 100755 diff --git a/node_modules/follow-redirects/package.json b/node_modules/follow-redirects/package.json old mode 100644 new mode 100755 diff --git a/node_modules/form-data/CHANGELOG.md b/node_modules/form-data/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/node_modules/form-data/License b/node_modules/form-data/License old mode 100644 new mode 100755 diff --git a/node_modules/form-data/README.md b/node_modules/form-data/README.md old mode 100644 new mode 100755 diff --git a/node_modules/form-data/index.d.ts b/node_modules/form-data/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/form-data/lib/browser.js b/node_modules/form-data/lib/browser.js old mode 100644 new mode 100755 diff --git a/node_modules/form-data/lib/form_data.js b/node_modules/form-data/lib/form_data.js old mode 100644 new mode 100755 diff --git a/node_modules/form-data/lib/populate.js b/node_modules/form-data/lib/populate.js old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-db/HISTORY.md b/node_modules/form-data/node_modules/mime-db/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-db/LICENSE b/node_modules/form-data/node_modules/mime-db/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-db/README.md b/node_modules/form-data/node_modules/mime-db/README.md old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-db/db.json b/node_modules/form-data/node_modules/mime-db/db.json old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-db/index.js b/node_modules/form-data/node_modules/mime-db/index.js old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-db/package.json b/node_modules/form-data/node_modules/mime-db/package.json old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-types/HISTORY.md b/node_modules/form-data/node_modules/mime-types/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-types/LICENSE b/node_modules/form-data/node_modules/mime-types/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-types/README.md b/node_modules/form-data/node_modules/mime-types/README.md old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-types/index.js b/node_modules/form-data/node_modules/mime-types/index.js old mode 100644 new mode 100755 diff --git a/node_modules/form-data/node_modules/mime-types/package.json b/node_modules/form-data/node_modules/mime-types/package.json old mode 100644 new mode 100755 diff --git a/node_modules/form-data/package.json b/node_modules/form-data/package.json old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/.eslintrc b/node_modules/has-tostringtag/.eslintrc old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/.github/FUNDING.yml b/node_modules/has-tostringtag/.github/FUNDING.yml old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/.nycrc b/node_modules/has-tostringtag/.nycrc old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/CHANGELOG.md b/node_modules/has-tostringtag/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/LICENSE b/node_modules/has-tostringtag/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/README.md b/node_modules/has-tostringtag/README.md old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/index.d.ts b/node_modules/has-tostringtag/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/index.js b/node_modules/has-tostringtag/index.js old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/package.json b/node_modules/has-tostringtag/package.json old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/shams.d.ts b/node_modules/has-tostringtag/shams.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/shams.js b/node_modules/has-tostringtag/shams.js old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/test/index.js b/node_modules/has-tostringtag/test/index.js old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/test/shams/core-js.js b/node_modules/has-tostringtag/test/shams/core-js.js old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/test/shams/get-own-property-symbols.js b/node_modules/has-tostringtag/test/shams/get-own-property-symbols.js old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/test/tests.js b/node_modules/has-tostringtag/test/tests.js old mode 100644 new mode 100755 diff --git a/node_modules/has-tostringtag/tsconfig.json b/node_modules/has-tostringtag/tsconfig.json old mode 100644 new mode 100755 diff --git a/node_modules/http_ece/README.md b/node_modules/http_ece/README.md old mode 100644 new mode 100755 diff --git a/node_modules/http_ece/ece.js b/node_modules/http_ece/ece.js old mode 100644 new mode 100755 diff --git a/node_modules/http_ece/package.json b/node_modules/http_ece/package.json old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/LICENSE b/node_modules/https-proxy-agent/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/README.md b/node_modules/https-proxy-agent/README.md old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/index.d.ts b/node_modules/https-proxy-agent/dist/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/index.d.ts.map b/node_modules/https-proxy-agent/dist/index.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/index.js b/node_modules/https-proxy-agent/dist/index.js old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/index.js.map b/node_modules/https-proxy-agent/dist/index.js.map old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts b/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts.map b/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts.map old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/parse-proxy-response.js b/node_modules/https-proxy-agent/dist/parse-proxy-response.js old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map b/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map old mode 100644 new mode 100755 diff --git a/node_modules/https-proxy-agent/package.json b/node_modules/https-proxy-agent/package.json old mode 100644 new mode 100755 diff --git a/node_modules/minimalistic-assert/LICENSE b/node_modules/minimalistic-assert/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/minimalistic-assert/index.js b/node_modules/minimalistic-assert/index.js old mode 100644 new mode 100755 diff --git a/node_modules/minimalistic-assert/package.json b/node_modules/minimalistic-assert/package.json old mode 100644 new mode 100755 diff --git a/node_modules/minimalistic-assert/readme.md b/node_modules/minimalistic-assert/readme.md old mode 100644 new mode 100755 diff --git a/node_modules/object-assign/index.js b/node_modules/object-assign/index.js old mode 100644 new mode 100755 diff --git a/node_modules/object-assign/license b/node_modules/object-assign/license old mode 100644 new mode 100755 diff --git a/node_modules/object-assign/package.json b/node_modules/object-assign/package.json old mode 100644 new mode 100755 diff --git a/node_modules/object-assign/readme.md b/node_modules/object-assign/readme.md old mode 100644 new mode 100755 diff --git a/node_modules/proxy-from-env/.eslintrc b/node_modules/proxy-from-env/.eslintrc old mode 100644 new mode 100755 diff --git a/node_modules/proxy-from-env/.travis.yml b/node_modules/proxy-from-env/.travis.yml old mode 100644 new mode 100755 diff --git a/node_modules/proxy-from-env/LICENSE b/node_modules/proxy-from-env/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/proxy-from-env/README.md b/node_modules/proxy-from-env/README.md old mode 100644 new mode 100755 diff --git a/node_modules/proxy-from-env/index.js b/node_modules/proxy-from-env/index.js old mode 100644 new mode 100755 diff --git a/node_modules/proxy-from-env/package.json b/node_modules/proxy-from-env/package.json old mode 100644 new mode 100755 diff --git a/node_modules/proxy-from-env/test.js b/node_modules/proxy-from-env/test.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/LICENSE b/node_modules/socket.io-adapter/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/Readme.md b/node_modules/socket.io-adapter/Readme.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/cluster-adapter.d.ts b/node_modules/socket.io-adapter/dist/cluster-adapter.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/cluster-adapter.js b/node_modules/socket.io-adapter/dist/cluster-adapter.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/contrib/yeast.d.ts b/node_modules/socket.io-adapter/dist/contrib/yeast.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/contrib/yeast.js b/node_modules/socket.io-adapter/dist/contrib/yeast.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/in-memory-adapter.d.ts b/node_modules/socket.io-adapter/dist/in-memory-adapter.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/in-memory-adapter.js b/node_modules/socket.io-adapter/dist/in-memory-adapter.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/index.d.ts b/node_modules/socket.io-adapter/dist/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/dist/index.js b/node_modules/socket.io-adapter/dist/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/debug/LICENSE b/node_modules/socket.io-adapter/node_modules/debug/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/debug/README.md b/node_modules/socket.io-adapter/node_modules/debug/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/debug/package.json b/node_modules/socket.io-adapter/node_modules/debug/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/debug/src/browser.js b/node_modules/socket.io-adapter/node_modules/debug/src/browser.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/debug/src/common.js b/node_modules/socket.io-adapter/node_modules/debug/src/common.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/debug/src/index.js b/node_modules/socket.io-adapter/node_modules/debug/src/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/debug/src/node.js b/node_modules/socket.io-adapter/node_modules/debug/src/node.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/LICENSE b/node_modules/socket.io-adapter/node_modules/ws/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/README.md b/node_modules/socket.io-adapter/node_modules/ws/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/browser.js b/node_modules/socket.io-adapter/node_modules/ws/browser.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/index.js b/node_modules/socket.io-adapter/node_modules/ws/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/buffer-util.js b/node_modules/socket.io-adapter/node_modules/ws/lib/buffer-util.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/constants.js b/node_modules/socket.io-adapter/node_modules/ws/lib/constants.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/event-target.js b/node_modules/socket.io-adapter/node_modules/ws/lib/event-target.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/extension.js b/node_modules/socket.io-adapter/node_modules/ws/lib/extension.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/limiter.js b/node_modules/socket.io-adapter/node_modules/ws/lib/limiter.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/permessage-deflate.js b/node_modules/socket.io-adapter/node_modules/ws/lib/permessage-deflate.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/receiver.js b/node_modules/socket.io-adapter/node_modules/ws/lib/receiver.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/sender.js b/node_modules/socket.io-adapter/node_modules/ws/lib/sender.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/stream.js b/node_modules/socket.io-adapter/node_modules/ws/lib/stream.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/subprotocol.js b/node_modules/socket.io-adapter/node_modules/ws/lib/subprotocol.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/validation.js b/node_modules/socket.io-adapter/node_modules/ws/lib/validation.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/websocket-server.js b/node_modules/socket.io-adapter/node_modules/ws/lib/websocket-server.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/lib/websocket.js b/node_modules/socket.io-adapter/node_modules/ws/lib/websocket.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/package.json b/node_modules/socket.io-adapter/node_modules/ws/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/node_modules/ws/wrapper.mjs b/node_modules/socket.io-adapter/node_modules/ws/wrapper.mjs old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-adapter/package.json b/node_modules/socket.io-adapter/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/LICENSE b/node_modules/socket.io-parser/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/Readme.md b/node_modules/socket.io-parser/Readme.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/cjs/binary.d.ts b/node_modules/socket.io-parser/build/cjs/binary.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/cjs/binary.js b/node_modules/socket.io-parser/build/cjs/binary.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/cjs/index.d.ts b/node_modules/socket.io-parser/build/cjs/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/cjs/index.js b/node_modules/socket.io-parser/build/cjs/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/cjs/is-binary.d.ts b/node_modules/socket.io-parser/build/cjs/is-binary.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/cjs/is-binary.js b/node_modules/socket.io-parser/build/cjs/is-binary.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/cjs/package.json b/node_modules/socket.io-parser/build/cjs/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm-debug/binary.d.ts b/node_modules/socket.io-parser/build/esm-debug/binary.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm-debug/binary.js b/node_modules/socket.io-parser/build/esm-debug/binary.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm-debug/index.d.ts b/node_modules/socket.io-parser/build/esm-debug/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm-debug/index.js b/node_modules/socket.io-parser/build/esm-debug/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm-debug/is-binary.d.ts b/node_modules/socket.io-parser/build/esm-debug/is-binary.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm-debug/is-binary.js b/node_modules/socket.io-parser/build/esm-debug/is-binary.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm-debug/package.json b/node_modules/socket.io-parser/build/esm-debug/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm/binary.d.ts b/node_modules/socket.io-parser/build/esm/binary.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm/binary.js b/node_modules/socket.io-parser/build/esm/binary.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm/index.d.ts b/node_modules/socket.io-parser/build/esm/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm/index.js b/node_modules/socket.io-parser/build/esm/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm/is-binary.d.ts b/node_modules/socket.io-parser/build/esm/is-binary.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm/is-binary.js b/node_modules/socket.io-parser/build/esm/is-binary.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/build/esm/package.json b/node_modules/socket.io-parser/build/esm/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/node_modules/debug/LICENSE b/node_modules/socket.io-parser/node_modules/debug/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/node_modules/debug/README.md b/node_modules/socket.io-parser/node_modules/debug/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/node_modules/debug/package.json b/node_modules/socket.io-parser/node_modules/debug/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/node_modules/debug/src/browser.js b/node_modules/socket.io-parser/node_modules/debug/src/browser.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/node_modules/debug/src/common.js b/node_modules/socket.io-parser/node_modules/debug/src/common.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/node_modules/debug/src/index.js b/node_modules/socket.io-parser/node_modules/debug/src/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/node_modules/debug/src/node.js b/node_modules/socket.io-parser/node_modules/debug/src/node.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io-parser/package.json b/node_modules/socket.io-parser/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/LICENSE b/node_modules/socket.io/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/Readme.md b/node_modules/socket.io/Readme.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.esm.min.js b/node_modules/socket.io/client-dist/socket.io.esm.min.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.esm.min.js.map b/node_modules/socket.io/client-dist/socket.io.esm.min.js.map old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.js b/node_modules/socket.io/client-dist/socket.io.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.js.map b/node_modules/socket.io/client-dist/socket.io.js.map old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.min.js b/node_modules/socket.io/client-dist/socket.io.min.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.min.js.map b/node_modules/socket.io/client-dist/socket.io.min.js.map old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.msgpack.min.js b/node_modules/socket.io/client-dist/socket.io.msgpack.min.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/client-dist/socket.io.msgpack.min.js.map b/node_modules/socket.io/client-dist/socket.io.msgpack.min.js.map old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/broadcast-operator.d.ts b/node_modules/socket.io/dist/broadcast-operator.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/broadcast-operator.js b/node_modules/socket.io/dist/broadcast-operator.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/client.d.ts b/node_modules/socket.io/dist/client.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/client.js b/node_modules/socket.io/dist/client.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/index.d.ts b/node_modules/socket.io/dist/index.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/index.js b/node_modules/socket.io/dist/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/namespace.d.ts b/node_modules/socket.io/dist/namespace.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/namespace.js b/node_modules/socket.io/dist/namespace.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/parent-namespace.d.ts b/node_modules/socket.io/dist/parent-namespace.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/parent-namespace.js b/node_modules/socket.io/dist/parent-namespace.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/socket-types.d.ts b/node_modules/socket.io/dist/socket-types.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/socket-types.js b/node_modules/socket.io/dist/socket-types.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/socket.d.ts b/node_modules/socket.io/dist/socket.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/socket.js b/node_modules/socket.io/dist/socket.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/typed-events.d.ts b/node_modules/socket.io/dist/typed-events.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/typed-events.js b/node_modules/socket.io/dist/typed-events.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/uws.d.ts b/node_modules/socket.io/dist/uws.d.ts old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/dist/uws.js b/node_modules/socket.io/dist/uws.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/accepts/HISTORY.md b/node_modules/socket.io/node_modules/accepts/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/accepts/LICENSE b/node_modules/socket.io/node_modules/accepts/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/accepts/README.md b/node_modules/socket.io/node_modules/accepts/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/accepts/index.js b/node_modules/socket.io/node_modules/accepts/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/accepts/package.json b/node_modules/socket.io/node_modules/accepts/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/debug/LICENSE b/node_modules/socket.io/node_modules/debug/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/debug/README.md b/node_modules/socket.io/node_modules/debug/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/debug/package.json b/node_modules/socket.io/node_modules/debug/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/debug/src/browser.js b/node_modules/socket.io/node_modules/debug/src/browser.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/debug/src/common.js b/node_modules/socket.io/node_modules/debug/src/common.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/debug/src/index.js b/node_modules/socket.io/node_modules/debug/src/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/debug/src/node.js b/node_modules/socket.io/node_modules/debug/src/node.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-db/HISTORY.md b/node_modules/socket.io/node_modules/mime-db/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-db/LICENSE b/node_modules/socket.io/node_modules/mime-db/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-db/README.md b/node_modules/socket.io/node_modules/mime-db/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-db/db.json b/node_modules/socket.io/node_modules/mime-db/db.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-db/index.js b/node_modules/socket.io/node_modules/mime-db/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-db/package.json b/node_modules/socket.io/node_modules/mime-db/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-types/HISTORY.md b/node_modules/socket.io/node_modules/mime-types/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-types/LICENSE b/node_modules/socket.io/node_modules/mime-types/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-types/README.md b/node_modules/socket.io/node_modules/mime-types/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-types/index.js b/node_modules/socket.io/node_modules/mime-types/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/mime-types/package.json b/node_modules/socket.io/node_modules/mime-types/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/HISTORY.md b/node_modules/socket.io/node_modules/negotiator/HISTORY.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/LICENSE b/node_modules/socket.io/node_modules/negotiator/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/README.md b/node_modules/socket.io/node_modules/negotiator/README.md old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/index.js b/node_modules/socket.io/node_modules/negotiator/index.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/lib/charset.js b/node_modules/socket.io/node_modules/negotiator/lib/charset.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/lib/encoding.js b/node_modules/socket.io/node_modules/negotiator/lib/encoding.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/lib/language.js b/node_modules/socket.io/node_modules/negotiator/lib/language.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/lib/mediaType.js b/node_modules/socket.io/node_modules/negotiator/lib/mediaType.js old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/node_modules/negotiator/package.json b/node_modules/socket.io/node_modules/negotiator/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/package.json b/node_modules/socket.io/package.json old mode 100644 new mode 100755 diff --git a/node_modules/socket.io/wrapper.mjs b/node_modules/socket.io/wrapper.mjs old mode 100644 new mode 100755 diff --git a/node_modules/validator/LICENSE b/node_modules/validator/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/validator/README.md b/node_modules/validator/README.md old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/index.js b/node_modules/validator/es/index.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/alpha.js b/node_modules/validator/es/lib/alpha.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/blacklist.js b/node_modules/validator/es/lib/blacklist.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/contains.js b/node_modules/validator/es/lib/contains.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/equals.js b/node_modules/validator/es/lib/equals.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/escape.js b/node_modules/validator/es/lib/escape.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isAbaRouting.js b/node_modules/validator/es/lib/isAbaRouting.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isAfter.js b/node_modules/validator/es/lib/isAfter.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isAlpha.js b/node_modules/validator/es/lib/isAlpha.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isAlphanumeric.js b/node_modules/validator/es/lib/isAlphanumeric.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isAscii.js b/node_modules/validator/es/lib/isAscii.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isBIC.js b/node_modules/validator/es/lib/isBIC.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isBase32.js b/node_modules/validator/es/lib/isBase32.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isBase58.js b/node_modules/validator/es/lib/isBase58.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isBase64.js b/node_modules/validator/es/lib/isBase64.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isBefore.js b/node_modules/validator/es/lib/isBefore.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isBoolean.js b/node_modules/validator/es/lib/isBoolean.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isBtcAddress.js b/node_modules/validator/es/lib/isBtcAddress.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isByteLength.js b/node_modules/validator/es/lib/isByteLength.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isCreditCard.js b/node_modules/validator/es/lib/isCreditCard.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isDataURI.js b/node_modules/validator/es/lib/isDataURI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isDate.js b/node_modules/validator/es/lib/isDate.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isDecimal.js b/node_modules/validator/es/lib/isDecimal.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isDivisibleBy.js b/node_modules/validator/es/lib/isDivisibleBy.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isEAN.js b/node_modules/validator/es/lib/isEAN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isEmail.js b/node_modules/validator/es/lib/isEmail.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isEmpty.js b/node_modules/validator/es/lib/isEmpty.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isEthereumAddress.js b/node_modules/validator/es/lib/isEthereumAddress.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isFQDN.js b/node_modules/validator/es/lib/isFQDN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isFloat.js b/node_modules/validator/es/lib/isFloat.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isFullWidth.js b/node_modules/validator/es/lib/isFullWidth.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isHSL.js b/node_modules/validator/es/lib/isHSL.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isHalfWidth.js b/node_modules/validator/es/lib/isHalfWidth.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isHash.js b/node_modules/validator/es/lib/isHash.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isHexColor.js b/node_modules/validator/es/lib/isHexColor.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isHexadecimal.js b/node_modules/validator/es/lib/isHexadecimal.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isIBAN.js b/node_modules/validator/es/lib/isIBAN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isIMEI.js b/node_modules/validator/es/lib/isIMEI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isIP.js b/node_modules/validator/es/lib/isIP.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isIPRange.js b/node_modules/validator/es/lib/isIPRange.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISBN.js b/node_modules/validator/es/lib/isISBN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISIN.js b/node_modules/validator/es/lib/isISIN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISO31661Alpha2.js b/node_modules/validator/es/lib/isISO31661Alpha2.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISO31661Alpha3.js b/node_modules/validator/es/lib/isISO31661Alpha3.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISO4217.js b/node_modules/validator/es/lib/isISO4217.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISO6346.js b/node_modules/validator/es/lib/isISO6346.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISO6391.js b/node_modules/validator/es/lib/isISO6391.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISO8601.js b/node_modules/validator/es/lib/isISO8601.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISRC.js b/node_modules/validator/es/lib/isISRC.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isISSN.js b/node_modules/validator/es/lib/isISSN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isIdentityCard.js b/node_modules/validator/es/lib/isIdentityCard.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isIn.js b/node_modules/validator/es/lib/isIn.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isInt.js b/node_modules/validator/es/lib/isInt.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isJSON.js b/node_modules/validator/es/lib/isJSON.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isJWT.js b/node_modules/validator/es/lib/isJWT.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isLatLong.js b/node_modules/validator/es/lib/isLatLong.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isLength.js b/node_modules/validator/es/lib/isLength.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isLicensePlate.js b/node_modules/validator/es/lib/isLicensePlate.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isLocale.js b/node_modules/validator/es/lib/isLocale.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isLowercase.js b/node_modules/validator/es/lib/isLowercase.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isLuhnNumber.js b/node_modules/validator/es/lib/isLuhnNumber.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMACAddress.js b/node_modules/validator/es/lib/isMACAddress.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMD5.js b/node_modules/validator/es/lib/isMD5.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMagnetURI.js b/node_modules/validator/es/lib/isMagnetURI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMailtoURI.js b/node_modules/validator/es/lib/isMailtoURI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMimeType.js b/node_modules/validator/es/lib/isMimeType.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMobilePhone.js b/node_modules/validator/es/lib/isMobilePhone.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMongoId.js b/node_modules/validator/es/lib/isMongoId.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isMultibyte.js b/node_modules/validator/es/lib/isMultibyte.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isNumeric.js b/node_modules/validator/es/lib/isNumeric.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isOctal.js b/node_modules/validator/es/lib/isOctal.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isPassportNumber.js b/node_modules/validator/es/lib/isPassportNumber.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isPort.js b/node_modules/validator/es/lib/isPort.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isPostalCode.js b/node_modules/validator/es/lib/isPostalCode.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isRFC3339.js b/node_modules/validator/es/lib/isRFC3339.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isRgbColor.js b/node_modules/validator/es/lib/isRgbColor.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isSemVer.js b/node_modules/validator/es/lib/isSemVer.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isSlug.js b/node_modules/validator/es/lib/isSlug.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isStrongPassword.js b/node_modules/validator/es/lib/isStrongPassword.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isSurrogatePair.js b/node_modules/validator/es/lib/isSurrogatePair.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isTaxID.js b/node_modules/validator/es/lib/isTaxID.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isTime.js b/node_modules/validator/es/lib/isTime.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isURL.js b/node_modules/validator/es/lib/isURL.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isUUID.js b/node_modules/validator/es/lib/isUUID.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isUppercase.js b/node_modules/validator/es/lib/isUppercase.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isVAT.js b/node_modules/validator/es/lib/isVAT.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isVariableWidth.js b/node_modules/validator/es/lib/isVariableWidth.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/isWhitelisted.js b/node_modules/validator/es/lib/isWhitelisted.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/ltrim.js b/node_modules/validator/es/lib/ltrim.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/matches.js b/node_modules/validator/es/lib/matches.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/normalizeEmail.js b/node_modules/validator/es/lib/normalizeEmail.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/rtrim.js b/node_modules/validator/es/lib/rtrim.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/stripLow.js b/node_modules/validator/es/lib/stripLow.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/toBoolean.js b/node_modules/validator/es/lib/toBoolean.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/toDate.js b/node_modules/validator/es/lib/toDate.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/toFloat.js b/node_modules/validator/es/lib/toFloat.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/toInt.js b/node_modules/validator/es/lib/toInt.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/trim.js b/node_modules/validator/es/lib/trim.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/unescape.js b/node_modules/validator/es/lib/unescape.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/util/algorithms.js b/node_modules/validator/es/lib/util/algorithms.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/util/assertString.js b/node_modules/validator/es/lib/util/assertString.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/util/includes.js b/node_modules/validator/es/lib/util/includes.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/util/merge.js b/node_modules/validator/es/lib/util/merge.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/util/multilineRegex.js b/node_modules/validator/es/lib/util/multilineRegex.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/util/toString.js b/node_modules/validator/es/lib/util/toString.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/util/typeOf.js b/node_modules/validator/es/lib/util/typeOf.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/es/lib/whitelist.js b/node_modules/validator/es/lib/whitelist.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/index.js b/node_modules/validator/index.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/alpha.js b/node_modules/validator/lib/alpha.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/blacklist.js b/node_modules/validator/lib/blacklist.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/contains.js b/node_modules/validator/lib/contains.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/equals.js b/node_modules/validator/lib/equals.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/escape.js b/node_modules/validator/lib/escape.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isAbaRouting.js b/node_modules/validator/lib/isAbaRouting.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isAfter.js b/node_modules/validator/lib/isAfter.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isAlpha.js b/node_modules/validator/lib/isAlpha.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isAlphanumeric.js b/node_modules/validator/lib/isAlphanumeric.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isAscii.js b/node_modules/validator/lib/isAscii.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isBIC.js b/node_modules/validator/lib/isBIC.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isBase32.js b/node_modules/validator/lib/isBase32.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isBase58.js b/node_modules/validator/lib/isBase58.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isBase64.js b/node_modules/validator/lib/isBase64.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isBefore.js b/node_modules/validator/lib/isBefore.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isBoolean.js b/node_modules/validator/lib/isBoolean.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isBtcAddress.js b/node_modules/validator/lib/isBtcAddress.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isByteLength.js b/node_modules/validator/lib/isByteLength.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isCreditCard.js b/node_modules/validator/lib/isCreditCard.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isDataURI.js b/node_modules/validator/lib/isDataURI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isDate.js b/node_modules/validator/lib/isDate.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isDecimal.js b/node_modules/validator/lib/isDecimal.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isDivisibleBy.js b/node_modules/validator/lib/isDivisibleBy.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isEAN.js b/node_modules/validator/lib/isEAN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isEmail.js b/node_modules/validator/lib/isEmail.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isEmpty.js b/node_modules/validator/lib/isEmpty.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isEthereumAddress.js b/node_modules/validator/lib/isEthereumAddress.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isFQDN.js b/node_modules/validator/lib/isFQDN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isFloat.js b/node_modules/validator/lib/isFloat.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isFullWidth.js b/node_modules/validator/lib/isFullWidth.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isHSL.js b/node_modules/validator/lib/isHSL.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isHalfWidth.js b/node_modules/validator/lib/isHalfWidth.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isHash.js b/node_modules/validator/lib/isHash.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isHexColor.js b/node_modules/validator/lib/isHexColor.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isHexadecimal.js b/node_modules/validator/lib/isHexadecimal.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isIBAN.js b/node_modules/validator/lib/isIBAN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isIMEI.js b/node_modules/validator/lib/isIMEI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isIP.js b/node_modules/validator/lib/isIP.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isIPRange.js b/node_modules/validator/lib/isIPRange.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISBN.js b/node_modules/validator/lib/isISBN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISIN.js b/node_modules/validator/lib/isISIN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISO31661Alpha2.js b/node_modules/validator/lib/isISO31661Alpha2.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISO31661Alpha3.js b/node_modules/validator/lib/isISO31661Alpha3.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISO4217.js b/node_modules/validator/lib/isISO4217.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISO6346.js b/node_modules/validator/lib/isISO6346.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISO6391.js b/node_modules/validator/lib/isISO6391.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISO8601.js b/node_modules/validator/lib/isISO8601.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISRC.js b/node_modules/validator/lib/isISRC.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isISSN.js b/node_modules/validator/lib/isISSN.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isIdentityCard.js b/node_modules/validator/lib/isIdentityCard.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isIn.js b/node_modules/validator/lib/isIn.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isInt.js b/node_modules/validator/lib/isInt.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isJSON.js b/node_modules/validator/lib/isJSON.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isJWT.js b/node_modules/validator/lib/isJWT.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isLatLong.js b/node_modules/validator/lib/isLatLong.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isLength.js b/node_modules/validator/lib/isLength.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isLicensePlate.js b/node_modules/validator/lib/isLicensePlate.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isLocale.js b/node_modules/validator/lib/isLocale.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isLowercase.js b/node_modules/validator/lib/isLowercase.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isLuhnNumber.js b/node_modules/validator/lib/isLuhnNumber.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMACAddress.js b/node_modules/validator/lib/isMACAddress.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMD5.js b/node_modules/validator/lib/isMD5.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMagnetURI.js b/node_modules/validator/lib/isMagnetURI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMailtoURI.js b/node_modules/validator/lib/isMailtoURI.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMimeType.js b/node_modules/validator/lib/isMimeType.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMobilePhone.js b/node_modules/validator/lib/isMobilePhone.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMongoId.js b/node_modules/validator/lib/isMongoId.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isMultibyte.js b/node_modules/validator/lib/isMultibyte.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isNumeric.js b/node_modules/validator/lib/isNumeric.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isOctal.js b/node_modules/validator/lib/isOctal.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isPassportNumber.js b/node_modules/validator/lib/isPassportNumber.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isPort.js b/node_modules/validator/lib/isPort.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isPostalCode.js b/node_modules/validator/lib/isPostalCode.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isRFC3339.js b/node_modules/validator/lib/isRFC3339.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isRgbColor.js b/node_modules/validator/lib/isRgbColor.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isSemVer.js b/node_modules/validator/lib/isSemVer.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isSlug.js b/node_modules/validator/lib/isSlug.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isStrongPassword.js b/node_modules/validator/lib/isStrongPassword.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isSurrogatePair.js b/node_modules/validator/lib/isSurrogatePair.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isTaxID.js b/node_modules/validator/lib/isTaxID.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isTime.js b/node_modules/validator/lib/isTime.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isURL.js b/node_modules/validator/lib/isURL.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isUUID.js b/node_modules/validator/lib/isUUID.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isUppercase.js b/node_modules/validator/lib/isUppercase.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isVAT.js b/node_modules/validator/lib/isVAT.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isVariableWidth.js b/node_modules/validator/lib/isVariableWidth.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/isWhitelisted.js b/node_modules/validator/lib/isWhitelisted.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/ltrim.js b/node_modules/validator/lib/ltrim.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/matches.js b/node_modules/validator/lib/matches.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/normalizeEmail.js b/node_modules/validator/lib/normalizeEmail.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/rtrim.js b/node_modules/validator/lib/rtrim.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/stripLow.js b/node_modules/validator/lib/stripLow.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/toBoolean.js b/node_modules/validator/lib/toBoolean.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/toDate.js b/node_modules/validator/lib/toDate.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/toFloat.js b/node_modules/validator/lib/toFloat.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/toInt.js b/node_modules/validator/lib/toInt.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/trim.js b/node_modules/validator/lib/trim.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/unescape.js b/node_modules/validator/lib/unescape.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/util/algorithms.js b/node_modules/validator/lib/util/algorithms.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/util/assertString.js b/node_modules/validator/lib/util/assertString.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/util/includes.js b/node_modules/validator/lib/util/includes.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/util/merge.js b/node_modules/validator/lib/util/merge.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/util/multilineRegex.js b/node_modules/validator/lib/util/multilineRegex.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/util/toString.js b/node_modules/validator/lib/util/toString.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/util/typeOf.js b/node_modules/validator/lib/util/typeOf.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/lib/whitelist.js b/node_modules/validator/lib/whitelist.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/package.json b/node_modules/validator/package.json old mode 100644 new mode 100755 diff --git a/node_modules/validator/validator.js b/node_modules/validator/validator.js old mode 100644 new mode 100755 diff --git a/node_modules/validator/validator.min.js b/node_modules/validator/validator.min.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/LICENSE b/node_modules/web-push/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/web-push/README.md b/node_modules/web-push/README.md old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jwa/LICENSE b/node_modules/web-push/node_modules/jwa/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jwa/README.md b/node_modules/web-push/node_modules/jwa/README.md old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jwa/index.js b/node_modules/web-push/node_modules/jwa/index.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jwa/opslevel.yml b/node_modules/web-push/node_modules/jwa/opslevel.yml old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jwa/package.json b/node_modules/web-push/node_modules/jwa/package.json old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/CHANGELOG.md b/node_modules/web-push/node_modules/jws/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/LICENSE b/node_modules/web-push/node_modules/jws/LICENSE old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/index.js b/node_modules/web-push/node_modules/jws/index.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/lib/data-stream.js b/node_modules/web-push/node_modules/jws/lib/data-stream.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/lib/sign-stream.js b/node_modules/web-push/node_modules/jws/lib/sign-stream.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/lib/tostring.js b/node_modules/web-push/node_modules/jws/lib/tostring.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/lib/verify-stream.js b/node_modules/web-push/node_modules/jws/lib/verify-stream.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/package.json b/node_modules/web-push/node_modules/jws/package.json old mode 100644 new mode 100755 diff --git a/node_modules/web-push/node_modules/jws/readme.md b/node_modules/web-push/node_modules/jws/readme.md old mode 100644 new mode 100755 diff --git a/node_modules/web-push/package.json b/node_modules/web-push/package.json old mode 100644 new mode 100755 diff --git a/node_modules/web-push/src/encryption-helper.js b/node_modules/web-push/src/encryption-helper.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/src/index.js b/node_modules/web-push/src/index.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/src/urlsafe-base64-helper.js b/node_modules/web-push/src/urlsafe-base64-helper.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/src/vapid-helper.js b/node_modules/web-push/src/vapid-helper.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/src/web-push-constants.js b/node_modules/web-push/src/web-push-constants.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/src/web-push-error.js b/node_modules/web-push/src/web-push-error.js old mode 100644 new mode 100755 diff --git a/node_modules/web-push/src/web-push-lib.js b/node_modules/web-push/src/web-push-lib.js old mode 100644 new mode 100755 diff --git a/package.json b/package.json index e93b3fd..20ef5b5 100755 --- a/package.json +++ b/package.json @@ -1,9 +1,12 @@ { "name": "evse-backend", "version": "1.0.0", - "main": "index.js", + "main": "src/server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node src/server.js", + "dev": "nodemon src/server.js", + "migrate": "knex migrate:latest", + "rollback": "knex migrate:rollback" }, "keywords": [], "author": "", diff --git a/projeto_parte1.c b/projeto_parte1.c old mode 100644 new mode 100755 index ba414cd..8ffc16d --- a/projeto_parte1.c +++ b/projeto_parte1.c @@ -1,24 +1,210 @@ -// === Início de: ./utils/pushService.js === -// utils/pushService.js +// === Início de: ./src/server.js === +// src/server.js +const http = require('http'); +const { Server } = require('socket.io'); +const jwt = require('jsonwebtoken'); + +const app = require('./app'); +const db = require('./db/knex'); +const config = require('./config'); + +// ✅ mqtt exports { on, ... } +const mqttClient = require('./mqtt'); + +const { normalizeChargingStatus } = require('./domain/normalize/chargingStatus'); +const { normalizeChargingConfig } = require('./domain/normalize/chargingConfig'); + +const server = http.createServer(app); + +const io = new Server(server, { + cors: { + origin: config.corsOrigins, + methods: ['GET', 'POST'], + credentials: true, + }, +}); + +console.log('MQTT client exports=', Object.keys(mqttClient || {})); +if (typeof mqttClient?.on !== 'function') { + console.error('[server] mqttClient.on não existe. Verifica src/mqtt/index.js'); +} + +// --------------------------- +// auth middleware do socket +// --------------------------- +io.use((socket, next) => { + const token = socket.handshake.auth?.token; + if (!token) return next(new Error('Authentication error: token required')); + + try { + const payload = jwt.verify(token, config.jwtSecret); + socket.user = payload; + next(); + } catch (err) { + next(new Error('Authentication error')); + } +}); + +io.on('connection', (socket) => { + console.log(`Client connected: ${socket.id}, user: ${socket.user.username}`); + + socket.on('joinChargers', async (chargerIds = []) => { + try { + if (!Array.isArray(chargerIds) || chargerIds.length === 0) return; + + const rows = await db('chargers') + .whereIn('id', chargerIds) + .andWhere({ user_id: socket.user.id }) + .select('id'); + + const allowed = rows.map((r) => r.id); + allowed.forEach((id) => socket.join(id)); + + console.log(`Socket ${socket.id} joined chargers: ${allowed}`); + } catch (err) { + console.error('joinChargers error:', err); + } + }); + + socket.on('disconnect', (reason) => { + console.log(`Client disconnected: ${socket.id}, reason: ${reason}`); + }); +}); + +// --------------------------- +// Relay MQTT -> Socket.IO +// --------------------------- +if (typeof mqttClient?.on === 'function') { + mqttClient.on('charging-status', (data) => { + const normalized = normalizeChargingStatus(data); + const chargerId = normalized.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('charging-status', normalized); + }); + + mqttClient.on('charging-config', (data) => { + const normalized = normalizeChargingConfig(data); + const chargerId = normalized.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('charging-config', normalized); + }); + + mqttClient.on('scheduler-state', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('evse-scheduler', evt); + io.to(chargerId).emit('scheduler-state', evt); + }); + + mqttClient.on('loadbalancing-state', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('evse-loadbalancing', evt); + io.to(chargerId).emit('loadbalancing-state', evt); + }); + + mqttClient.on('meter-live', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + io.to(chargerId).emit('meter-live', evt); + }); + + mqttClient.on('auth-state', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + io.to(chargerId).emit('auth-state', evt); + }); + + mqttClient.on('meters-config', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + io.to(chargerId).emit('meters-config', evt); + }); +} + +server.listen(config.port, () => { + console.log(`Server listening on http://localhost:${config.port}`); +}); + +// === Fim de: ./src/server.js === + + +// === Início de: ./src/app.js === +// src/app.js +const express = require('express'); + +const config = require('./config'); + +const usersRouter = require('./routes/users.routes'); +const chargersRouter = require('./routes/chargers.routes'); +const sessionsRouter = require('./routes/sessions.routes'); +const pushRouter = require('./routes/push.routes'); + +const errorHandler = require('./middleware/errorHandler'); + +const app = express(); + +// body parser +app.use(express.json({ limit: '1mb' })); + +// CORS simples sem dependência extra +app.use((req, res, next) => { + const origin = req.headers.origin; + if (origin && config.corsOrigins.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + } + + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); + + if (req.method === 'OPTIONS') return res.sendStatus(204); + next(); +}); + +// health +app.get('/health', (req, res) => res.json({ ok: true })); + +// routes +app.use('/api/users', usersRouter); +app.use('/api/chargers', chargersRouter); +app.use('/api/charger_sessions', sessionsRouter); +app.use('/api/push', pushRouter); + +// 404 +app.use((req, res) => { + res.status(404).json({ success: false, message: 'Not found' }); +}); + +// error handler +app.use(errorHandler); + +module.exports = app; + +// === Fim de: ./src/app.js === + + +// === Início de: ./src/services/push.service.js === +// src/services/push.service.js const webpush = require('web-push'); -const db = require('../db'); +const config = require('../config'); +const pushRepo = require('../repositories/push.repo'); -const hasVapid = - !!process.env.VAPID_PUBLIC_KEY && !!process.env.VAPID_PRIVATE_KEY; +const hasVapid = !!config.vapid.publicKey && !!config.vapid.privateKey; if (!hasVapid) { console.warn('[Push] VAPID keys não definidas. Push desativado.'); } else { - webpush.setVapidDetails( - process.env.VAPID_SUBJECT || 'mailto:admin@evstation.local', - process.env.VAPID_PUBLIC_KEY, - process.env.VAPID_PRIVATE_KEY - ); + webpush.setVapidDetails(config.vapid.subject, config.vapid.publicKey, config.vapid.privateKey); } -// retry simples p/ 429 e 5xx async function sendWithRetry(subscription, message, tries = 2) { try { return await webpush.sendNotification(subscription, message); @@ -35,10 +221,7 @@ async function sendWithRetry(subscription, message, tries = 2) { async function sendPushToUser(userId, payload) { if (!hasVapid) return; - const subs = await db('push_subscriptions') - .where({ user_id: userId }) - .select('id', 'endpoint', 'p256dh', 'auth'); - + const subs = await pushRepo.listByUser(userId); if (!subs.length) return; const message = JSON.stringify(payload); @@ -47,26 +230,15 @@ async function sendPushToUser(userId, payload) { subs.map(async (s) => { const subscription = { endpoint: s.endpoint, - keys: { - p256dh: s.p256dh, - auth: s.auth, - }, + keys: { p256dh: s.p256dh, auth: s.auth }, }; try { await sendWithRetry(subscription, message); - - // TTL opcional (default 15min) - // se quiseres usar TTL do payload, descomenta e passa options no sendWithRetry - // await webpush.sendNotification(subscription, message, { - // TTL: payload.ttl ?? 60 * 15, - // }); } catch (err) { const code = err?.statusCode; - - // remove subscriptions mortas if (code === 404 || code === 410) { - await db('push_subscriptions').where({ id: s.id }).del(); + await pushRepo.deleteById(s.id); } else { console.error('[Push] erro ao enviar:', err.message); } @@ -77,18 +249,558 @@ async function sendPushToUser(userId, payload) { module.exports = { sendPushToUser }; -// === Fim de: ./utils/pushService.js === +// === Fim de: ./src/services/push.service.js === -// === Início de: ./routes/charger_sessions.js === -// routes/charger_sessions.js -const express = require('express'); -const { param, query, body, validationResult } = require('express-validator'); -const router = express.Router(); -const verifyToken = require('../middleware/verifyToken'); -const db = require('../db'); +// === Início de: ./src/services/sessions.service.js === +// src/services/sessions.service.js +const chargersRepo = require('../repositories/chargers.repo'); +const sessionsRepo = require('../repositories/sessions.repo'); +const { httpError } = require('../utils/httpError'); -router.use(verifyToken); +function stripUndef(obj) { + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); +} + +async function listByCharger(userId, chargerId) { + const charger = await chargersRepo.findByIdForUser(chargerId, userId); + if (!charger) throw httpError(403, 'Acesso não autorizado'); + + return sessionsRepo.listByCharger(chargerId); +} + +async function history(userId, chargerId, viewMode) { + const charger = await chargersRepo.findByIdForUser(chargerId, userId); + if (!charger) throw httpError(403, 'Acesso não autorizado'); + + const rows = await sessionsRepo.historyAgg(chargerId, viewMode); + if (!rows.length) return []; + + return rows.map((r) => ({ + started_at: r.period, + kwh: parseFloat(r.total_kwh) || 0, + })); +} + +async function getById(userId, sessionId) { + const session = await sessionsRepo.findByIdForUser(sessionId, userId); + if (!session) throw httpError(404, 'Sessão não encontrada'); + return session; +} + +async function create(userId, charger_id) { + const charger = await chargersRepo.findByIdForUser(charger_id, userId); + if (!charger) throw httpError(403, 'Acesso não autorizado'); + + return sessionsRepo.insertSession({ charger_id, started_at: new Date() }); +} + +async function update(userId, sessionId, patch) { + const session = await sessionsRepo.findByIdForUser(sessionId, userId); + if (!session) throw httpError(404, 'Sessão não encontrada'); + + const clean = stripUndef(patch); + return sessionsRepo.updateById(sessionId, clean); +} + +async function remove(userId, sessionId) { + const deleted = await sessionsRepo.deleteByIdForUser(sessionId, userId); + if (!deleted) throw httpError(404, 'Sessão não encontrada'); + return true; +} + +module.exports = { listByCharger, history, getById, create, update, remove }; + +// === Fim de: ./src/services/sessions.service.js === + + +// === Início de: ./src/services/pushHttp.service.js === +// src/services/pushHttp.service.js +const pushRepo = require('../repositories/push.repo'); +const { httpError } = require('../utils/httpError'); + +async function subscribe(userId, endpoint, keys, userAgent) { + if (!endpoint || !keys?.p256dh || !keys?.auth) { + throw httpError(400, 'Subscription inválida'); + } + + // dedupe (mesmo user) + const existing = await pushRepo.findByUserAndEndpoint(userId, endpoint); + if (existing) return { row: existing, created: false }; + + // como endpoint é UNIQUE na tabela, evita conflito com outro user + const usedByOther = await pushRepo.findByEndpoint(endpoint); + if (usedByOther && usedByOther.user_id !== userId) { + throw httpError(409, 'Este endpoint já está associado a outro utilizador'); + } + + const inserted = await pushRepo.insertSubscription({ + user_id: userId, + endpoint, + p256dh: keys.p256dh, + auth: keys.auth, + user_agent: userAgent || null, + created_at: new Date().toISOString(), + }); + + return { row: inserted, created: true }; +} + +async function unsubscribe(userId, endpoint) { + if (!endpoint) return { ok: true, message: 'No subscription' }; + await pushRepo.deleteByUserAndEndpoint(userId, endpoint); + return { ok: true, message: 'Unsubscribed' }; +} + +module.exports = { subscribe, unsubscribe }; + +// === Fim de: ./src/services/pushHttp.service.js === + + +// === Início de: ./src/services/configs.service.js === + +// === Fim de: ./src/services/configs.service.js === + + +// === Início de: ./src/services/chargers.service.js === +// src/services/chargers.service.js +const crypto = require('crypto'); +const axios = require('axios'); + +const chargersRepo = require('../repositories/chargers.repo'); +const { httpError } = require('../utils/httpError'); +const mqttClient = require('../mqtt'); + +function stripUndef(obj) { + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); +} + +// throttle in-memory (por charger) +const lastConfigUpdateAt = new Map(); + +function clampAmp(v) { + const n = Number(v); + return Math.max(6, Math.min(n, 64)); +} + +function clampTemp(v) { + const n = Number(v); + return Math.max(0, Math.min(n, 120)); +} + +function normalizeNumericFields(charger) { + const numericFields = [ + 'power_l1', + 'power_l2', + 'power_l3', + 'voltage_l1', + 'voltage_l2', + 'voltage_l3', + 'current_l1', + 'current_l2', + 'current_l3', + 'charging_current', + 'consumption', + ]; + + numericFields.forEach((field) => { + const v = charger[field]; + charger[field] = v === null || v === undefined || v === '' ? 0 : Number(v); + if (Number.isNaN(charger[field])) charger[field] = 0; + }); + + return charger; +} + +async function list(userId) { + return chargersRepo.listByUser(userId); +} + +async function getOne(userId, id) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + + let config = await chargersRepo.getConfig(charger.id); + if (!config) { + config = { + charger_id: charger.id, + max_charging_current: 32, + require_auth: false, + rcm_enabled: false, + temperature_limit: 60, + }; + } + + return { ...normalizeNumericFields(charger), config }; +} + +async function create(userId, location) { + if (!location || typeof location !== 'string' || location.trim().length < 1) { + throw httpError(400, 'O campo location é obrigatório'); + } + + const now = new Date().toISOString(); + + let chargerID; + do { + chargerID = crypto.randomBytes(6).toString('hex'); + } while (await chargersRepo.findByMqttTopic(chargerID)); + + const mqtt_topic = chargerID; + const mqtt_user = chargerID; + const mqtt_pass = crypto.randomBytes(6).toString('hex'); + + const charger = await chargersRepo.insertCharger({ + user_id: userId, + location: location.trim(), + status: 'offline', + charging_current: 0, + charging_time: 0, + consumption: 0, + power_l1: 0.0, + power_l2: 0.0, + power_l3: 0.0, + voltage_l1: 0.0, + voltage_l2: 0.0, + voltage_l3: 0.0, + current_l1: 0.0, + current_l2: 0.0, + current_l3: 0.0, + mqtt_user, + mqtt_pass, + mqtt_topic, + updated_at: now, + }); + + await chargersRepo.insertConfig({ + charger_id: charger.id, + max_charging_current: 32, + require_auth: false, + rcm_enabled: false, + temperature_limit: 60, + config_received_at: now, + }); + + // mosquitto mgmt (mantém como tinhas) + try { + await axios.post( + 'http://localhost:7000/client/create', + { + client_name: charger.mqtt_user, + chargeID: charger.mqtt_topic, + password: charger.mqtt_pass, + }, + { timeout: 5000 } + ); + } catch (err) { + console.error('Erro ao criar cliente Mosquitto:', err?.response?.data || err.message); + } + + return charger; +} + +async function update(userId, id, payload = {}) { + let { charger = {}, config = {} } = payload; + + // compat: { location } no root + if (payload.location && !charger.location) charger.location = payload.location; + + // só permitimos location por agora + const safeChargerUpdate = {}; + if (charger.location !== undefined) safeChargerUpdate.location = charger.location; + + let updatedCharger = null; + + if (Object.keys(safeChargerUpdate).length > 0) { + updatedCharger = await chargersRepo.updateChargerForUser(id, userId, { + ...safeChargerUpdate, + updated_at: new Date().toISOString(), + }); + } else { + updatedCharger = await chargersRepo.findByIdForUser(id, userId); + } + + if (!updatedCharger) throw httpError(404, 'Carregador não encontrado'); + + // ✅ config patch com whitelist (para evitar lixo no DB) + if (config && Object.keys(config).length > 0) { + const ALLOWED = ['max_charging_current', 'require_auth', 'rcm_enabled', 'temperature_limit']; + let safeConfig = Object.fromEntries( + Object.entries(config || {}).filter(([k]) => ALLOWED.includes(k)) + ); + + // clamp leve (evita valores inválidos no DB) + if (safeConfig.max_charging_current !== undefined) { + safeConfig.max_charging_current = clampAmp(safeConfig.max_charging_current); + } + if (safeConfig.temperature_limit !== undefined) { + safeConfig.temperature_limit = clampTemp(safeConfig.temperature_limit); + } + + await chargersRepo.upsertConfig(id, { + ...stripUndef(safeConfig), + config_received_at: new Date().toISOString(), + }); + } + + return updatedCharger; +} + +async function remove(userId, id) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + + try { + await axios.post( + 'http://localhost:7000/client/delete', + { + client_name: charger.mqtt_user, + chargeID: charger.mqtt_topic, + }, + { timeout: 5000 } + ); + } catch (err) { + console.error('Erro ao deletar cliente Mosquitto:', err?.response?.data || err.message); + } + + await chargersRepo.deleteChargerForUser(id, userId); + return true; +} + +async function updateConfig(userId, id, incomingConfig = {}) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Charger not found or unauthorized'); + + const existing = await chargersRepo.getConfig(id); + + const nowMs = Date.now(); + const lastMs = lastConfigUpdateAt.get(id) || 0; + const tooSoon = nowMs - lastMs < 800; + + const ALLOWED = ['max_charging_current', 'require_auth', 'rcm_enabled', 'temperature_limit']; + let safeConfig = Object.fromEntries( + Object.entries(incomingConfig || {}).filter(([k]) => ALLOWED.includes(k)) + ); + + if (safeConfig.max_charging_current !== undefined) { + safeConfig.max_charging_current = clampAmp(safeConfig.max_charging_current); + } + if (safeConfig.temperature_limit !== undefined) { + safeConfig.temperature_limit = clampTemp(safeConfig.temperature_limit); + } + + const onlyAmp = + Object.keys(safeConfig).length === 1 && safeConfig.max_charging_current !== undefined; + + if ( + existing && + onlyAmp && + Number(existing.max_charging_current) === Number(safeConfig.max_charging_current) + ) { + return { data: existing, message: 'Config unchanged' }; + } + + if (tooSoon && existing && onlyAmp) { + return { data: existing, message: 'Throttled' }; + } + + const updated = await chargersRepo.upsertConfig(id, { + ...safeConfig, + config_received_at: new Date().toISOString(), + }); + + lastConfigUpdateAt.set(id, nowMs); + + // publica p/ firmware novo: cmd/evse/settings + const evseSettings = {}; + if (safeConfig.max_charging_current !== undefined) { + evseSettings.currentLimit = Number(safeConfig.max_charging_current); + } + if (safeConfig.temperature_limit !== undefined) { + evseSettings.temperatureLimit = Number(safeConfig.temperature_limit); + } + if (Object.keys(evseSettings).length > 0) { + mqttClient.sendEvseSettings(charger.mqtt_topic, evseSettings); + } + + return { data: updated }; +} + +async function getSchedules(userId, id) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + return chargersRepo.listSchedules(id); +} + +async function createSchedule(userId, id, start, end, repeat) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + + const row = await chargersRepo.insertSchedule({ + charger_id: id, + start, + end, + repeat, + created_at: new Date().toISOString(), + }); + + return row; +} + +async function action(userId, id, actionName, ampLimit) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado ou não autorizado'); + + if (ampLimit !== undefined) { + const safeAmp = clampAmp(ampLimit); + + await chargersRepo.upsertConfig(id, { + max_charging_current: safeAmp, + config_received_at: new Date().toISOString(), + }); + + mqttClient.sendEvseSettings(charger.mqtt_topic, { currentLimit: safeAmp }); + } + + const enable = actionName === 'start'; + mqttClient.sendEnable(charger.mqtt_topic, enable); + + return true; +} + +module.exports = { + list, + getOne, + create, + update, + remove, + updateConfig, + getSchedules, + createSchedule, + action, +}; + +// === Fim de: ./src/services/chargers.service.js === + + +// === Início de: ./src/services/users.service.js === +// src/services/users.service.js +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const config = require('../config'); +const usersRepo = require('../repositories/users.repo'); +const { httpError } = require('../utils/httpError'); + +async function login(username, password) { + if (!username || !password) { + throw httpError(400, 'Usuário e senha são obrigatórios'); + } + + const user = await usersRepo.findByUsername(username); + if (!user) throw httpError(401, 'Credenciais inválidas'); + + const ok = await bcrypt.compare(password, user.password); + if (!ok) throw httpError(401, 'Credenciais inválidas'); + + const token = jwt.sign({ id: user.id, username: user.username }, config.jwtSecret, { + expiresIn: '24h', + }); + + return { token }; +} + +async function register(username, password) { + if ( + !username || + !password || + typeof username !== 'string' || + typeof password !== 'string' || + username.length < 3 || + password.length < 4 + ) { + throw httpError( + 400, + 'Nome de usuário deve ter pelo menos 3 caracteres e senha pelo menos 4 caracteres' + ); + } + + const existing = await usersRepo.findByUsername(username); + if (existing) throw httpError(409, 'Nome de usuário já está em uso'); + + const hashed = await bcrypt.hash(password, 10); + const id = await usersRepo.insertUser({ username, passwordHash: hashed }); + + const token = jwt.sign({ id, username }, config.jwtSecret, { expiresIn: '24h' }); + return { token }; +} + +module.exports = { login, register }; + +// === Fim de: ./src/services/users.service.js === + + +// === Início de: ./src/utils/httpError.js === +// src/utils/httpError.js +class HttpError extends Error { + constructor(statusCode, message) { + super(message); + this.statusCode = statusCode; + } +} + +function httpError(statusCode, message) { + return new HttpError(statusCode, message); +} + +module.exports = { HttpError, httpError }; + +// === Fim de: ./src/utils/httpError.js === + + +// === Início de: ./src/middleware/auth.js === +// src/middleware/auth.js +const jwt = require('jsonwebtoken'); +const config = require('../config'); + +function verifyToken(req, res, next) { + const authHeader = req.headers['authorization'] || req.headers['Authorization']; + + if (!authHeader) { + return res.status(401).json({ error: 'Token não fornecido' }); + } + + const match = authHeader.match(/^Bearer\s+(.+)$/i); + if (!match) { + return res.status(401).json({ error: 'Token malformado. Use "Bearer "' }); + } + + const token = match[1]; + + jwt.verify(token, config.jwtSecret, (err, payload) => { + if (err) { + if (err.name === 'TokenExpiredError') { + return res.status(401).json({ error: 'Sessão expirada' }); + } + return res.status(401).json({ error: 'Token inválido' }); + } + + if (!payload?.id) { + return res.status(401).json({ error: 'Token inválido' }); + } + + req.user = payload; + next(); + }); +} + +module.exports = verifyToken; + +// === Fim de: ./src/middleware/auth.js === + + +// === Início de: ./src/middleware/validate.js === +// src/middleware/validate.js +const { validationResult } = require('express-validator'); function handleValidation(req, res, next) { const errors = validationResult(req); @@ -98,257 +810,616 @@ function handleValidation(req, res, next) { next(); } +module.exports = handleValidation; + +// === Fim de: ./src/middleware/validate.js === + + +// === Início de: ./src/middleware/errorHandler.js === +// src/middleware/errorHandler.js +function errorHandler(err, req, res, next) { + console.error('[errorHandler]', err); + + if (res.headersSent) return next(err); + + const status = err.statusCode || err.status || 500; + const message = err.message || 'Erro interno do servidor'; + + res.status(status).json({ success: false, message }); + } + + module.exports = errorHandler; + +// === Fim de: ./src/middleware/errorHandler.js === + + +// === Início de: ./src/db/knex.js === +// src/db/knex.js +const knex = require('knex'); +const path = require('path'); + +const knexfile = require(path.join(__dirname, '../../knexfile.js')); +const env = process.env.NODE_ENV || 'development'; + +const db = knex(knexfile[env] || knexfile); + +module.exports = db; + +// === Fim de: ./src/db/knex.js === + + +// === Início de: ./src/db/migrations/20251123_create_charger_schedules.js === +exports.up = async function (knex) { + const exists = await knex.schema.hasTable('charger_schedules'); + if (exists) return; + + return knex.schema.createTable('charger_schedules', (t) => { + t.uuid('id') + .primary() + .defaultTo(knex.raw('gen_random_uuid()')); + + t.uuid('charger_id') + .notNullable() + .references('id') + .inTable('chargers') + .onDelete('CASCADE'); + + t.string('start', 5).notNullable(); + t.string('end', 5).notNullable(); + + t.enu('repeat', ['everyday', 'weekdays', 'weekends']) + .notNullable() + .defaultTo('everyday'); + + t.timestamp('created_at').defaultTo(knex.fn.now()); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTableIfExists('charger_schedules'); +}; + +// === Fim de: ./src/db/migrations/20251123_create_charger_schedules.js === + + +// === Início de: ./src/db/migrations/20251123_create_push_subscriptions.js === +exports.up = function (knex) { + return knex.schema.createTable('push_subscriptions', (t) => { + t.uuid('id') + .primary() + .defaultTo(knex.raw('gen_random_uuid()')); + + // ✅ users.id é integer no teu caso + t.integer('user_id') + .notNullable() + .references('id') + .inTable('users') + .onDelete('CASCADE'); + + t.text('endpoint').notNullable().unique(); + t.text('p256dh').notNullable(); + t.text('auth').notNullable(); + t.text('user_agent'); + t.timestamp('created_at').defaultTo(knex.fn.now()); + }); + }; + + exports.down = function (knex) { + return knex.schema.dropTableIfExists('push_subscriptions'); + }; + +// === Fim de: ./src/db/migrations/20251123_create_push_subscriptions.js === + + +// === Início de: ./src/db/migrations/20250619_create_tables.js === +// migrations/20250619_create_tables.js +exports.up = async function(knex) { + // Create 'users' table + await knex.schema.createTable('users', (table) => { + table.increments('id').primary(); + table.string('username', 255).notNullable().unique(); + table.string('password', 255).notNullable(); + table.timestamp('created_at').defaultTo(knex.fn.now()); + }); + + // Create 'chargers' table with new fields + await knex.schema.createTable('chargers', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.integer('user_id').unsigned().notNullable() + .references('id').inTable('users').onDelete('CASCADE'); + table.string('location', 255).notNullable(); + table.string('status', 50).notNullable().defaultTo('offline'); + table.integer('charging_current').notNullable().defaultTo(32); + table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()); + table.string('mqtt_user', 255).notNullable(); + table.string('mqtt_pass', 255).notNullable(); + table.string('mqtt_topic', 255).notNullable().unique(); + table.timestamp('created_at').defaultTo(knex.fn.now()); + + // Add power and current for 3 phases, voltage and other new fields + table.integer('charging_time').notNullable().defaultTo(0); // Total charging time + table.decimal('consumption', 8, 2).notNullable().defaultTo(0); // Consumption (kWh) + + // Power for 3 phases (L1, L2, L3) + table.decimal('power_l1', 8, 2).notNullable().defaultTo(0); + table.decimal('power_l2', 8, 2).notNullable().defaultTo(0); + table.decimal('power_l3', 8, 2).notNullable().defaultTo(0); + + // Voltage for 3 phases (L1, L2, L3) + table.decimal('voltage_l1', 8, 2).notNullable().defaultTo(0); + table.decimal('voltage_l2', 8, 2).notNullable().defaultTo(0); + table.decimal('voltage_l3', 8, 2).notNullable().defaultTo(0); + + // Current for 3 phases (L1, L2, L3) + table.decimal('current_l1', 8, 2).notNullable().defaultTo(0); + table.decimal('current_l2', 8, 2).notNullable().defaultTo(0); + table.decimal('current_l3', 8, 2).notNullable().defaultTo(0); + }); + + // Create 'charger_configs' table + await knex.schema.createTable('charger_configs', (table) => { + table.uuid('charger_id').primary() + .references('id').inTable('chargers').onDelete('CASCADE'); + table.integer('max_charging_current').notNullable().defaultTo(32); + table.boolean('require_auth').notNullable().defaultTo(false); + table.boolean('rcm_enabled').notNullable().defaultTo(false); + table.integer('temperature_limit').notNullable().defaultTo(60); + table.timestamp('config_received_at').notNullable().defaultTo(knex.fn.now()); + }); + + // Create 'charger_sessions' table + await knex.schema.createTable('charger_sessions', (table) => { + table.increments('id').primary(); + table.uuid('charger_id').notNullable() + .references('id').inTable('chargers').onDelete('CASCADE'); + table.timestamp('started_at').notNullable(); + table.timestamp('ended_at'); + table.decimal('kwh', 8, 2).notNullable().defaultTo(0); + table.decimal('cost', 10, 2); + table.timestamp('created_at').defaultTo(knex.fn.now()); + }); +}; + +exports.down = async function(knex) { + await knex.schema.dropTableIfExists('charger_sessions'); + await knex.schema.dropTableIfExists('charger_configs'); + await knex.schema.dropTableIfExists('chargers'); + await knex.schema.dropTableIfExists('users'); +}; + +// === Fim de: ./src/db/migrations/20250619_create_tables.js === + + +// === Início de: ./src/db/migrations/20250618_enable_pgcrypto.js === +exports.up = async function (knex) { + await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto'); + }; + + exports.down = async function (knex) { + // normalmente não se remove extensão em down, mas deixo seguro: + // await knex.raw('DROP EXTENSION IF EXISTS pgcrypto'); + }; + +// === Fim de: ./src/db/migrations/20250618_enable_pgcrypto.js === + + +// === Início de: ./src/db/migrations/20251123084023_create_charger_schedules.js === +// shim para compatibilidade com o nome antigo registado no knex_migrations +module.exports = require('./20251123_create_charger_schedules'); + +// === Fim de: ./src/db/migrations/20251123084023_create_charger_schedules.js === + + +// === Início de: ./src/domain/normalize/chargingConfig.js === +// src/domain/normalize/chargingConfig.js + +/** + * Normaliza eventos de config (quando o carregador manda config) + */ +function normalizeChargingConfig(data = {}) { + const chargerId = data.charger_id || data.chargerId || data.id; + const cfg = data.config || data.raw?.config || data; + + return { + charger_id: chargerId, + mqtt_topic: data.mqtt_topic || data.mqttTopic, + config: { + max_charging_current: + cfg.max_charging_current ?? + cfg.maxChargingCurrent ?? + cfg.max_current ?? + cfg.maxCurrent ?? + undefined, + + require_auth: cfg.require_auth ?? cfg.requireAuth ?? undefined, + rcm_enabled: cfg.rcm_enabled ?? cfg.rcmEnabled ?? undefined, + temperature_limit: + cfg.temperature_limit ?? + cfg.temperatureThreshold ?? + cfg.temp_limit ?? + undefined, + }, + raw: data.raw || data, + updated_at: new Date().toISOString(), + }; +} + +module.exports = { normalizeChargingConfig }; + +// === Fim de: ./src/domain/normalize/chargingConfig.js === + + +// === Início de: ./src/domain/normalize/chargingStatus.js === +// src/domain/normalize/chargingStatus.js + +const toNum = (v) => { + if (v === null || v === undefined || v === '') return 0; + const n = typeof v === 'number' ? v : parseFloat(v); + return Number.isFinite(n) ? n : 0; +}; + +const toArr3 = (v) => { + if (Array.isArray(v)) return [toNum(v[0]), toNum(v[1]), toNum(v[2])]; + if (v && typeof v === 'object') return [toNum(v.l1), toNum(v.l2), toNum(v.l3)]; + return [0, 0, 0]; +}; + +const normalizeStatusText = (rawStatus) => { + const s = String(rawStatus || '').toLowerCase(); + + if (s.includes('charging')) return '⚡ Charging'; + if (s.includes('ready')) return '🟢 Ready'; + if (s.includes('fault') || s.includes('error')) return '⚠️ Fault'; + if (s.includes('wait')) return '⚡ Wait'; + if (s.includes('not conn') || s.includes('disconnected')) return '🔌 Not Conn.'; + if (s.includes('vent')) return '💨 Vent'; + + return rawStatus || '—'; +}; + +/** + * Normaliza eventos de status (realtime) vindos do mqtt -> socket + */ +function normalizeChargingStatus(data = {}) { + const chargerId = data.charger_id || data.chargerId || data.id; + + const powerArr = toArr3(data.power || data.raw?.power); + const voltageArr = toArr3(data.voltage || data.raw?.voltage); + const currentArr = toArr3(data.current || data.raw?.current); + + const status = normalizeStatusText(data.status || data.state || data.raw?.state); + + const chargingTime = + toNum(data.charging_time) || + toNum(data.chargingTime) || + toNum(data.raw?.chargingTime) || + toNum(data.raw?.sessionTime); + + const consumption = toNum(data.consumption) || toNum(data.raw?.consumption); + + const chargingCurrent = + toNum(data.charging_current) || + toNum(data.chargingCurrent) || + currentArr[0]; + + return { + charger_id: chargerId, + mqtt_topic: data.mqtt_topic || data.mqttTopic, + + status, + stateCode: data.stateCode || data.raw?.stateCode || undefined, + + consumption, + charging_time: chargingTime, + charging_current: chargingCurrent, + + power: powerArr, + voltage: voltageArr, + current: currentArr, + + raw: data.raw || data, + updated_at: new Date().toISOString(), + }; +} + +module.exports = { normalizeChargingStatus }; + +// === Fim de: ./src/domain/normalize/chargingStatus.js === + + +// === Início de: ./src/routes/chargers.routes.js === +// src/routes/chargers.routes.js +const express = require('express'); +const { body, param } = require('express-validator'); + +const verifyToken = require('../middleware/auth'); +const handleValidation = require('../middleware/validate'); +const chargersService = require('../services/chargers.service'); + +const router = express.Router(); +router.use(verifyToken); + +router.get('/', async (req, res, next) => { + try { + const data = await chargersService.list(req.user.id); + res.json({ success: true, data }); + } catch (err) { + next(err); + } +}); + +router.get( + '/:id', + [param('id').isUUID()], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.getOne(req.user.id, req.params.id); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.post( + '/', + [body('location').exists().isString().isLength({ min: 1 }).trim()], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.create(req.user.id, req.body.location); + res.status(201).json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.put( + '/:id', + [ + param('id').isUUID(), + body('charger').optional().isObject(), + body('config').optional().isObject(), + body('location').optional().isString(), + ], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.update(req.user.id, req.params.id, req.body); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.delete( + '/:id', + [param('id').isUUID()], + handleValidation, + async (req, res, next) => { + try { + await chargersService.remove(req.user.id, req.params.id); + res.json({ success: true, message: 'Carregador excluído com sucesso' }); + } catch (err) { + next(err); + } + } +); + +router.put( + '/:id/config', + [param('id').isUUID(), body('config').isObject()], + handleValidation, + async (req, res, next) => { + try { + const out = await chargersService.updateConfig(req.user.id, req.params.id, req.body.config); + res.json({ success: true, data: out.data, message: out.message }); + } catch (err) { + next(err); + } + } +); + +router.get( + '/:id/schedule', + [param('id').isUUID()], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.getSchedules(req.user.id, req.params.id); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.post( + '/:id/schedule', + [ + param('id').isUUID(), + body('start').matches(/^\d{2}:\d{2}$/), + body('end').matches(/^\d{2}:\d{2}$/), + body('repeat').isIn(['everyday', 'weekdays', 'weekends']), + ], + handleValidation, + async (req, res, next) => { + try { + const { start, end, repeat } = req.body; + const data = await chargersService.createSchedule(req.user.id, req.params.id, start, end, repeat); + res.status(201).json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.post( + '/:id/action', + [ + param('id').isUUID(), + body('action').isIn(['start', 'stop']), + body('ampLimit').optional().isInt({ min: 6, max: 64 }), + ], + handleValidation, + async (req, res, next) => { + try { + const { action, ampLimit } = req.body; + await chargersService.action(req.user.id, req.params.id, action, ampLimit); + res.json({ success: true, message: `Comando '${action}' enviado com sucesso` }); + } catch (err) { + next(err); + } + } +); + +module.exports = router; + +// === Fim de: ./src/routes/chargers.routes.js === + + +// === Início de: ./src/routes/sessions.routes.js === +// src/routes/sessions.routes.js +const express = require('express'); +const { param, query, body } = require('express-validator'); + +const verifyToken = require('../middleware/auth'); +const handleValidation = require('../middleware/validate'); +const sessionsService = require('../services/sessions.service'); + +const router = express.Router(); +router.use(verifyToken); + // GET /api/charger_sessions?chargerId=... router.get( '/', [query('chargerId').isUUID().withMessage('chargerId deve ser UUID válido')], handleValidation, - async (req, res) => { - const { chargerId } = req.query; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id: chargerId, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(403) - .json({ success: false, message: 'Acesso não autorizado' }); + async (req, res, next) => { + try { + const { chargerId } = req.query; + const data = await sessionsService.listByCharger(req.user.id, chargerId); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +// ✅ /history antes de /:id +router.get( + '/history/:chargerId', + [ + param('chargerId').isUUID().withMessage('chargerId deve ser UUID válido'), + query('viewMode').isIn(['Day', 'Week', 'Month']).withMessage('viewMode inválido'), + ], + handleValidation, + async (req, res, next) => { + try { + const { chargerId } = req.params; + const { viewMode } = req.query; + + const data = await sessionsService.history(req.user.id, chargerId, viewMode); + res.json({ success: true, data }); + } catch (err) { + next(err); } - - const sessions = await db('charger_sessions') - .where({ charger_id: chargerId }) - .orderBy('started_at', 'desc'); - - res.json({ success: true, data: sessions }); } ); -// GET /api/charger_sessions/:id router.get( '/:id', - [param('id').isUUID().withMessage('ID de sessão inválido')], + [param('id').isInt().withMessage('ID de sessão inválido')], handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - const session = await db('charger_sessions') - .join('chargers', 'charger_sessions.charger_id', 'chargers.id') - .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) - .select('charger_sessions.*') - .first(); - - if (!session) { - return res - .status(404) - .json({ success: false, message: 'Sessão não encontrada' }); + async (req, res, next) => { + try { + const data = await sessionsService.getById(req.user.id, Number(req.params.id)); + res.json({ success: true, data }); + } catch (err) { + next(err); } - - res.json({ success: true, data: session }); } ); -// POST /api/charger_sessions router.post( '/', [body('charger_id').isUUID().withMessage('charger_id deve ser UUID válido')], handleValidation, - async (req, res) => { - const { charger_id } = req.body; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id: charger_id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(403) - .json({ success: false, message: 'Acesso não autorizado' }); + async (req, res, next) => { + try { + const data = await sessionsService.create(req.user.id, req.body.charger_id); + res.status(201).json({ success: true, data }); + } catch (err) { + next(err); } - - const [inserted] = await db('charger_sessions') - .insert({ charger_id, started_at: new Date() }) - .returning('*'); - - res.status(201).json({ success: true, data: inserted }); } ); -// PUT /api/charger_sessions/:id router.put( '/:id', [ - param('id').isUUID().withMessage('ID de sessão inválido'), + param('id').isInt().withMessage('ID de sessão inválido'), body('ended_at').optional().isISO8601().toDate(), body('kwh').optional().isFloat({ min: 0 }), body('cost').optional().isFloat({ min: 0 }), ], handleValidation, - async (req, res) => { - const { id } = req.params; - const { ended_at, kwh, cost } = req.body; - const userId = req.user.id; + async (req, res, next) => { + try { + const id = Number(req.params.id); + const { ended_at, kwh, cost } = req.body; - const session = await db('charger_sessions') - .join('chargers', 'charger_sessions.charger_id', 'chargers.id') - .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) - .first(); - - if (!session) { - return res - .status(404) - .json({ success: false, message: 'Sessão não encontrada' }); + const data = await sessionsService.update(req.user.id, id, { ended_at, kwh, cost }); + res.json({ success: true, data }); + } catch (err) { + next(err); } - - const [updated] = await db('charger_sessions') - .where({ id }) - .update({ ended_at, kwh, cost }) - .returning('*'); - - res.json({ success: true, data: updated }); } ); -// DELETE /api/charger_sessions/:id router.delete( '/:id', - [param('id').isUUID().withMessage('ID de sessão inválido')], + [param('id').isInt().withMessage('ID de sessão inválido')], handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - const deleted = await db('charger_sessions') - .join('chargers', 'charger_sessions.charger_id', 'chargers.id') - .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) - .del(); - - if (!deleted) { - return res - .status(404) - .json({ success: false, message: 'Sessão não encontrada' }); + async (req, res, next) => { + try { + const ok = await sessionsService.remove(req.user.id, Number(req.params.id)); + res.json({ success: true, message: ok ? 'Sessão excluída com sucesso' : 'OK' }); + } catch (err) { + next(err); } - - res.json({ success: true, message: 'Sessão excluída com sucesso' }); - } -); - -// GET /api/charger_sessions/history/:chargerId -router.get( - '/history/:chargerId', - [ - param('chargerId').isUUID().withMessage('chargerId deve ser UUID válido'), - query('viewMode') - .isIn(['Day', 'Week', 'Month']) - .withMessage('viewMode inválido'), - ], - handleValidation, - async (req, res) => { - const { chargerId } = req.params; - const { viewMode } = req.query; - const userId = req.user.id; - - // ownership check - const charger = await db('chargers') - .where({ id: chargerId, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(403) - .json({ success: false, message: 'Acesso não autorizado' }); - } - - let qb = db('charger_sessions') - .where({ charger_id: chargerId }) - .sum('kwh as total_kwh'); - - switch (viewMode) { - case 'Day': - qb = qb - .select(db.raw('DATE(started_at) AS period')) - .groupBy(db.raw('DATE(started_at)')) - .orderBy('period', 'desc'); - break; - - case 'Week': - qb = qb - .select( - db.raw('EXTRACT(ISOYEAR FROM started_at) AS y'), - db.raw('EXTRACT(WEEK FROM started_at) AS w'), - db.raw( - "EXTRACT(ISOYEAR FROM started_at)||'-'||LPAD(EXTRACT(WEEK FROM started_at)::text,2,'0') AS period" - ) - ) - .groupBy('y', 'w') - .orderBy([{ column: 'y', order: 'desc' }, { column: 'w', order: 'desc' }]); - break; - - case 'Month': - qb = qb - .select( - db.raw('EXTRACT(YEAR FROM started_at) AS y'), - db.raw('EXTRACT(MONTH FROM started_at) AS m'), - db.raw( - "EXTRACT(YEAR FROM started_at)||'-'||LPAD(EXTRACT(MONTH FROM started_at)::text,2,'0') AS period" - ) - ) - .groupBy('y', 'm') - .orderBy([{ column: 'y', order: 'desc' }, { column: 'm', order: 'desc' }]); - break; - } - - const rows = await qb; - - // ✅ devolve lista vazia em vez de 404 - if (!rows.length) { - return res.json({ success: true, data: [] }); - } - - const data = rows.map((r) => ({ - started_at: r.period, - kwh: parseFloat(r.total_kwh) || 0, - })); - - res.json({ success: true, data }); } ); module.exports = router; -// === Fim de: ./routes/charger_sessions.js === +// === Fim de: ./src/routes/sessions.routes.js === -// === Início de: ./routes/push.js === -// routes/push.js +// === Início de: ./src/routes/push.routes.js === +// src/routes/push.routes.js const express = require('express'); -const { body, validationResult } = require('express-validator'); -const verifyToken = require('../middleware/verifyToken'); -const db = require('../db'); -const { sendPushToUser } = require('../utils/pushService'); +const { body } = require('express-validator'); + +const verifyToken = require('../middleware/auth'); +const handleValidation = require('../middleware/validate'); +const config = require('../config'); + +const pushHttpService = require('../services/pushHttp.service'); +const { sendPushToUser } = require('../services/push.service'); const router = express.Router(); router.use(verifyToken); -function handleValidation(req, res, next) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, errors: errors.array() }); - } - next(); -} - // GET /api/push/vapid-public-key router.get('/vapid-public-key', (req, res) => { - if (!process.env.VAPID_PUBLIC_KEY) { - return res - .status(503) - .json({ success: false, message: 'Push indisponível' }); + if (!config.vapid.publicKey) { + return res.status(503).json({ success: false, message: 'Push indisponível' }); } - res.json({ success: true, data: { key: process.env.VAPID_PUBLIC_KEY } }); + res.json({ success: true, data: { key: config.vapid.publicKey } }); }); // POST /api/push/subscribe @@ -360,32 +1431,17 @@ router.post( body('keys.auth').isString(), ], handleValidation, - async (req, res) => { - const userId = req.user.id; - const { endpoint, keys } = req.body; - const ua = req.headers['user-agent'] || null; + async (req, res, next) => { + try { + const userId = req.user.id; + const { endpoint, keys } = req.body; + const ua = req.headers['user-agent'] || null; - // evita duplicados - const existing = await db('push_subscriptions') - .where({ endpoint, user_id: userId }) - .first(); - - if (existing) { - return res.json({ success: true, data: existing }); + const out = await pushHttpService.subscribe(userId, endpoint, keys, ua); + res.status(out.created ? 201 : 200).json({ success: true, data: out.row }); + } catch (err) { + next(err); } - - const [inserted] = await db('push_subscriptions') - .insert({ - user_id: userId, - endpoint, - p256dh: keys.p256dh, - auth: keys.auth, - user_agent: ua, - created_at: new Date().toISOString(), - }) - .returning('*'); - - res.status(201).json({ success: true, data: inserted }); } ); @@ -394,53 +1450,46 @@ router.post( '/unsubscribe', [body('endpoint').optional().isString()], handleValidation, - async (req, res) => { - const userId = req.user.id; - const { endpoint } = req.body || {}; - - // se não houver sub no navegador, responde ok - if (!endpoint) { - return res.json({ success: true, message: 'No subscription' }); + async (req, res, next) => { + try { + const userId = req.user.id; + const { endpoint } = req.body || {}; + const out = await pushHttpService.unsubscribe(userId, endpoint); + res.json({ success: true, message: out.message }); + } catch (err) { + next(err); } - - await db('push_subscriptions') - .where({ endpoint, user_id: userId }) - .del(); - - res.json({ success: true, message: 'Unsubscribed' }); } ); // POST /api/push/test -router.post('/test', async (req, res) => { - const userId = req.user.id; - await sendPushToUser(userId, { - title: '📬 Teste EV Station', - body: 'Push notifications estão a funcionar!', - url: '/', - }); - - res.json({ success: true, message: 'Push enviado' }); +router.post('/test', async (req, res, next) => { + try { + const userId = req.user.id; + await sendPushToUser(userId, { + title: '📬 Teste EV Station', + body: 'Push notifications estão a funcionar!', + url: '/', + }); + res.json({ success: true, message: 'Push enviado' }); + } catch (err) { + next(err); + } }); module.exports = router; -// === Fim de: ./routes/push.js === +// === Fim de: ./src/routes/push.routes.js === -// === Início de: ./routes/users.js === +// === Início de: ./src/routes/users.routes.js === +// src/routes/users.routes.js const express = require('express'); -const router = express.Router(); -const db = require('../db'); -const jwt = require('jsonwebtoken'); -const bcrypt = require('bcryptjs'); const rateLimit = require('express-rate-limit'); +const usersService = require('../services/users.service'); -if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET não definido no .env'); -} +const router = express.Router(); -// limiter só para auth const authLimiter = rateLimit({ windowMs: 60 * 1000, max: 10, @@ -448,722 +1497,1197 @@ const authLimiter = rateLimit({ legacyHeaders: false, }); -// POST /api/users/login -router.post('/login', authLimiter, async (req, res) => { - const { username, password } = req.body; - - if (!username || !password) { - return res - .status(400) - .json({ success: false, message: 'Usuário e senha são obrigatórios' }); - } - +router.post('/login', authLimiter, async (req, res, next) => { try { - const user = await db('users').where({ username }).first(); - - if (!user) { - return res - .status(401) - .json({ success: false, message: 'Credenciais inválidas' }); - } - - const isValidPassword = await bcrypt.compare(password, user.password); - if (!isValidPassword) { - return res - .status(401) - .json({ success: false, message: 'Credenciais inválidas' }); - } - - const token = jwt.sign( - { id: user.id, username: user.username }, - process.env.JWT_SECRET, - { expiresIn: '24h' } - ); - - return res.json({ success: true, data: { token } }); + const { username, password } = req.body; + const data = await usersService.login(username, password); + res.json({ success: true, data }); } catch (err) { - console.error('Erro ao autenticar usuário:', err); - return res - .status(500) - .json({ success: false, message: 'Erro interno do servidor' }); + next(err); } }); -// POST /api/users/register -router.post('/register', authLimiter, async (req, res) => { - const { username, password } = req.body; - - if ( - !username || - !password || - typeof username !== 'string' || - typeof password !== 'string' || - username.length < 3 || - password.length < 4 - ) { - return res.status(400).json({ - success: false, - message: - 'Nome de usuário deve ter pelo menos 3 caracteres e senha pelo menos 4 caracteres', - }); - } - +router.post('/register', authLimiter, async (req, res, next) => { try { - const existing = await db('users').where({ username }).first(); - if (existing) { - return res - .status(409) - .json({ success: false, message: 'Nome de usuário já está em uso' }); - } - - const hashedPassword = await bcrypt.hash(password, 10); - - const [row] = await db('users') - .insert({ username, password: hashedPassword }) - .returning('id'); - - const token = jwt.sign( - { id: row.id, username }, - process.env.JWT_SECRET, - { expiresIn: '24h' } - ); - - return res.status(201).json({ success: true, data: { token } }); + const { username, password } = req.body; + const data = await usersService.register(username, password); + res.status(201).json({ success: true, data }); } catch (err) { - console.error('Erro ao registrar usuário:', err); - return res.status(500).json({ - success: false, - message: 'Erro interno ao registrar usuário', - }); + next(err); } }); module.exports = router; -// === Fim de: ./routes/users.js === +// === Fim de: ./src/routes/users.routes.js === -// === Início de: ./routes/chargers.js === -// routes/chargers.js -const express = require('express'); -const { body, param, validationResult } = require('express-validator'); -const router = express.Router(); -const verifyToken = require('../middleware/verifyToken'); -const db = require('../db'); -const crypto = require('crypto'); -const axios = require('axios'); -const mqttClient = require('../mqtt/client'); +// === Início de: ./src/repositories/chargers.repo.js === +// src/repositories/chargers.repo.js +const db = require('../db/knex'); -router.use(verifyToken); - -function handleValidation(req, res, next) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, errors: errors.array() }); - } - next(); +async function listByUser(userId) { + return db('chargers').where({ user_id: userId }).select('*'); } -// throttling simples em memória (por charger) -const lastConfigUpdateAt = new Map(); +async function findByIdForUser(id, userId) { + return db('chargers').where({ id, user_id: userId }).first(); +} + +async function findByMqttTopic(mqtt_topic) { + return db('chargers').where({ mqtt_topic }).first(); +} + +async function insertCharger(data) { + const [row] = await db('chargers').insert(data).returning('*'); + return row; +} + +async function updateChargerForUser(id, userId, patch) { + const [row] = await db('chargers') + .where({ id, user_id: userId }) + .update(patch) + .returning('*'); + return row; +} + +async function deleteChargerForUser(id, userId) { + // devolve o charger antes para poderes usar mqtt_user/pass/topic etc + const charger = await findByIdForUser(id, userId); + if (!charger) return null; + + await db('chargers').where({ id, user_id: userId }).del(); + return charger; +} + +async function getConfig(charger_id) { + return db('charger_configs').where({ charger_id }).first(); +} + +async function insertConfig(data) { + const [row] = await db('charger_configs').insert(data).returning('*'); + return row; +} + +async function updateConfig(charger_id, patch) { + const [row] = await db('charger_configs') + .where({ charger_id }) + .update(patch) + .returning('*'); + return row; +} + +async function upsertConfig(charger_id, patch) { + const existing = await getConfig(charger_id); + if (existing) return updateConfig(charger_id, patch); + return insertConfig({ charger_id, ...patch }); +} + +async function listSchedules(charger_id) { + return db('charger_schedules').where({ charger_id }).orderBy('created_at', 'desc'); +} + +async function insertSchedule(data) { + const [row] = await db('charger_schedules').insert(data).returning('*'); + return row; +} + +module.exports = { + listByUser, + findByIdForUser, + findByMqttTopic, + insertCharger, + updateChargerForUser, + deleteChargerForUser, + getConfig, + insertConfig, + updateConfig, + upsertConfig, + listSchedules, + insertSchedule, +}; + +// === Fim de: ./src/repositories/chargers.repo.js === + + +// === Início de: ./src/repositories/push.repo.js === +// src/repositories/push.repo.js +const db = require('../db/knex'); + +async function findByUserAndEndpoint(user_id, endpoint) { + return db('push_subscriptions').where({ user_id, endpoint }).first(); +} + +async function findByEndpoint(endpoint) { + return db('push_subscriptions').where({ endpoint }).first(); +} + +async function listByUser(user_id) { + return db('push_subscriptions') + .where({ user_id }) + .select('id', 'endpoint', 'p256dh', 'auth'); +} + +async function insertSubscription(data) { + const [row] = await db('push_subscriptions').insert(data).returning('*'); + return row; +} + +async function deleteByUserAndEndpoint(user_id, endpoint) { + return db('push_subscriptions').where({ user_id, endpoint }).del(); +} + +async function deleteById(id) { + return db('push_subscriptions').where({ id }).del(); +} + +module.exports = { + findByUserAndEndpoint, + findByEndpoint, + listByUser, + insertSubscription, + deleteByUserAndEndpoint, + deleteById, +}; + +// === Fim de: ./src/repositories/push.repo.js === + + +// === Início de: ./src/repositories/sessions.repo.js === +// src/repositories/sessions.repo.js +const db = require('../db/knex'); + +async function listByCharger(charger_id) { + return db('charger_sessions') + .where({ charger_id }) + .orderBy('started_at', 'desc'); +} + +async function findByIdForUser(id, userId) { + return db('charger_sessions') + .join('chargers', 'charger_sessions.charger_id', 'chargers.id') + .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) + .select('charger_sessions.*') + .first(); +} + +async function insertSession(data) { + const [row] = await db('charger_sessions').insert(data).returning('*'); + return row; +} + +async function updateById(id, patch) { + const [row] = await db('charger_sessions').where({ id }).update(patch).returning('*'); + return row; +} + +async function deleteByIdForUser(id, userId) { + return db('charger_sessions') + .join('chargers', 'charger_sessions.charger_id', 'chargers.id') + .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) + .del(); +} + +async function historyAgg(chargerId, viewMode) { + let qb = db('charger_sessions') + .where({ charger_id: chargerId }) + .sum('kwh as total_kwh'); + + switch (viewMode) { + case 'Day': + qb = qb + .select(db.raw('DATE(started_at) AS period')) + .groupBy(db.raw('DATE(started_at)')) + .orderBy('period', 'desc'); + break; + + case 'Week': + qb = qb + .select( + db.raw('EXTRACT(ISOYEAR FROM started_at) AS y'), + db.raw('EXTRACT(WEEK FROM started_at) AS w'), + db.raw( + "EXTRACT(ISOYEAR FROM started_at)||'-'||LPAD(EXTRACT(WEEK FROM started_at)::text,2,'0') AS period" + ) + ) + .groupBy('y', 'w') + .orderBy([{ column: 'y', order: 'desc' }, { column: 'w', order: 'desc' }]); + break; + + case 'Month': + qb = qb + .select( + db.raw('EXTRACT(YEAR FROM started_at) AS y'), + db.raw('EXTRACT(MONTH FROM started_at) AS m'), + db.raw( + "EXTRACT(YEAR FROM started_at)||'-'||LPAD(EXTRACT(MONTH FROM started_at)::text,2,'0') AS period" + ) + ) + .groupBy('y', 'm') + .orderBy([{ column: 'y', order: 'desc' }, { column: 'm', order: 'desc' }]); + break; + } + + return qb; +} + +module.exports = { + listByCharger, + findByIdForUser, + insertSession, + updateById, + deleteByIdForUser, + historyAgg, +}; + +// === Fim de: ./src/repositories/sessions.repo.js === + + +// === Início de: ./src/repositories/users.repo.js === +// src/repositories/users.repo.js +const db = require('../db/knex'); + +async function findByUsername(username) { + return db('users').where({ username }).first(); +} + +async function insertUser({ username, passwordHash }) { + const [row] = await db('users') + .insert({ username, password: passwordHash }) + .returning('id'); + + // pg pode devolver {id} ou o valor direto (depende config) + return row?.id ?? row; +} + +module.exports = { findByUsername, insertUser }; + +// === Fim de: ./src/repositories/users.repo.js === + + +// === Início de: ./src/mqtt/index.js === +// src/mqtt/index.js +const mqtt = require('mqtt'); +const EventEmitter = require('events'); + +const db = require('../db/knex'); +const config = require('../config'); +const { sendPushToUser } = require('../services/push.service'); + +const { createPublishers } = require('./publishers'); +const { createMqttContext } = require('./context'); + +const { + handleStateEvse, + handleStateScheduler, + handleStateLoadbalancing, + handleStateAuth, + handleStateMetersConfig, +} = require('./handlers/evse.handler'); +const { handleStateMeter } = require('./handlers/meter.handler'); +const { handleLegacyState, handleLegacyConfigResponse } = require('./handlers/legacy.handler'); + +const emitter = new EventEmitter(); + +// -------------------- +// MQTT client +// -------------------- +const MQTT_URL = config.mqtt.url; +const mqttUser = config.mqtt.user; +const mqttPass = config.mqtt.pass; + +const client = mqtt.connect(MQTT_URL, { + username: mqttUser, + password: mqttPass, + reconnectPeriod: 2000, +}); + +// publishers separados +const publishers = createPublishers(client); + +// ctx separado (helpers + caches + db helpers) +const ctx = createMqttContext({ db, config, emitter, sendPushToUser }); + +// -------------------- +// Subscribe +// -------------------- +client.on('connect', () => { + console.log('[MQTT] Conectado ao broker:', MQTT_URL); + + const fixedTopics = [ + '+/state/#', // state/evse, state/scheduler, state/loadbalancing, state/meter/... + '+/state', // legacy + '+/response/#', // legacy + '+/response/config/evse', // legacy + ]; + + const envTopics = config.mqtt.subTopics || []; + const topicsToSub = [...new Set([...fixedTopics, ...envTopics])]; + + topicsToSub.forEach((t) => { + client.subscribe(t, { qos: 0 }, (err, granted) => { + if (err) { + console.error('[MQTT] Falha ao subscrever', t, err.message); + } else { + console.log('[MQTT] Subscrito:', granted?.map((g) => g.topic).join(', ') || t); + } + }); + }); +}); + +// -------------------- +// Messages +// -------------------- +client.on('message', async (topic, message) => { + const parts = topic.split('/'); + const mqttTopic = parts[0]; + const subtopic = parts.slice(1).join('/'); + + const payload = ctx.safeJsonParse(message); + if (!payload) { + console.warn('[MQTT] JSON inválido em', topic, 'payload=', message.toString()); + return; + } -// GET /api/chargers -router.get('/', async (req, res) => { - const userId = req.user.id; try { - const chargers = await db('chargers') - .where({ user_id: userId }) - .select('*'); - res.json({ success: true, data: chargers }); + // -------- NOVOS TÓPICOS DO EVSE -------- + if (subtopic === 'state/evse') return await handleStateEvse(ctx, mqttTopic, payload); + if (subtopic === 'state/scheduler') + return await handleStateScheduler(ctx, mqttTopic, payload); + if (subtopic === 'state/loadbalancing') + return await handleStateLoadbalancing(ctx, mqttTopic, payload); + if (subtopic === 'state/auth') return await handleStateAuth(ctx, mqttTopic, payload); + if (subtopic === 'state/meters-config') + return await handleStateMetersConfig(ctx, mqttTopic, payload); + if (subtopic === 'state/meter/evse') + return await handleStateMeter(ctx, mqttTopic, payload, 'evse'); + if (subtopic === 'state/meter/grid') + return await handleStateMeter(ctx, mqttTopic, payload, 'grid'); + + // -------- LEGACY COMPAT -------- + if (subtopic === 'state') return await handleLegacyState(ctx, mqttTopic, payload); + if (subtopic === 'response/config/evse') + return await handleLegacyConfigResponse(ctx, mqttTopic, payload); + + // fallback ignorado } catch (err) { - console.error('Erro ao buscar carregadores:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); + console.error('[MQTT] Erro ao processar', topic, err); } }); -// GET /api/chargers/:id -router.get( - '/:id', - [param('id').isUUID()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; +// -------------------- +// Broker offline / checker +// -------------------- +client.on('offline', async () => { + console.warn('[MQTT] Broker offline'); - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); + try { + const chargers = await ctx.db('chargers').select('id', 'user_id', 'location'); + const uniqueUsers = [...new Set(chargers.map((c) => c.user_id))]; - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - let config = null; - try { - config = await db('charger_configs') - .where({ charger_id: charger.id }) - .first(); - } catch (cfgErr) { - console.error('[GET charger] erro ao buscar charger_configs:', cfgErr); - } - - if (!config) - config = { - charger_id: charger.id, - max_charging_current: 32, - require_auth: false, - rcm_enabled: false, - temperature_limit: 60, - }; - - const numericFields = [ - 'power_l1', - 'power_l2', - 'power_l3', - 'voltage_l1', - 'voltage_l2', - 'voltage_l3', - 'current_l1', - 'current_l2', - 'current_l3', - 'charging_current', - 'consumption', - ]; - - numericFields.forEach((field) => { - const v = charger[field]; - charger[field] = - v === null || v === undefined || v === '' ? 0 : Number(v); - if (Number.isNaN(charger[field])) charger[field] = 0; - }); - - return res.json({ success: true, data: { ...charger, config } }); - } catch (err) { - console.error('Erro ao buscar carregador:', err); - - return res.status(500).json({ - success: false, - message: - process.env.NODE_ENV === 'development' - ? err.message || 'Erro no servidor' - : 'Erro no servidor', - }); - } - } -); - -// POST /api/chargers -router.post( - '/', - [ - body('location') - .exists() - .withMessage('O campo location é obrigatório') - .isString() - .withMessage('Location deve ser uma string') - .isLength({ min: 1 }) - .withMessage('Location não pode estar vazio') - .trim(), - ], - handleValidation, - async (req, res) => { - const { location } = req.body; - const userId = req.user.id; - const now = new Date().toISOString(); - - let chargerID; - do { - chargerID = crypto.randomBytes(6).toString('hex'); - } while ( - await db('chargers').where({ mqtt_topic: chargerID }).first() - ); - - const mqtt_topic = chargerID; - const mqtt_user = chargerID; - const mqtt_pass = crypto.randomBytes(6).toString('hex'); - - try { - const [charger] = await db('chargers') - .insert({ - user_id: userId, - location, - status: 'offline', - charging_current: 0, - charging_time: 0, - consumption: 0, - power_l1: 0.0, - power_l2: 0.0, - power_l3: 0.0, - voltage_l1: 0.0, - voltage_l2: 0.0, - voltage_l3: 0.0, - current_l1: 0.0, - current_l2: 0.0, - current_l3: 0.0, - mqtt_user, - mqtt_pass, - mqtt_topic, - updated_at: now, + await Promise.allSettled( + uniqueUsers.map((userId) => + ctx.sendPushToUser(userId, { + title: '📡 Broker MQTT offline', + body: 'O sistema perdeu ligação ao broker. Alguns estados podem estar desatualizados.', + url: '/', }) - .returning('*'); - - await db('charger_configs').insert({ - charger_id: charger.id, - max_charging_current: 32, - require_auth: false, - rcm_enabled: false, - temperature_limit: 60, - config_received_at: now, - }); - - try { - await axios.post( - 'http://192.168.1.110:7000/client/create', - { - client_name: charger.mqtt_user, - chargeID: charger.mqtt_topic, - password: charger.mqtt_pass, - }, - { timeout: 5000 } - ); - } catch (err) { - console.error( - 'Erro ao criar cliente Mosquitto:', - err?.response?.data || err.message - ); - } - - res.status(201).json({ success: true, data: charger }); - } catch (err) { - console.error('Erro ao criar carregador:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); - } - } -); - -// PUT /api/chargers/:id -router.put( - '/:id', - [ - param('id').isUUID(), - body('charger').optional().isObject(), - body('config').optional().isObject(), - body('location').optional().isString(), // compat front - ], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - let { charger = {}, config = {} } = req.body; - - // ✅ compatibilidade com front antigo: { location } - if (req.body.location && !charger.location) { - charger.location = req.body.location; - } - - const trx = await db.transaction(); - - const ALLOWED_CHARGER_FIELDS = ['location']; - const safeChargerUpdate = Object.fromEntries( - Object.entries(charger).filter(([k]) => - ALLOWED_CHARGER_FIELDS.includes(k) ) ); + } catch (err) { + console.error('[MQTT] erro offline push:', err.message); + } +}); - try { - let updatedCharger; +setInterval(async () => { + try { + const timeoutMinutes = config.chargerOfflineMinutes; + const limitDate = new Date(Date.now() - timeoutMinutes * 60 * 1000); - if (Object.keys(safeChargerUpdate).length > 0) { - [updatedCharger] = await trx('chargers') - .where({ id, user_id: userId }) - .update({ - ...safeChargerUpdate, - updated_at: new Date().toISOString(), - }) - .returning('*'); - } else { - updatedCharger = await trx('chargers') - .where({ id, user_id: userId }) - .first(); + const offlineChargers = await ctx.db('chargers') + .where('updated_at', '<', limitDate.toISOString()) + .andWhereNot({ status: 'offline' }) + .select('*'); + + for (const ch of offlineChargers) { + await ctx.db('chargers').where({ id: ch.id }).update({ status: 'offline' }); + + ctx.lastDbStateByChargerId.delete(ch.id); + ctx.lastMetaByChargerId.delete(ch.id); + ctx.lastTotalEnergyByChargerId.delete(ch.id); + ctx.sessionStartEnergyByChargerId.delete(ch.id); + + ctx.lastEnabled[ch.id] = false; + + await ctx.sendPushToUser(ch.user_id, { + title: '🔌 Carregador offline', + body: `${ch.location || 'Carregador'} está offline há mais de ${timeoutMinutes} min.`, + url: `/charger/${ch.id}`, + }); + } + } catch (err) { + console.error('[MQTT] offline checker erro:', err.message); + } +}, 60 * 1000); + +// -------------------- +// API pública (compat com o resto do projeto) +// -------------------- +function on(event, handler) { + emitter.on(event, handler); +} + +module.exports = { + on, + ...publishers, +}; + +// === Fim de: ./src/mqtt/index.js === + + +// === Início de: ./src/mqtt/publishers.js === +// src/mqtt/publishers.js +function createPublishers(client) { + /** + * ✅ Firmware novo: + * Para settings: /cmd/evse/settings + * payload: { currentLimit, temperatureLimit } + */ + function sendEvseSettings(chargerTopic, settings = {}) { + const payload = {}; + if (settings.currentLimit !== undefined) payload.currentLimit = Number(settings.currentLimit); + if (settings.temperatureLimit !== undefined) + payload.temperatureLimit = Number(settings.temperatureLimit); + + if (!Object.keys(payload).length) return; + + client.publish(`${chargerTopic}/cmd/evse/settings`, JSON.stringify(payload), { qos: 1 }); + } + + // compat + mapeamento + function sendConfig(chargerTopic, property, value) { + const map = { + maxChargingCurrent: 'currentLimit', + temperatureThreshold: 'temperatureLimit', + }; + + if (map[property]) { + return sendEvseSettings(chargerTopic, { [map[property]]: value }); } + + // fallback legacy + const payload = { [property]: value }; + client.publish(`${chargerTopic}/set/config/evse`, JSON.stringify(payload), { qos: 1 }); + } + + // legacy (mantidos) + function sendEnable(chargerTopic, enable) { + client.publish(`${chargerTopic}/enable`, JSON.stringify({ enable: !!enable }), { qos: 1 }); + } + + function requestConfig(chargerTopic) { + client.publish(`${chargerTopic}/request/config/evse`, null, { qos: 1 }); + } + + // helper genérico novo + function sendCmd(chargerTopic, cmdSubtopic, obj) { + const t = `${chargerTopic}/cmd/${cmdSubtopic}`; + const msg = obj ? JSON.stringify(obj) : ''; + client.publish(t, msg, { qos: 1 }); + } + + return { sendConfig, sendEvseSettings, sendEnable, requestConfig, sendCmd }; + } + + module.exports = { createPublishers }; + +// === Fim de: ./src/mqtt/publishers.js === - if (!updatedCharger) { - await trx.rollback(); - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - if (Object.keys(config).length > 0) { - const existing = await trx('charger_configs') - .where({ charger_id: id }) - .first(); +// === Início de: ./src/mqtt/context.js === +// src/mqtt/context.js +function createMqttContext({ db, config, emitter, sendPushToUser }) { + // -------------------- + // Helpers + // -------------------- + const toNum = (v) => { + if (v === null || v === undefined || v === '') return 0; + const n = typeof v === 'number' ? v : parseFloat(v); + return Number.isFinite(n) ? n : 0; + }; - const data = { - ...config, - config_received_at: new Date().toISOString(), + const round1 = (v) => Math.round(toNum(v) * 10) / 10; + const round2 = (v) => Math.round(toNum(v) * 100) / 100; + + const wToKw2 = (w) => round2(toNum(w) / 1000); + + const toArr3 = (v) => { + if (Array.isArray(v)) return [round1(v[0]), round1(v[1]), round1(v[2])]; + if (v && typeof v === 'object') return [round1(v.l1), round1(v.l2), round1(v.l3)]; + return [0, 0, 0]; + }; + + const toArr3Kw2 = (v) => { + if (Array.isArray(v)) return [wToKw2(v[0]), wToKw2(v[1]), wToKw2(v[2])]; + if (v && typeof v === 'object') return [wToKw2(v.l1), wToKw2(v.l2), wToKw2(v.l3)]; + return [0, 0, 0]; + }; + + function safeJsonParse(buf) { + try { + return JSON.parse(buf.toString()); + } catch { + return null; + } + } + + function shallowEqual(a, b) { + if (a === b) return true; + if (!a || !b) return false; + + const ak = Object.keys(a); + const bk = Object.keys(b); + if (ak.length !== bk.length) return false; + + for (const k of ak) { + const av = a[k]; + const bv = b[k]; + + if (Array.isArray(av) || Array.isArray(bv)) { + if (!Array.isArray(av) || !Array.isArray(bv)) return false; + if (av.length !== bv.length) return false; + for (let i = 0; i < av.length; i++) { + if (av[i] !== bv[i]) return false; + } + } else if (av !== bv) { + return false; + } + } + + return true; + } + + function inferStateCode(raw) { + const s = String(raw || '').trim(); + if (!s) return ''; + return s.split(/\s+/)[0] || ''; + } + + function getStatusFromStateCode(code) { + const map = { + A: '🔌 Not Conn.', + A1: '🔌 Not Conn.', + B1: '🟡 Unauth.', + B2: '🟢 Ready', + C1: '⚡ Wait', + C2: '⚡ Charging', + D1: '💨 Vent (req)', + D2: '💨 Vent', + E: '❌ CP Error', + F: '⚠️ Fault', + }; + return map[code] || '❓ Unknown'; + } + + function stripUndef(obj) { + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); + } + + // -------------------- + // Caches + // -------------------- + const lastDbStateByChargerId = new Map(); // chargerId -> { ...dbFields } + const lastMetaByChargerId = new Map(); // chargerId -> { status, stateCode, rawStatus } + const lastTotalEnergyByChargerId = new Map(); // chargerId -> totalEnergy (kWh) + const sessionStartEnergyByChargerId = new Map(); // chargerId -> totalEnergy at session start + const lastEnabled = {}; // chargerId -> boolean + + const chargerCache = new Map(); // mqttTopic -> { charger, fetchedAt } + const CHARGER_CACHE_TTL_MS = config.chargerCacheTtlMs; + + // -------------------- + // DB helpers + // -------------------- + async function getChargerByMqttTopic(mqttTopic) { + const cached = chargerCache.get(mqttTopic); + const now = Date.now(); + + if (cached && now - cached.fetchedAt < CHARGER_CACHE_TTL_MS) { + return cached.charger; + } + + const charger = await db('chargers').where({ mqtt_topic: mqttTopic }).first(); + if (charger) chargerCache.set(mqttTopic, { charger, fetchedAt: now }); + return charger; + } + + async function updateChargerDbIfChanged(chargerId, partialUpdate) { + const prev = lastDbStateByChargerId.get(chargerId) || null; + + const cleanPartial = stripUndef(partialUpdate); + const next = { ...(prev || {}), ...cleanPartial }; + + const changed = !prev || !shallowEqual(prev, next); + if (!changed) return false; + + await db('chargers').where({ id: chargerId }).update(cleanPartial); + lastDbStateByChargerId.set(chargerId, next); + return true; + } + + // ctx é o “contrato” que os handlers usam + return { + db, + config, + emitter, + sendPushToUser, + + // helpers + toNum, + round1, + round2, + wToKw2, + toArr3, + toArr3Kw2, + safeJsonParse, + shallowEqual, + inferStateCode, + getStatusFromStateCode, + stripUndef, + + // caches + lastDbStateByChargerId, + lastMetaByChargerId, + lastTotalEnergyByChargerId, + sessionStartEnergyByChargerId, + lastEnabled, + + // db helpers + getChargerByMqttTopic, + updateChargerDbIfChanged, + }; +} + +module.exports = { createMqttContext }; + +// === Fim de: ./src/mqtt/context.js === + + +// === Início de: ./src/mqtt/handlers/legacy.handler.js === +// src/mqtt/handlers/legacy.handler.js +async function handleLegacyState(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) { + console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); + return; + } + + const chargerId = charger.id; + const now = new Date(); + + const stateCode = ctx.inferStateCode(payload?.state); + if (!stateCode) return; + + const status = ctx.getStatusFromStateCode(stateCode); + ctx.lastMetaByChargerId.set(chargerId, { status, stateCode, rawStatus: payload?.state }); + + // legacy arrays (assumimos W -> converter p/ kW) + const powerArrKw = ctx.toArr3Kw2(payload?.power); + const voltageArr = ctx.toArr3(payload?.voltage); + const currentArr = ctx.toArr3(payload?.current); + + const consumption = ctx.round2(payload?.consumption); + const chargingTime = ctx.round1(payload?.chargingTime ?? payload?.sessionTime); + + const dbUpdate = { + status, + charging_current: currentArr[0], + consumption, + charging_time: chargingTime, + + power_l1: powerArrKw[0], + power_l2: powerArrKw[1], + power_l3: powerArrKw[2], + + voltage_l1: voltageArr[0], + voltage_l2: voltageArr[1], + voltage_l3: voltageArr[2], + + current_l1: currentArr[0], + current_l2: currentArr[1], + current_l3: currentArr[2], + + updated_at: now.toISOString(), + }; + + await ctx.updateChargerDbIfChanged(chargerId, dbUpdate); + + ctx.emitter.emit('charging-status', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + status, + stateCode, + consumption, + chargingTime, + power: powerArrKw, + voltage: voltageArr, + current: currentArr, + raw: payload, + }); +} + +async function handleLegacyConfigResponse(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + const configData = { + charger_id: charger.id, + max_charging_current: payload?.maxChargingCurrent || 32, + require_auth: !!payload?.requireAuth, + rcm_enabled: !!payload?.rcm, + temperature_limit: payload?.temperatureThreshold || 60, + config_received_at: new Date().toISOString(), + }; + + const existingConfig = await ctx.db('charger_configs').where({ charger_id: charger.id }).first(); + + if (existingConfig) { + await ctx.db('charger_configs').where({ charger_id: charger.id }).update(configData); + } else { + await ctx.db('charger_configs').insert(configData); + } + + ctx.emitter.emit('charging-config', { + ...configData, + mqtt_topic: mqttTopic, + raw: payload, + }); +} + +module.exports = { handleLegacyState, handleLegacyConfigResponse }; + +// === Fim de: ./src/mqtt/handlers/legacy.handler.js === + + +// === Início de: ./src/mqtt/handlers/evse.handler.js === +// src/mqtt/handlers/evse.handler.js +async function handleStateEvse(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) { + console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); + return; + } + + const chargerId = charger.id; + const now = new Date(); + + const rawStatus = payload?.status || payload?.state || ''; + const stateCode = ctx.inferStateCode(rawStatus); + if (!stateCode) { + console.warn(`[MQTT] state/evse sem stateCode válido (charger ${chargerId})`); + return; + } + + const status = ctx.getStatusFromStateCode(stateCode); + ctx.lastMetaByChargerId.set(chargerId, { status, stateCode, rawStatus }); + + const isCharging = stateCode === 'C2'; + + // do firmware: chargers[0].current / power (W) + const ch0 = Array.isArray(payload?.chargers) ? payload.chargers[0] : null; + + // ⚠️ current no state/evse costuma ser limite (mesmo sem estar a carregar) + const currentA = isCharging ? ctx.round1(ch0?.current) : 0; + + // ⚠️ power no state/evse vem em W -> guardar kW (e só quando está a carregar) + const powerKw = isCharging ? ctx.wToKw2(ch0?.power) : 0; + + const dbUpdate = { + status, + charging_current: currentA, + power_l1: powerKw, + power_l2: 0, + power_l3: 0, + current_l1: currentA, + current_l2: 0, + current_l3: 0, + updated_at: now.toISOString(), + }; + + await ctx.updateChargerDbIfChanged(chargerId, dbUpdate); + + // Sessões start/stop baseado em C2 + const previouslyEnabled = ctx.lastEnabled[chargerId] || false; + const currentlyEnabled = isCharging; + + if (!previouslyEnabled && currentlyEnabled) { + const startEnergy = ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0; + ctx.sessionStartEnergyByChargerId.set(chargerId, startEnergy); + + const activeSession = await ctx.db('charger_sessions') + .where({ charger_id: chargerId }) + .whereNull('ended_at') + .first(); + + if (!activeSession) { + await ctx.db('charger_sessions').insert({ + charger_id: chargerId, + started_at: now, + kwh: 0, + }); + console.log(`[DB] Sessão iniciada para charger ID ${chargerId}`); + } + } + + if (previouslyEnabled && !currentlyEnabled) { + const session = await ctx.db('charger_sessions') + .where({ charger_id: chargerId }) + .whereNull('ended_at') + .first(); + + if (session) { + const endEnergy = ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0; + const startEnergy = ctx.sessionStartEnergyByChargerId.get(chargerId) ?? 0; + const delta = Math.max(0, ctx.round2(endEnergy - startEnergy)); + + await ctx.db('charger_sessions') + .where({ id: session.id }) + .update({ ended_at: now, kwh: delta }); + + console.log(`[DB] Sessão finalizada para charger ID ${chargerId} (kWh=${delta})`); + } + + ctx.sessionStartEnergyByChargerId.delete(chargerId); + + await ctx.sendPushToUser(charger.user_id, { + title: '✅ Carregamento concluído', + body: `${charger.location || 'Carregador'} terminou o carregamento.`, + url: `/history`, + }); + } + + ctx.lastEnabled[chargerId] = currentlyEnabled; + + ctx.emitter.emit('charging-status', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + status, + stateCode, + consumption: ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0, + chargingTime: ctx.toNum(payload?.chargingTime) || 0, + power: [powerKw, 0, 0], + voltage: [0, 0, 0], + current: [currentA, 0, 0], + raw: payload, + }); + + if (status === '⚠️ Fault' || status === '❌ CP Error') { + await ctx.sendPushToUser(charger.user_id, { + title: '⚠️ Erro no carregador', + body: `${charger.location || 'Carregador'} entrou em falha.`, + url: `/charger/${charger.id}`, + }); + } +} + +async function handleStateScheduler(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('scheduler-state', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +async function handleStateLoadbalancing(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('loadbalancing-state', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +async function handleStateAuth(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('auth-state', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +async function handleStateMetersConfig(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('meters-config', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +module.exports = { + handleStateEvse, + handleStateScheduler, + handleStateLoadbalancing, + handleStateAuth, + handleStateMetersConfig, +}; + +// === Fim de: ./src/mqtt/handlers/evse.handler.js === + + +// === Início de: ./src/mqtt/handlers/meter.handler.js === +// src/mqtt/handlers/meter.handler.js +async function handleStateMeter(ctx, mqttTopic, payload, meterKind /* 'evse'|'grid' */) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) { + console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); + return; + } + + const chargerId = charger.id; + const now = new Date(); + + const vrms = ctx.toArr3(payload?.vrms); + const irms = ctx.toArr3(payload?.irms); + + // ✅ watt vem em W -> guardar kW no DB + const wattKw = ctx.toArr3Kw2(payload?.watt); + + const totalEnergy = ctx.round2(payload?.totalEnergy); // acumulado (kWh) + const source = String(payload?.source || meterKind || '').toUpperCase(); + + if (Number.isFinite(totalEnergy) && totalEnergy >= 0) { + ctx.lastTotalEnergyByChargerId.set(chargerId, totalEnergy); + } + + // só gravamos métricas no DB para EVSE (normalmente o que interessa) + if (meterKind === 'evse' || source === 'EVSE') { + const dbUpdate = { + consumption: totalEnergy, + charging_current: irms[0], + + power_l1: wattKw[0], + power_l2: wattKw[1], + power_l3: wattKw[2], + + voltage_l1: vrms[0], + voltage_l2: vrms[1], + voltage_l3: vrms[2], + + current_l1: irms[0], + current_l2: irms[1], + current_l3: irms[2], + + updated_at: now.toISOString(), }; - if (existing) { - await trx('charger_configs') - .where({ charger_id: id }) - .update(data); - } else { - await trx('charger_configs') - .insert({ charger_id: id, ...data }); + await ctx.updateChargerDbIfChanged(chargerId, dbUpdate); + + // atualiza sessão ativa com delta (sem fechar) + const currentlyEnabled = ctx.lastEnabled[chargerId] || false; + if (currentlyEnabled) { + const session = await ctx.db('charger_sessions') + .where({ charger_id: chargerId }) + .whereNull('ended_at') + .first(); + + if (session) { + const startEnergy = ctx.sessionStartEnergyByChargerId.get(chargerId) ?? 0; + const delta = Math.max( + 0, + ctx.round2((ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0) - startEnergy) + ); + + await ctx.db('charger_sessions').where({ id: session.id }).update({ kwh: delta }); + } } - } - await trx.commit(); - res.json({ success: true, data: updatedCharger }); - } catch (err) { - await trx.rollback(); - console.error('Erro ao atualizar carregador:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); - } - } -); - -// DELETE /api/chargers/:id -router.delete( - '/:id', - [param('id').isUUID()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - try { - await axios.post( - 'http://192.168.1.110:7000/client/delete', - { - client_name: charger.mqtt_user, - chargeID: charger.mqtt_topic, - }, - { timeout: 5000 } - ); - } catch (err) { - console.error( - 'Erro ao deletar cliente Mosquitto:', - err?.response?.data || err.message - ); - } - - await db('chargers').where({ id }).del(); - res.json({ success: true, message: 'Carregador excluído com sucesso' }); - } catch (err) { - console.error('Erro ao excluir carregador:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); - } - } -); - -// PUT /api/chargers/:id/config -router.put( - '/:id/config', - [param('id').isUUID(), body('config').isObject()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const { config } = req.body; - const userId = req.user.id; - - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res.status(404).json({ - success: false, - message: 'Charger not found or unauthorized', + const meta = ctx.lastMetaByChargerId.get(chargerId) || { status: '—', stateCode: undefined }; + ctx.emitter.emit('charging-status', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + status: meta.status, + stateCode: meta.stateCode, + consumption: totalEnergy, + chargingTime: 0, + power: wattKw, + voltage: vrms, + current: irms, + raw: payload, }); - } - - // lê config atual - const existing = await db('charger_configs') - .where({ charger_id: id }) - .first(); - - // throttle simples por charger - const nowMs = Date.now(); - const lastMs = lastConfigUpdateAt.get(id) || 0; - const tooSoon = nowMs - lastMs < 800; - - // se veio max_charging_current, normaliza/clampa - let safeConfig = { ...config }; - if (safeConfig.max_charging_current !== undefined) { - const safeAmp = Math.max(6, Math.min(Number(safeConfig.max_charging_current), 64)); - safeConfig.max_charging_current = safeAmp; - } - - const onlyAmp = - Object.keys(safeConfig).length === 1 && - safeConfig.max_charging_current !== undefined; - - // dedupe: mesmo valor -> responde ok sem update nem mqtt - if ( - existing && - onlyAmp && - existing.max_charging_current === safeConfig.max_charging_current - ) { - return res.json({ success: true, data: existing, message: 'Config unchanged' }); - } - - // se é update muito rápido e igual ao que já está, ignora - if (tooSoon && existing && onlyAmp) { - return res.json({ success: true, data: existing, message: 'Throttled' }); - } - - // ✅ upsert - let updatedConfig; - if (existing) { - [updatedConfig] = await db('charger_configs') - .where({ charger_id: id }) - .update({ - ...safeConfig, - config_received_at: new Date().toISOString(), - }) - .returning('*'); - } else { - [updatedConfig] = await db('charger_configs') - .insert({ - charger_id: id, - ...safeConfig, - config_received_at: new Date().toISOString(), - }) - .returning('*'); - } - - lastConfigUpdateAt.set(id, nowMs); - - const keyMap = { - max_charging_current: 'maxChargingCurrent', - require_auth: 'requireAuth', - temperature_limit: 'temperatureThreshold', - }; - - Object.entries(safeConfig).forEach(([dbKey, value]) => { - const publishKey = keyMap[dbKey] || dbKey; - mqttClient.sendConfig(charger.mqtt_topic, publishKey, value); - }); - - return res.json({ success: true, data: updatedConfig }); - } catch (err) { - console.error('Error updating config:', err); - return res - .status(500) - .json({ success: false, message: 'Server error' }); - } - } -); - -// GET /api/chargers/:id/schedule -router.get( - '/:id/schedule', - [param('id').isUUID()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); } - try { - const schedules = await db('charger_schedules') - .where({ charger_id: id }) - .orderBy('created_at', 'desc'); - - return res.json({ success: true, data: schedules }); - } catch (err) { - console.error('Erro ao buscar schedules:', err); - return res.json({ success: true, data: [] }); - } - } -); - -// POST /api/chargers/:id/schedule -router.post( - '/:id/schedule', - [ - param('id').isUUID(), - body('start').matches(/^\d{2}:\d{2}$/).withMessage('start inválido'), - body('end').matches(/^\d{2}:\d{2}$/).withMessage('end inválido'), - body('repeat') - .isIn(['everyday', 'weekdays', 'weekends']) - .withMessage('repeat inválido'), - ], - handleValidation, - async (req, res) => { - const { id } = req.params; - const { start, end, repeat } = req.body; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - try { - const [inserted] = await db('charger_schedules') - .insert({ - charger_id: id, - start, - end, - repeat, - created_at: new Date().toISOString(), - }) - .returning('*'); - - return res.status(201).json({ success: true, data: inserted }); - } catch (err) { - console.error('Erro ao criar schedule:', err); - return res - .status(500) - .json({ success: false, message: 'Erro ao criar schedule' }); - } - } -); - -// POST /api/chargers/:id/action -router.post( - '/:id/action', - [ - param('id').isUUID().withMessage('ID do carregador inválido'), - body('action').isIn(['start', 'stop']).withMessage('Ação inválida'), - body('ampLimit') - .optional() - .isInt({ min: 6, max: 64 }) - .withMessage('ampLimit deve estar entre 6 e 64'), - ], - handleValidation, - async (req, res) => { - const { id } = req.params; - const { action, ampLimit } = req.body; - const userId = req.user.id; - - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res.status(404).json({ - success: false, - message: 'Carregador não encontrado ou não autorizado', - }); - } - - if (ampLimit !== undefined) { - const safeAmp = Math.max(6, Math.min(Number(ampLimit), 64)); - - await db('charger_configs') - .where({ charger_id: id }) - .update({ - max_charging_current: safeAmp, - config_received_at: new Date().toISOString(), - }); - - mqttClient.sendConfig( - charger.mqtt_topic, - 'maxChargingCurrent', - safeAmp - ); - } - - const enable = action === 'start'; - mqttClient.sendEnable(charger.mqtt_topic, enable); - - return res.json({ - success: true, - message: `Comando '${action}' enviado com sucesso`, - }); - } catch (err) { - console.error( - `Erro ao processar ação '${action}' para carregador ${id}:`, - err - ); - return res - .status(500) - .json({ success: false, message: 'Erro ao processar ação' }); - } - } -); - -module.exports = router; - -// === Fim de: ./routes/chargers.js === - - -// === Início de: ./middleware/verifyToken.js === -const jwt = require('jsonwebtoken'); - -if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET não definido no .env'); + ctx.emitter.emit('meter-live', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + meter: meterKind, + vrms, + irms, + watt: wattKw, + totalEnergy, + raw: payload, + updated_at: now.toISOString(), + }); } -function verifyToken(req, res, next) { - const authHeader = - req.headers['authorization'] || req.headers['Authorization']; +module.exports = { handleStateMeter }; - if (!authHeader) { - return res.status(403).json({ error: 'Token não fornecido' }); - } +// === Fim de: ./src/mqtt/handlers/meter.handler.js === - const match = authHeader.match(/^Bearer\s+(.+)$/i); - if (!match) { - return res - .status(403) - .json({ error: 'Token malformado. Use "Bearer "' }); - } - const token = match[1]; +// === Início de: ./src/config/index.js === +// src/config/index.js +require('dotenv').config(); - jwt.verify(token, process.env.JWT_SECRET, (err, payload) => { - if (err) { - if (err.name === 'TokenExpiredError') { - return res.status(403).json({ error: 'Sessão expirada' }); - } - return res.status(403).json({ error: 'Token inválido' }); - } - - if (!payload?.id) { - return res.status(403).json({ error: 'Token inválido' }); - } - - req.user = payload; - next(); - }); +function must(name) { + const v = process.env[name]; + if (!v) throw new Error(`${name} não definido no .env`); + return v; } -module.exports = verifyToken; +const config = { + env: process.env.NODE_ENV || 'development', -// === Fim de: ./middleware/verifyToken.js === + port: Number(process.env.PORT || 4000), + + jwtSecret: must('JWT_SECRET'), + + corsOrigins: (process.env.CORS_ORIGIN + ? process.env.CORS_ORIGIN.split(',').map((s) => s.trim()) + : ['http://localhost:5173'] + ).filter(Boolean), + + mqtt: { + url: process.env.MQTT_URL || 'mqtt://localhost:1883', + user: process.env.MQTT_USER || 'admin', + pass: process.env.MQTT_PASS || '123QWEasd', + subTopics: (process.env.MQTT_SUB_TOPICS || '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }, + + chargerOfflineMinutes: Number(process.env.CHARGER_OFFLINE_MINUTES || 5), + chargerCacheTtlMs: Number(process.env.CHARGER_CACHE_TTL_MS || 30000), + + vapid: { + publicKey: process.env.VAPID_PUBLIC_KEY || '', + privateKey: process.env.VAPID_PRIVATE_KEY || '', + subject: process.env.VAPID_SUBJECT || 'mailto:admin@evstation.local', + }, +}; + +module.exports = config; + +// === Fim de: ./src/config/index.js === + + +// === Início de: ./knexfile.js === +// knexfile.js +require('dotenv').config(); + +function must(name) { + const v = process.env[name]; + if (!v) throw new Error(`${name} não definido no .env`); + return v; +} + +const shared = { + client: 'pg', + migrations: { + directory: './src/db/migrations', + }, +}; + +function buildConnectionFromEnv() { + // Se houver DATABASE_URL, usa-o. + // Se PGSSL=true, aplica ssl no formato esperado pelo driver pg (dentro de connection). + const ssl = + process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined; + + if (process.env.DATABASE_URL) { + // knex aceita string, mas o ssl precisa estar no objeto: + return ssl + ? { connectionString: process.env.DATABASE_URL, ssl } + : process.env.DATABASE_URL; + } + + // fallback para vars soltas + return { + host: process.env.PGHOST || '127.0.0.1', + port: Number(process.env.PGPORT || 5432), + user: process.env.PGUSER || 'postgres', + password: process.env.PGPASSWORD || 'postgres', + database: process.env.PGDATABASE || 'evse', + ...(ssl ? { ssl } : {}), + }; +} + +module.exports = { + development: { + ...shared, + connection: buildConnectionFromEnv(), + }, + + production: { + ...shared, + // Em produção normalmente queres obrigar DATABASE_URL (se for o teu caso): + // connection: must('DATABASE_URL'), + // Mas mantendo compatível com vars soltas: + connection: buildConnectionFromEnv(), + pool: { min: 2, max: 10 }, + }, +}; + +// === Fim de: ./knexfile.js === + + +// === Início de: ./package.json === +{ + "name": "evse-backend", + "version": "1.0.0", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js", + "migrate": "knex migrate:latest", + "rollback": "knex migrate:rollback" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.13.2", + "bcryptjs": "^3.0.2", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "express-rate-limit": "^8.2.1", + "express-validator": "^7.2.1", + "jsonwebtoken": "^9.0.2", + "knex": "^3.1.0", + "mqtt": "^5.13.1", + "pg": "^8.16.0", + "socket.io": "^4.8.1", + "web-push": "^3.6.7" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } +} + +// === Fim de: ./package.json === diff --git a/projeto_parte2.c b/projeto_parte2.c deleted file mode 100644 index 6a6335f..0000000 --- a/projeto_parte2.c +++ /dev/null @@ -1,264 +0,0 @@ - - -// === Início de: ./package.json === -{ - "name": "evse-backend", - "version": "1.0.0", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "axios": "^1.13.2", - "bcryptjs": "^3.0.2", - "dotenv": "^16.5.0", - "express": "^5.1.0", - "express-rate-limit": "^8.2.1", - "express-validator": "^7.2.1", - "jsonwebtoken": "^9.0.2", - "knex": "^3.1.0", - "mqtt": "^5.13.1", - "pg": "^8.16.0", - "socket.io": "^4.8.1", - "web-push": "^3.6.7" - }, - "devDependencies": { - "nodemon": "^3.1.10" - } -} - -// === Fim de: ./package.json === - - -// === Início de: ./server.js === -// server.js -const http = require('http'); -const { Server } = require('socket.io'); -const app = require('./app'); -const db = require('./db'); -const jwt = require('jsonwebtoken'); -require('dotenv').config(); - -if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET não definido no .env'); -} - -if (!process.env.MQTT_URL) { - console.warn('Warning: MQTT_URL is not defined.'); -} - -const server = http.createServer(app); - -const origins = process.env.CORS_ORIGIN - ? process.env.CORS_ORIGIN.split(',').map((s) => s.trim()) - : ['http://localhost:5173']; - -const io = new Server(server, { - cors: { - origin: origins, - methods: ['GET', 'POST'], - credentials: true, - }, -}); - -const { on } = require('./mqtt/client'); -console.log('MQTT client initialized.'); - -// --------------------------- -// Helpers de normalização -// --------------------------- -const toNum = (v) => { - if (v === null || v === undefined || v === '') return 0; - const n = typeof v === 'number' ? v : parseFloat(v); - return Number.isFinite(n) ? n : 0; -}; - -const toArr3 = (v) => { - if (Array.isArray(v)) { - return [toNum(v[0]), toNum(v[1]), toNum(v[2])]; - } - // se vier como objeto {l1,l2,l3} - if (v && typeof v === 'object') { - return [toNum(v.l1), toNum(v.l2), toNum(v.l3)]; - } - return [0, 0, 0]; -}; - -const normalizeStatus = (rawStatus) => { - const s = String(rawStatus || '').toLowerCase(); - - if (s.includes('charging')) return '⚡ Charging'; - if (s.includes('ready')) return '🟢 Ready'; - if (s.includes('fault') || s.includes('error')) return '⚠️ Fault'; - if (s.includes('wait')) return '⚡ Wait'; - if (s.includes('not conn') || s.includes('disconnected')) return '🔌 Not Conn.'; - if (s.includes('vent')) return '💨 Vent'; - - // fallback: devolve string original se não bater em nada - return rawStatus || '—'; -}; - -// Normaliza eventos de status (realtime) -function normalizeChargingStatus(data = {}) { - const chargerId = data.charger_id || data.chargerId || data.id; - - const powerArr = toArr3(data.power || data.raw?.power); - const voltageArr = toArr3(data.voltage || data.raw?.voltage); - const currentArr = toArr3(data.current || data.raw?.current); - - const status = - normalizeStatus(data.status || data.state || data.raw?.state); - - const chargingTime = - toNum(data.charging_time) || - toNum(data.chargingTime) || - toNum(data.raw?.chargingTime) || - toNum(data.raw?.sessionTime); - - const consumption = - toNum(data.consumption) || - toNum(data.raw?.consumption); - - const chargingCurrent = - toNum(data.charging_current) || - toNum(data.chargingCurrent) || - currentArr[0]; - - return { - charger_id: chargerId, - mqtt_topic: data.mqtt_topic || data.mqttTopic, - - status, - stateCode: data.stateCode || data.raw?.stateCode || undefined, - - consumption, - charging_time: chargingTime, - charging_current: chargingCurrent, - - power: powerArr, - voltage: voltageArr, - current: currentArr, - - raw: data.raw || data, // mantém raw p/ debug, mas já limpinho - updated_at: new Date().toISOString(), - }; -} - -// Normaliza eventos de config (quando o carregador manda config) -function normalizeChargingConfig(data = {}) { - const chargerId = data.charger_id || data.chargerId || data.id; - - // se vierem chaves diferentes, mapeia - const cfg = data.config || data.raw?.config || data; - - return { - charger_id: chargerId, - mqtt_topic: data.mqtt_topic || data.mqttTopic, - config: { - max_charging_current: - cfg.max_charging_current ?? - cfg.maxChargingCurrent ?? - cfg.max_current ?? - cfg.maxCurrent ?? - undefined, - - require_auth: - cfg.require_auth ?? - cfg.requireAuth ?? - undefined, - - rcm_enabled: - cfg.rcm_enabled ?? - cfg.rcmEnabled ?? - undefined, - - temperature_limit: - cfg.temperature_limit ?? - cfg.temperatureThreshold ?? - cfg.temp_limit ?? - undefined, - }, - raw: data.raw || data, - updated_at: new Date().toISOString(), - }; -} - -// --------------------------- -// auth middleware do socket -// --------------------------- -io.use((socket, next) => { - const token = socket.handshake.auth.token; - if (!token) return next(new Error('Authentication error: token required')); - - try { - const payload = jwt.verify(token, process.env.JWT_SECRET); - socket.user = payload; - next(); - } catch (err) { - next(new Error('Authentication error')); - } -}); - -io.on('connection', (socket) => { - console.log(`Client connected: ${socket.id}, user: ${socket.user.username}`); - - // join rooms apenas do user autenticado - socket.on('joinChargers', async (chargerIds = []) => { - try { - if (!Array.isArray(chargerIds) || chargerIds.length === 0) return; - - const rows = await db('chargers') - .whereIn('id', chargerIds) - .andWhere({ user_id: socket.user.id }) - .select('id'); - - const allowed = rows.map((r) => r.id); - allowed.forEach((id) => socket.join(id)); - - console.log(`Socket ${socket.id} joined chargers: ${allowed}`); - } catch (err) { - console.error('joinChargers error:', err); - } - }); - - socket.on('charger-action', ({ chargerId, action, ampLimit }) => { - console.log( - `Received action "${action}" for charger ${chargerId} by user ${socket.user.id}` - ); - io.to(chargerId).emit('charger-action-status', 'success'); - }); - - socket.on('disconnect', (reason) => { - console.log(`Client disconnected: ${socket.id}, reason: ${reason}`); - }); -}); - -// --------------------------- -// Relay MQTT -> Socket.IO (NORMALIZADO) -// --------------------------- -on('charging-status', (data) => { - const normalized = normalizeChargingStatus(data); - const chargerId = normalized.charger_id; - if (!chargerId) return; - - io.to(chargerId).emit('charging-status', normalized); -}); - -on('charging-config', (data) => { - const normalized = normalizeChargingConfig(data); - const chargerId = normalized.charger_id; - if (!chargerId) return; - - io.to(chargerId).emit('charging-config', normalized); -}); - -const PORT = process.env.PORT || 4000; -server.listen(PORT, () => { - console.log(`Server listening on http://localhost:${PORT}`); -}); - -// === Fim de: ./server.js === diff --git a/read_project.py b/read_project.py index 5db3ffd..e70e9d9 100755 --- a/read_project.py +++ b/read_project.py @@ -1,6 +1,6 @@ import os -TAMANHO_MAX = 31000 # Limite por arquivo +TAMANHO_MAX = 100000 # Limite por arquivo EXCLUIR_PASTAS = {"node_modules", "dist", "build", ".git", ".vite"} @@ -54,14 +54,14 @@ def main(): diretorio_base = "." # Subpastas principais que queremos incluir (se existirem) - componentes_escolhidos = ["src", "public", "config", "utils", "routes", "middleware"] + componentes_escolhidos = ["src", "public", "config", "utils", "routes", "middleware", "mqtt", "migrations"] diretorios_para_incluir = [os.path.join(diretorio_base, nome) for nome in componentes_escolhidos if os.path.exists(os.path.join(diretorio_base, nome))] # Arquivos individuais importantes na raiz do projeto arquivos_extras = [] - for nome in ["vite.config.js", "vite.config.ts", "package.json", "tsconfig.json", "server.js"]: + for nome in ["vite.config.js", "vite.config.ts", "knexfile.js" , "package.json", "tsconfig.json", "server.js"]: caminho = os.path.join(diretorio_base, nome) if os.path.isfile(caminho): arquivos_extras.append(caminho) diff --git a/routes/charger_sessions.js b/routes/charger_sessions.js deleted file mode 100755 index 2c6ed94..0000000 --- a/routes/charger_sessions.js +++ /dev/null @@ -1,237 +0,0 @@ -// routes/charger_sessions.js -const express = require('express'); -const { param, query, body, validationResult } = require('express-validator'); -const router = express.Router(); -const verifyToken = require('../middleware/verifyToken'); -const db = require('../db'); - -router.use(verifyToken); - -function handleValidation(req, res, next) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, errors: errors.array() }); - } - next(); -} - -// GET /api/charger_sessions?chargerId=... -router.get( - '/', - [query('chargerId').isUUID().withMessage('chargerId deve ser UUID válido')], - handleValidation, - async (req, res) => { - const { chargerId } = req.query; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id: chargerId, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(403) - .json({ success: false, message: 'Acesso não autorizado' }); - } - - const sessions = await db('charger_sessions') - .where({ charger_id: chargerId }) - .orderBy('started_at', 'desc'); - - res.json({ success: true, data: sessions }); - } -); - -// GET /api/charger_sessions/:id -router.get( - '/:id', - [param('id').isUUID().withMessage('ID de sessão inválido')], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - const session = await db('charger_sessions') - .join('chargers', 'charger_sessions.charger_id', 'chargers.id') - .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) - .select('charger_sessions.*') - .first(); - - if (!session) { - return res - .status(404) - .json({ success: false, message: 'Sessão não encontrada' }); - } - - res.json({ success: true, data: session }); - } -); - -// POST /api/charger_sessions -router.post( - '/', - [body('charger_id').isUUID().withMessage('charger_id deve ser UUID válido')], - handleValidation, - async (req, res) => { - const { charger_id } = req.body; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id: charger_id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(403) - .json({ success: false, message: 'Acesso não autorizado' }); - } - - const [inserted] = await db('charger_sessions') - .insert({ charger_id, started_at: new Date() }) - .returning('*'); - - res.status(201).json({ success: true, data: inserted }); - } -); - -// PUT /api/charger_sessions/:id -router.put( - '/:id', - [ - param('id').isUUID().withMessage('ID de sessão inválido'), - body('ended_at').optional().isISO8601().toDate(), - body('kwh').optional().isFloat({ min: 0 }), - body('cost').optional().isFloat({ min: 0 }), - ], - handleValidation, - async (req, res) => { - const { id } = req.params; - const { ended_at, kwh, cost } = req.body; - const userId = req.user.id; - - const session = await db('charger_sessions') - .join('chargers', 'charger_sessions.charger_id', 'chargers.id') - .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) - .first(); - - if (!session) { - return res - .status(404) - .json({ success: false, message: 'Sessão não encontrada' }); - } - - const [updated] = await db('charger_sessions') - .where({ id }) - .update({ ended_at, kwh, cost }) - .returning('*'); - - res.json({ success: true, data: updated }); - } -); - -// DELETE /api/charger_sessions/:id -router.delete( - '/:id', - [param('id').isUUID().withMessage('ID de sessão inválido')], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - const deleted = await db('charger_sessions') - .join('chargers', 'charger_sessions.charger_id', 'chargers.id') - .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) - .del(); - - if (!deleted) { - return res - .status(404) - .json({ success: false, message: 'Sessão não encontrada' }); - } - - res.json({ success: true, message: 'Sessão excluída com sucesso' }); - } -); - -// GET /api/charger_sessions/history/:chargerId -router.get( - '/history/:chargerId', - [ - param('chargerId').isUUID().withMessage('chargerId deve ser UUID válido'), - query('viewMode') - .isIn(['Day', 'Week', 'Month']) - .withMessage('viewMode inválido'), - ], - handleValidation, - async (req, res) => { - const { chargerId } = req.params; - const { viewMode } = req.query; - const userId = req.user.id; - - // ownership check - const charger = await db('chargers') - .where({ id: chargerId, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(403) - .json({ success: false, message: 'Acesso não autorizado' }); - } - - let qb = db('charger_sessions') - .where({ charger_id: chargerId }) - .sum('kwh as total_kwh'); - - switch (viewMode) { - case 'Day': - qb = qb - .select(db.raw('DATE(started_at) AS period')) - .groupBy(db.raw('DATE(started_at)')) - .orderBy('period', 'desc'); - break; - - case 'Week': - qb = qb - .select( - db.raw('EXTRACT(ISOYEAR FROM started_at) AS y'), - db.raw('EXTRACT(WEEK FROM started_at) AS w'), - db.raw( - "EXTRACT(ISOYEAR FROM started_at)||'-'||LPAD(EXTRACT(WEEK FROM started_at)::text,2,'0') AS period" - ) - ) - .groupBy('y', 'w') - .orderBy([{ column: 'y', order: 'desc' }, { column: 'w', order: 'desc' }]); - break; - - case 'Month': - qb = qb - .select( - db.raw('EXTRACT(YEAR FROM started_at) AS y'), - db.raw('EXTRACT(MONTH FROM started_at) AS m'), - db.raw( - "EXTRACT(YEAR FROM started_at)||'-'||LPAD(EXTRACT(MONTH FROM started_at)::text,2,'0') AS period" - ) - ) - .groupBy('y', 'm') - .orderBy([{ column: 'y', order: 'desc' }, { column: 'm', order: 'desc' }]); - break; - } - - const rows = await qb; - - // ✅ devolve lista vazia em vez de 404 - if (!rows.length) { - return res.json({ success: true, data: [] }); - } - - const data = rows.map((r) => ({ - started_at: r.period, - kwh: parseFloat(r.total_kwh) || 0, - })); - - res.json({ success: true, data }); - } -); - -module.exports = router; diff --git a/routes/chargers.js b/routes/chargers.js deleted file mode 100755 index 5afa3a4..0000000 --- a/routes/chargers.js +++ /dev/null @@ -1,575 +0,0 @@ -// routes/chargers.js -const express = require('express'); -const { body, param, validationResult } = require('express-validator'); -const router = express.Router(); -const verifyToken = require('../middleware/verifyToken'); -const db = require('../db'); -const crypto = require('crypto'); -const axios = require('axios'); -const mqttClient = require('../mqtt/client'); - -router.use(verifyToken); - -function handleValidation(req, res, next) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, errors: errors.array() }); - } - next(); -} - -// throttling simples em memória (por charger) -const lastConfigUpdateAt = new Map(); - -// GET /api/chargers -router.get('/', async (req, res) => { - const userId = req.user.id; - try { - const chargers = await db('chargers') - .where({ user_id: userId }) - .select('*'); - res.json({ success: true, data: chargers }); - } catch (err) { - console.error('Erro ao buscar carregadores:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); - } -}); - -// GET /api/chargers/:id -router.get( - '/:id', - [param('id').isUUID()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - let config = null; - try { - config = await db('charger_configs') - .where({ charger_id: charger.id }) - .first(); - } catch (cfgErr) { - console.error('[GET charger] erro ao buscar charger_configs:', cfgErr); - } - - if (!config) - config = { - charger_id: charger.id, - max_charging_current: 32, - require_auth: false, - rcm_enabled: false, - temperature_limit: 60, - }; - - const numericFields = [ - 'power_l1', - 'power_l2', - 'power_l3', - 'voltage_l1', - 'voltage_l2', - 'voltage_l3', - 'current_l1', - 'current_l2', - 'current_l3', - 'charging_current', - 'consumption', - ]; - - numericFields.forEach((field) => { - const v = charger[field]; - charger[field] = - v === null || v === undefined || v === '' ? 0 : Number(v); - if (Number.isNaN(charger[field])) charger[field] = 0; - }); - - return res.json({ success: true, data: { ...charger, config } }); - } catch (err) { - console.error('Erro ao buscar carregador:', err); - - return res.status(500).json({ - success: false, - message: - process.env.NODE_ENV === 'development' - ? err.message || 'Erro no servidor' - : 'Erro no servidor', - }); - } - } -); - -// POST /api/chargers -router.post( - '/', - [ - body('location') - .exists() - .withMessage('O campo location é obrigatório') - .isString() - .withMessage('Location deve ser uma string') - .isLength({ min: 1 }) - .withMessage('Location não pode estar vazio') - .trim(), - ], - handleValidation, - async (req, res) => { - const { location } = req.body; - const userId = req.user.id; - const now = new Date().toISOString(); - - let chargerID; - do { - chargerID = crypto.randomBytes(6).toString('hex'); - } while ( - await db('chargers').where({ mqtt_topic: chargerID }).first() - ); - - const mqtt_topic = chargerID; - const mqtt_user = chargerID; - const mqtt_pass = crypto.randomBytes(6).toString('hex'); - - try { - const [charger] = await db('chargers') - .insert({ - user_id: userId, - location, - status: 'offline', - charging_current: 0, - charging_time: 0, - consumption: 0, - power_l1: 0.0, - power_l2: 0.0, - power_l3: 0.0, - voltage_l1: 0.0, - voltage_l2: 0.0, - voltage_l3: 0.0, - current_l1: 0.0, - current_l2: 0.0, - current_l3: 0.0, - mqtt_user, - mqtt_pass, - mqtt_topic, - updated_at: now, - }) - .returning('*'); - - await db('charger_configs').insert({ - charger_id: charger.id, - max_charging_current: 32, - require_auth: false, - rcm_enabled: false, - temperature_limit: 60, - config_received_at: now, - }); - - try { - await axios.post( - 'http://localhost:7000/client/create', - { - client_name: charger.mqtt_user, - chargeID: charger.mqtt_topic, - password: charger.mqtt_pass, - }, - { timeout: 5000 } - ); - } catch (err) { - console.error( - 'Erro ao criar cliente Mosquitto:', - err?.response?.data || err.message - ); - } - - res.status(201).json({ success: true, data: charger }); - } catch (err) { - console.error('Erro ao criar carregador:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); - } - } -); - -// PUT /api/chargers/:id -router.put( - '/:id', - [ - param('id').isUUID(), - body('charger').optional().isObject(), - body('config').optional().isObject(), - body('location').optional().isString(), // compat front - ], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - let { charger = {}, config = {} } = req.body; - - // ✅ compatibilidade com front antigo: { location } - if (req.body.location && !charger.location) { - charger.location = req.body.location; - } - - const trx = await db.transaction(); - - const ALLOWED_CHARGER_FIELDS = ['location']; - const safeChargerUpdate = Object.fromEntries( - Object.entries(charger).filter(([k]) => - ALLOWED_CHARGER_FIELDS.includes(k) - ) - ); - - try { - let updatedCharger; - - if (Object.keys(safeChargerUpdate).length > 0) { - [updatedCharger] = await trx('chargers') - .where({ id, user_id: userId }) - .update({ - ...safeChargerUpdate, - updated_at: new Date().toISOString(), - }) - .returning('*'); - } else { - updatedCharger = await trx('chargers') - .where({ id, user_id: userId }) - .first(); - } - - if (!updatedCharger) { - await trx.rollback(); - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - if (Object.keys(config).length > 0) { - const existing = await trx('charger_configs') - .where({ charger_id: id }) - .first(); - - const data = { - ...config, - config_received_at: new Date().toISOString(), - }; - - if (existing) { - await trx('charger_configs') - .where({ charger_id: id }) - .update(data); - } else { - await trx('charger_configs') - .insert({ charger_id: id, ...data }); - } - } - - await trx.commit(); - res.json({ success: true, data: updatedCharger }); - } catch (err) { - await trx.rollback(); - console.error('Erro ao atualizar carregador:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); - } - } -); - -// DELETE /api/chargers/:id -router.delete( - '/:id', - [param('id').isUUID()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - try { - await axios.post( - 'http://localhost:7000/client/delete', - { - client_name: charger.mqtt_user, - chargeID: charger.mqtt_topic, - }, - { timeout: 5000 } - ); - } catch (err) { - console.error( - 'Erro ao deletar cliente Mosquitto:', - err?.response?.data || err.message - ); - } - - await db('chargers').where({ id }).del(); - res.json({ success: true, message: 'Carregador excluído com sucesso' }); - } catch (err) { - console.error('Erro ao excluir carregador:', err); - res.status(500).json({ success: false, message: 'Erro no servidor' }); - } - } -); - -// PUT /api/chargers/:id/config -router.put( - '/:id/config', - [param('id').isUUID(), body('config').isObject()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const { config } = req.body; - const userId = req.user.id; - - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res.status(404).json({ - success: false, - message: 'Charger not found or unauthorized', - }); - } - - // lê config atual - const existing = await db('charger_configs') - .where({ charger_id: id }) - .first(); - - // throttle simples por charger - const nowMs = Date.now(); - const lastMs = lastConfigUpdateAt.get(id) || 0; - const tooSoon = nowMs - lastMs < 800; - - // se veio max_charging_current, normaliza/clampa - let safeConfig = { ...config }; - if (safeConfig.max_charging_current !== undefined) { - const safeAmp = Math.max(6, Math.min(Number(safeConfig.max_charging_current), 64)); - safeConfig.max_charging_current = safeAmp; - } - - const onlyAmp = - Object.keys(safeConfig).length === 1 && - safeConfig.max_charging_current !== undefined; - - // dedupe: mesmo valor -> responde ok sem update nem mqtt - if ( - existing && - onlyAmp && - existing.max_charging_current === safeConfig.max_charging_current - ) { - return res.json({ success: true, data: existing, message: 'Config unchanged' }); - } - - // se é update muito rápido e igual ao que já está, ignora - if (tooSoon && existing && onlyAmp) { - return res.json({ success: true, data: existing, message: 'Throttled' }); - } - - // ✅ upsert - let updatedConfig; - if (existing) { - [updatedConfig] = await db('charger_configs') - .where({ charger_id: id }) - .update({ - ...safeConfig, - config_received_at: new Date().toISOString(), - }) - .returning('*'); - } else { - [updatedConfig] = await db('charger_configs') - .insert({ - charger_id: id, - ...safeConfig, - config_received_at: new Date().toISOString(), - }) - .returning('*'); - } - - lastConfigUpdateAt.set(id, nowMs); - - const keyMap = { - max_charging_current: 'maxChargingCurrent', - require_auth: 'requireAuth', - temperature_limit: 'temperatureThreshold', - }; - - Object.entries(safeConfig).forEach(([dbKey, value]) => { - const publishKey = keyMap[dbKey] || dbKey; - mqttClient.sendConfig(charger.mqtt_topic, publishKey, value); - }); - - return res.json({ success: true, data: updatedConfig }); - } catch (err) { - console.error('Error updating config:', err); - return res - .status(500) - .json({ success: false, message: 'Server error' }); - } - } -); - -// GET /api/chargers/:id/schedule -router.get( - '/:id/schedule', - [param('id').isUUID()], - handleValidation, - async (req, res) => { - const { id } = req.params; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - try { - const schedules = await db('charger_schedules') - .where({ charger_id: id }) - .orderBy('created_at', 'desc'); - - return res.json({ success: true, data: schedules }); - } catch (err) { - console.error('Erro ao buscar schedules:', err); - return res.json({ success: true, data: [] }); - } - } -); - -// POST /api/chargers/:id/schedule -router.post( - '/:id/schedule', - [ - param('id').isUUID(), - body('start').matches(/^\d{2}:\d{2}$/).withMessage('start inválido'), - body('end').matches(/^\d{2}:\d{2}$/).withMessage('end inválido'), - body('repeat') - .isIn(['everyday', 'weekdays', 'weekends']) - .withMessage('repeat inválido'), - ], - handleValidation, - async (req, res) => { - const { id } = req.params; - const { start, end, repeat } = req.body; - const userId = req.user.id; - - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res - .status(404) - .json({ success: false, message: 'Carregador não encontrado' }); - } - - try { - const [inserted] = await db('charger_schedules') - .insert({ - charger_id: id, - start, - end, - repeat, - created_at: new Date().toISOString(), - }) - .returning('*'); - - return res.status(201).json({ success: true, data: inserted }); - } catch (err) { - console.error('Erro ao criar schedule:', err); - return res - .status(500) - .json({ success: false, message: 'Erro ao criar schedule' }); - } - } -); - -// POST /api/chargers/:id/action -router.post( - '/:id/action', - [ - param('id').isUUID().withMessage('ID do carregador inválido'), - body('action').isIn(['start', 'stop']).withMessage('Ação inválida'), - body('ampLimit') - .optional() - .isInt({ min: 6, max: 64 }) - .withMessage('ampLimit deve estar entre 6 e 64'), - ], - handleValidation, - async (req, res) => { - const { id } = req.params; - const { action, ampLimit } = req.body; - const userId = req.user.id; - - try { - const charger = await db('chargers') - .where({ id, user_id: userId }) - .first(); - - if (!charger) { - return res.status(404).json({ - success: false, - message: 'Carregador não encontrado ou não autorizado', - }); - } - - if (ampLimit !== undefined) { - const safeAmp = Math.max(6, Math.min(Number(ampLimit), 64)); - - await db('charger_configs') - .where({ charger_id: id }) - .update({ - max_charging_current: safeAmp, - config_received_at: new Date().toISOString(), - }); - - mqttClient.sendConfig( - charger.mqtt_topic, - 'maxChargingCurrent', - safeAmp - ); - } - - const enable = action === 'start'; - mqttClient.sendEnable(charger.mqtt_topic, enable); - - return res.json({ - success: true, - message: `Comando '${action}' enviado com sucesso`, - }); - } catch (err) { - console.error( - `Erro ao processar ação '${action}' para carregador ${id}:`, - err - ); - return res - .status(500) - .json({ success: false, message: 'Erro ao processar ação' }); - } - } -); - -module.exports = router; diff --git a/routes/push.js b/routes/push.js deleted file mode 100644 index 070832b..0000000 --- a/routes/push.js +++ /dev/null @@ -1,101 +0,0 @@ -// routes/push.js -const express = require('express'); -const { body, validationResult } = require('express-validator'); -const verifyToken = require('../middleware/verifyToken'); -const db = require('../db'); -const { sendPushToUser } = require('../utils/pushService'); - -const router = express.Router(); -router.use(verifyToken); - -function handleValidation(req, res, next) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ success: false, errors: errors.array() }); - } - next(); -} - -// GET /api/push/vapid-public-key -router.get('/vapid-public-key', (req, res) => { - if (!process.env.VAPID_PUBLIC_KEY) { - return res - .status(503) - .json({ success: false, message: 'Push indisponível' }); - } - res.json({ success: true, data: { key: process.env.VAPID_PUBLIC_KEY } }); -}); - -// POST /api/push/subscribe -router.post( - '/subscribe', - [ - body('endpoint').isString(), - body('keys.p256dh').isString(), - body('keys.auth').isString(), - ], - handleValidation, - async (req, res) => { - const userId = req.user.id; - const { endpoint, keys } = req.body; - const ua = req.headers['user-agent'] || null; - - // evita duplicados - const existing = await db('push_subscriptions') - .where({ endpoint, user_id: userId }) - .first(); - - if (existing) { - return res.json({ success: true, data: existing }); - } - - const [inserted] = await db('push_subscriptions') - .insert({ - user_id: userId, - endpoint, - p256dh: keys.p256dh, - auth: keys.auth, - user_agent: ua, - created_at: new Date().toISOString(), - }) - .returning('*'); - - res.status(201).json({ success: true, data: inserted }); - } -); - -// POST /api/push/unsubscribe -router.post( - '/unsubscribe', - [body('endpoint').optional().isString()], - handleValidation, - async (req, res) => { - const userId = req.user.id; - const { endpoint } = req.body || {}; - - // se não houver sub no navegador, responde ok - if (!endpoint) { - return res.json({ success: true, message: 'No subscription' }); - } - - await db('push_subscriptions') - .where({ endpoint, user_id: userId }) - .del(); - - res.json({ success: true, message: 'Unsubscribed' }); - } -); - -// POST /api/push/test -router.post('/test', async (req, res) => { - const userId = req.user.id; - await sendPushToUser(userId, { - title: '📬 Teste EV Station', - body: 'Push notifications estão a funcionar!', - url: '/', - }); - - res.json({ success: true, message: 'Push enviado' }); -}); - -module.exports = router; diff --git a/routes/users.js b/routes/users.js deleted file mode 100755 index 68b9a58..0000000 --- a/routes/users.js +++ /dev/null @@ -1,110 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const db = require('../db'); -const jwt = require('jsonwebtoken'); -const bcrypt = require('bcryptjs'); -const rateLimit = require('express-rate-limit'); - -if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET não definido no .env'); -} - -// limiter só para auth -const authLimiter = rateLimit({ - windowMs: 60 * 1000, - max: 10, - standardHeaders: true, - legacyHeaders: false, -}); - -// POST /api/users/login -router.post('/login', authLimiter, async (req, res) => { - const { username, password } = req.body; - - if (!username || !password) { - return res - .status(400) - .json({ success: false, message: 'Usuário e senha são obrigatórios' }); - } - - try { - const user = await db('users').where({ username }).first(); - - if (!user) { - return res - .status(401) - .json({ success: false, message: 'Credenciais inválidas' }); - } - - const isValidPassword = await bcrypt.compare(password, user.password); - if (!isValidPassword) { - return res - .status(401) - .json({ success: false, message: 'Credenciais inválidas' }); - } - - const token = jwt.sign( - { id: user.id, username: user.username }, - process.env.JWT_SECRET, - { expiresIn: '24h' } - ); - - return res.json({ success: true, data: { token } }); - } catch (err) { - console.error('Erro ao autenticar usuário:', err); - return res - .status(500) - .json({ success: false, message: 'Erro interno do servidor' }); - } -}); - -// POST /api/users/register -router.post('/register', authLimiter, async (req, res) => { - const { username, password } = req.body; - - if ( - !username || - !password || - typeof username !== 'string' || - typeof password !== 'string' || - username.length < 3 || - password.length < 4 - ) { - return res.status(400).json({ - success: false, - message: - 'Nome de usuário deve ter pelo menos 3 caracteres e senha pelo menos 4 caracteres', - }); - } - - try { - const existing = await db('users').where({ username }).first(); - if (existing) { - return res - .status(409) - .json({ success: false, message: 'Nome de usuário já está em uso' }); - } - - const hashedPassword = await bcrypt.hash(password, 10); - - const [row] = await db('users') - .insert({ username, password: hashedPassword }) - .returning('id'); - - const token = jwt.sign( - { id: row.id, username }, - process.env.JWT_SECRET, - { expiresIn: '24h' } - ); - - return res.status(201).json({ success: true, data: { token } }); - } catch (err) { - console.error('Erro ao registrar usuário:', err); - return res.status(500).json({ - success: false, - message: 'Erro interno ao registrar usuário', - }); - } -}); - -module.exports = router; diff --git a/server.js b/server.js deleted file mode 100755 index f582175..0000000 --- a/server.js +++ /dev/null @@ -1,225 +0,0 @@ -// server.js -const http = require('http'); -const { Server } = require('socket.io'); -const app = require('./app'); -const db = require('./db'); -const jwt = require('jsonwebtoken'); -require('dotenv').config(); - -if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET não definido no .env'); -} - -if (!process.env.MQTT_URL) { - console.warn('Warning: MQTT_URL is not defined.'); -} - -const server = http.createServer(app); - -const origins = process.env.CORS_ORIGIN - ? process.env.CORS_ORIGIN.split(',').map((s) => s.trim()) - : ['http://localhost:5173']; - -const io = new Server(server, { - cors: { - origin: origins, - methods: ['GET', 'POST'], - credentials: true, - }, -}); - -const { on } = require('./mqtt/client'); -console.log('MQTT client initialized.'); - -// --------------------------- -// Helpers de normalização -// --------------------------- -const toNum = (v) => { - if (v === null || v === undefined || v === '') return 0; - const n = typeof v === 'number' ? v : parseFloat(v); - return Number.isFinite(n) ? n : 0; -}; - -const toArr3 = (v) => { - if (Array.isArray(v)) { - return [toNum(v[0]), toNum(v[1]), toNum(v[2])]; - } - // se vier como objeto {l1,l2,l3} - if (v && typeof v === 'object') { - return [toNum(v.l1), toNum(v.l2), toNum(v.l3)]; - } - return [0, 0, 0]; -}; - -const normalizeStatus = (rawStatus) => { - const s = String(rawStatus || '').toLowerCase(); - - if (s.includes('charging')) return '⚡ Charging'; - if (s.includes('ready')) return '🟢 Ready'; - if (s.includes('fault') || s.includes('error')) return '⚠️ Fault'; - if (s.includes('wait')) return '⚡ Wait'; - if (s.includes('not conn') || s.includes('disconnected')) return '🔌 Not Conn.'; - if (s.includes('vent')) return '💨 Vent'; - - // fallback: devolve string original se não bater em nada - return rawStatus || '—'; -}; - -// Normaliza eventos de status (realtime) -function normalizeChargingStatus(data = {}) { - const chargerId = data.charger_id || data.chargerId || data.id; - - const powerArr = toArr3(data.power || data.raw?.power); - const voltageArr = toArr3(data.voltage || data.raw?.voltage); - const currentArr = toArr3(data.current || data.raw?.current); - - const status = - normalizeStatus(data.status || data.state || data.raw?.state); - - const chargingTime = - toNum(data.charging_time) || - toNum(data.chargingTime) || - toNum(data.raw?.chargingTime) || - toNum(data.raw?.sessionTime); - - const consumption = - toNum(data.consumption) || - toNum(data.raw?.consumption); - - const chargingCurrent = - toNum(data.charging_current) || - toNum(data.chargingCurrent) || - currentArr[0]; - - return { - charger_id: chargerId, - mqtt_topic: data.mqtt_topic || data.mqttTopic, - - status, - stateCode: data.stateCode || data.raw?.stateCode || undefined, - - consumption, - charging_time: chargingTime, - charging_current: chargingCurrent, - - power: powerArr, - voltage: voltageArr, - current: currentArr, - - raw: data.raw || data, // mantém raw p/ debug, mas já limpinho - updated_at: new Date().toISOString(), - }; -} - -// Normaliza eventos de config (quando o carregador manda config) -function normalizeChargingConfig(data = {}) { - const chargerId = data.charger_id || data.chargerId || data.id; - - // se vierem chaves diferentes, mapeia - const cfg = data.config || data.raw?.config || data; - - return { - charger_id: chargerId, - mqtt_topic: data.mqtt_topic || data.mqttTopic, - config: { - max_charging_current: - cfg.max_charging_current ?? - cfg.maxChargingCurrent ?? - cfg.max_current ?? - cfg.maxCurrent ?? - undefined, - - require_auth: - cfg.require_auth ?? - cfg.requireAuth ?? - undefined, - - rcm_enabled: - cfg.rcm_enabled ?? - cfg.rcmEnabled ?? - undefined, - - temperature_limit: - cfg.temperature_limit ?? - cfg.temperatureThreshold ?? - cfg.temp_limit ?? - undefined, - }, - raw: data.raw || data, - updated_at: new Date().toISOString(), - }; -} - -// --------------------------- -// auth middleware do socket -// --------------------------- -io.use((socket, next) => { - const token = socket.handshake.auth.token; - if (!token) return next(new Error('Authentication error: token required')); - - try { - const payload = jwt.verify(token, process.env.JWT_SECRET); - socket.user = payload; - next(); - } catch (err) { - next(new Error('Authentication error')); - } -}); - -io.on('connection', (socket) => { - console.log(`Client connected: ${socket.id}, user: ${socket.user.username}`); - - // join rooms apenas do user autenticado - socket.on('joinChargers', async (chargerIds = []) => { - try { - if (!Array.isArray(chargerIds) || chargerIds.length === 0) return; - - const rows = await db('chargers') - .whereIn('id', chargerIds) - .andWhere({ user_id: socket.user.id }) - .select('id'); - - const allowed = rows.map((r) => r.id); - allowed.forEach((id) => socket.join(id)); - - console.log(`Socket ${socket.id} joined chargers: ${allowed}`); - } catch (err) { - console.error('joinChargers error:', err); - } - }); - - socket.on('charger-action', ({ chargerId, action, ampLimit }) => { - console.log( - `Received action "${action}" for charger ${chargerId} by user ${socket.user.id}` - ); - io.to(chargerId).emit('charger-action-status', 'success'); - }); - - socket.on('disconnect', (reason) => { - console.log(`Client disconnected: ${socket.id}, reason: ${reason}`); - }); -}); - -// --------------------------- -// Relay MQTT -> Socket.IO (NORMALIZADO) -// --------------------------- -on('charging-status', (data) => { - const normalized = normalizeChargingStatus(data); - const chargerId = normalized.charger_id; - if (!chargerId) return; - - io.to(chargerId).emit('charging-status', normalized); -}); - -on('charging-config', (data) => { - const normalized = normalizeChargingConfig(data); - const chargerId = normalized.charger_id; - if (!chargerId) return; - - io.to(chargerId).emit('charging-config', normalized); -}); - -const PORT = process.env.PORT || 4000; -server.listen(PORT, () => { - console.log(`Server listening on http://localhost:${PORT}`); -}); diff --git a/src/app.js b/src/app.js new file mode 100755 index 0000000..ff06fce --- /dev/null +++ b/src/app.js @@ -0,0 +1,76 @@ +// src/app.js +const express = require('express'); + +const config = require('./config'); +const db = require('./db/knex'); +const mqttClient = require('./mqtt'); + +const usersRouter = require('./routes/users.routes'); +const chargersRouter = require('./routes/chargers.routes'); +const sessionsRouter = require('./routes/sessions.routes'); +const pushRouter = require('./routes/push.routes'); + +const errorHandler = require('./middleware/errorHandler'); + +const app = express(); + +app.disable('x-powered-by'); + +// ✅ atrás de reverse proxy em produção (nginx/traefik/etc.) +if (config.env === 'production') { + app.set('trust proxy', 1); +} + +// body parser +app.use(express.json({ limit: '1mb' })); + +// CORS simples sem dependência extra +app.use((req, res, next) => { + const origin = req.headers.origin; + if (origin && config.corsOrigins.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + } + + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); + + if (req.method === 'OPTIONS') return res.sendStatus(204); + next(); +}); + +// liveness +app.get('/health', (req, res) => res.json({ ok: true })); + +// readiness: DB + MQTT +app.get('/ready', async (req, res) => { + try { + await db.raw('select 1 as ok'); + const mqttOk = typeof mqttClient?.isConnected === 'function' ? mqttClient.isConnected() : false; + + if (!mqttOk) { + return res.status(503).json({ ok: false, db: true, mqtt: false }); + } + + return res.json({ ok: true, db: true, mqtt: true }); + } catch (err) { + return res.status(503).json({ ok: false, db: false, mqtt: false }); + } +}); + +// routes +app.use('/api/users', usersRouter); +app.use('/api/chargers', chargersRouter); +app.use('/api/charger_sessions', sessionsRouter); +app.use('/api/push', pushRouter); + +// 404 +app.use((req, res) => { + res.status(404).json({ success: false, message: 'Not found' }); +}); + +// error handler +app.use(errorHandler); + +module.exports = app; diff --git a/src/config/index.js b/src/config/index.js new file mode 100755 index 0000000..f90c404 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,65 @@ +// src/config/index.js +require('dotenv').config(); + +function must(name) { + const v = process.env[name]; + if (!v) throw new Error(`${name} não definido no .env`); + return v; +} + +function mustInProd(name, fallback = '') { + const v = process.env[name]; + if ((process.env.NODE_ENV || 'development') === 'production') { + if (!v) throw new Error(`${name} não definido (obrigatório em production)`); + return v; + } + return v || fallback; +} + +const env = process.env.NODE_ENV || 'development'; + +const config = { + env, + + port: Number(process.env.PORT || 4000), + + // ✅ sempre obrigatório + jwtSecret: must('JWT_SECRET'), + + // ✅ em produção deve estar explícito e restrito + corsOrigins: (mustInProd('CORS_ORIGIN', 'http://localhost:5173')) + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + + mqtt: { + // ✅ em produção não usar defaults + url: mustInProd('MQTT_URL', 'mqtt://localhost:1883'), + user: mustInProd('MQTT_USER', 'admin'), + pass: mustInProd('MQTT_PASS', '123QWEasd'), + subTopics: (process.env.MQTT_SUB_TOPICS || '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }, + + chargerOfflineMinutes: Number(process.env.CHARGER_OFFLINE_MINUTES || 5), + chargerCacheTtlMs: Number(process.env.CHARGER_CACHE_TTL_MS || 30000), + + vapid: { + publicKey: process.env.VAPID_PUBLIC_KEY || '', + privateKey: process.env.VAPID_PRIVATE_KEY || '', + subject: process.env.VAPID_SUBJECT || 'mailto:admin@evstation.local', + }, + + // ✅ para remover o hardcode de localhost:7000 + mosquittoMgmt: { + baseUrl: process.env.MOSQUITTO_MGMT_URL || '', // ex: http://mosquitto-mgmt:7000 + timeoutMs: Number(process.env.MOSQUITTO_MGMT_TIMEOUT_MS || 5000), + // se quiseres no futuro: user/pass/token + // user: process.env.MOSQUITTO_MGMT_USER || '', + // pass: process.env.MOSQUITTO_MGMT_PASS || '', + }, +}; + +module.exports = config; diff --git a/src/db/knex.js b/src/db/knex.js new file mode 100755 index 0000000..378d3ad --- /dev/null +++ b/src/db/knex.js @@ -0,0 +1,10 @@ +// src/db/knex.js +const knex = require('knex'); +const path = require('path'); + +const knexfile = require(path.join(__dirname, '../../knexfile.js')); +const env = process.env.NODE_ENV || 'development'; + +const db = knex(knexfile[env] || knexfile); + +module.exports = db; diff --git a/src/db/migrations/20250618_enable_pgcrypto.js b/src/db/migrations/20250618_enable_pgcrypto.js new file mode 100644 index 0000000..2317fe2 --- /dev/null +++ b/src/db/migrations/20250618_enable_pgcrypto.js @@ -0,0 +1,9 @@ +exports.up = async function (knex) { + await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto'); + }; + + exports.down = async function (knex) { + // normalmente não se remove extensão em down, mas deixo seguro: + // await knex.raw('DROP EXTENSION IF EXISTS pgcrypto'); + }; + \ No newline at end of file diff --git a/src/db/migrations/20250619_create_tables.js b/src/db/migrations/20250619_create_tables.js new file mode 100755 index 0000000..95b7fe2 --- /dev/null +++ b/src/db/migrations/20250619_create_tables.js @@ -0,0 +1,116 @@ +// migrations/20250619_create_tables.js +exports.up = async function (knex) { + // Create 'users' table + await knex.schema.createTable('users', (table) => { + table.increments('id').primary(); + table.string('username', 255).notNullable().unique(); + table.string('password', 255).notNullable(); + table.timestamp('created_at').defaultTo(knex.fn.now()); + }); + + // Create 'chargers' table + await knex.schema.createTable('chargers', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + + table + .integer('user_id') + .unsigned() + .notNullable() + .references('id') + .inTable('users') + .onDelete('CASCADE'); + + table.string('location', 255).notNullable(); + table.string('status', 50).notNullable().defaultTo('offline'); + + // ✅ FIX: firmware manda amps com decimais (ex: 14.2) + table.decimal('charging_current', 8, 2).notNullable().defaultTo(32); + + // updated_at: usado pelo offline checker + table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()); + + table.string('mqtt_user', 255).notNullable(); + table.string('mqtt_pass', 255).notNullable(); + table.string('mqtt_topic', 255).notNullable().unique(); + + table.timestamp('created_at').defaultTo(knex.fn.now()); + + // Total charging time: + // ✅ Recomendo guardar em segundos (int). + // Se o teu firmware mandar decimal (minutos/horas), troca para decimal(10,1). + table.integer('charging_time').notNullable().defaultTo(0); + // table.decimal('charging_time', 10, 1).notNullable().defaultTo(0); + + // Consumption (kWh) + table.decimal('consumption', 10, 3).notNullable().defaultTo(0); + // ↑ aumentei precisão (kWh acumulado pode crescer e 8,2 é curto em alguns cenários) + + // Power (kW) + table.decimal('power_l1', 10, 3).notNullable().defaultTo(0); + table.decimal('power_l2', 10, 3).notNullable().defaultTo(0); + table.decimal('power_l3', 10, 3).notNullable().defaultTo(0); + + // Voltage (V) + table.decimal('voltage_l1', 10, 3).notNullable().defaultTo(0); + table.decimal('voltage_l2', 10, 3).notNullable().defaultTo(0); + table.decimal('voltage_l3', 10, 3).notNullable().defaultTo(0); + + // Current (A) + table.decimal('current_l1', 10, 3).notNullable().defaultTo(0); + table.decimal('current_l2', 10, 3).notNullable().defaultTo(0); + table.decimal('current_l3', 10, 3).notNullable().defaultTo(0); + + // índices úteis (produção) + table.index(['user_id']); + table.index(['updated_at']); + table.index(['status']); + }); + + // Create 'charger_configs' table + await knex.schema.createTable('charger_configs', (table) => { + table + .uuid('charger_id') + .primary() + .references('id') + .inTable('chargers') + .onDelete('CASCADE'); + + table.integer('max_charging_current').notNullable().defaultTo(32); + table.boolean('require_auth').notNullable().defaultTo(false); + table.boolean('rcm_enabled').notNullable().defaultTo(false); + table.integer('temperature_limit').notNullable().defaultTo(60); + table.timestamp('config_received_at').notNullable().defaultTo(knex.fn.now()); + }); + + // Create 'charger_sessions' table + await knex.schema.createTable('charger_sessions', (table) => { + table.increments('id').primary(); + + table + .uuid('charger_id') + .notNullable() + .references('id') + .inTable('chargers') + .onDelete('CASCADE'); + + table.timestamp('started_at').notNullable(); + table.timestamp('ended_at'); + + table.decimal('kwh', 10, 3).notNullable().defaultTo(0); + table.decimal('cost', 12, 2); + + table.timestamp('created_at').defaultTo(knex.fn.now()); + + // índices úteis + table.index(['charger_id']); + table.index(['started_at']); + table.index(['ended_at']); + }); +}; + +exports.down = async function (knex) { + await knex.schema.dropTableIfExists('charger_sessions'); + await knex.schema.dropTableIfExists('charger_configs'); + await knex.schema.dropTableIfExists('chargers'); + await knex.schema.dropTableIfExists('users'); +}; diff --git a/migrations/20251123084023_create_charger_schedules.js b/src/db/migrations/20251123084023_create_charger_schedules.js old mode 100644 new mode 100755 similarity index 100% rename from migrations/20251123084023_create_charger_schedules.js rename to src/db/migrations/20251123084023_create_charger_schedules.js diff --git a/migrations/20251123_create_charger_schedules.js b/src/db/migrations/20251123_create_charger_schedules.js old mode 100644 new mode 100755 similarity index 100% rename from migrations/20251123_create_charger_schedules.js rename to src/db/migrations/20251123_create_charger_schedules.js diff --git a/migrations/20251123_create_push_subscriptions.js b/src/db/migrations/20251123_create_push_subscriptions.js old mode 100644 new mode 100755 similarity index 100% rename from migrations/20251123_create_push_subscriptions.js rename to src/db/migrations/20251123_create_push_subscriptions.js diff --git a/src/db/migrations/20260110_alter_chargers_numeric_fields.js b/src/db/migrations/20260110_alter_chargers_numeric_fields.js new file mode 100644 index 0000000..b597905 --- /dev/null +++ b/src/db/migrations/20260110_alter_chargers_numeric_fields.js @@ -0,0 +1,38 @@ +// src/db/migrations/20260110_alter_chargers_numeric_fields.js + +exports.up = async function (knex) { + // charging_current: estava INTEGER, mas o firmware manda amps com decimal (ex: 14.2) + await knex.raw(` + ALTER TABLE chargers + ALTER COLUMN charging_current + TYPE numeric(8,2) + USING charging_current::numeric + `); + + // charging_time: também estava INTEGER; em alguns firmwares pode vir com decimal (ex: 12.5) + // Se tens a certeza que é sempre inteiro, podes remover este bloco. + await knex.raw(` + ALTER TABLE chargers + ALTER COLUMN charging_time + TYPE numeric(10,1) + USING charging_time::numeric + `); + }; + + exports.down = async function (knex) { + // volta para INTEGER arredondando + await knex.raw(` + ALTER TABLE chargers + ALTER COLUMN charging_current + TYPE integer + USING ROUND(charging_current)::integer + `); + + await knex.raw(` + ALTER TABLE chargers + ALTER COLUMN charging_time + TYPE integer + USING ROUND(charging_time)::integer + `); + }; + \ No newline at end of file diff --git a/src/domain/normalize/chargingConfig.js b/src/domain/normalize/chargingConfig.js new file mode 100755 index 0000000..35b8e4f --- /dev/null +++ b/src/domain/normalize/chargingConfig.js @@ -0,0 +1,34 @@ +// src/domain/normalize/chargingConfig.js + +/** + * Normaliza eventos de config (quando o carregador manda config) + */ +function normalizeChargingConfig(data = {}) { + const chargerId = data.charger_id || data.chargerId || data.id; + const cfg = data.config || data.raw?.config || data; + + return { + charger_id: chargerId, + mqtt_topic: data.mqtt_topic || data.mqttTopic, + config: { + max_charging_current: + cfg.max_charging_current ?? + cfg.maxChargingCurrent ?? + cfg.max_current ?? + cfg.maxCurrent ?? + undefined, + + require_auth: cfg.require_auth ?? cfg.requireAuth ?? undefined, + rcm_enabled: cfg.rcm_enabled ?? cfg.rcmEnabled ?? undefined, + temperature_limit: + cfg.temperature_limit ?? + cfg.temperatureThreshold ?? + cfg.temp_limit ?? + undefined, + }, + raw: data.raw || data, + updated_at: new Date().toISOString(), + }; +} + +module.exports = { normalizeChargingConfig }; diff --git a/src/domain/normalize/chargingStatus.js b/src/domain/normalize/chargingStatus.js new file mode 100755 index 0000000..357e976 --- /dev/null +++ b/src/domain/normalize/chargingStatus.js @@ -0,0 +1,73 @@ +// src/domain/normalize/chargingStatus.js + +const toNum = (v) => { + if (v === null || v === undefined || v === '') return 0; + const n = typeof v === 'number' ? v : parseFloat(v); + return Number.isFinite(n) ? n : 0; +}; + +const toArr3 = (v) => { + if (Array.isArray(v)) return [toNum(v[0]), toNum(v[1]), toNum(v[2])]; + if (v && typeof v === 'object') return [toNum(v.l1), toNum(v.l2), toNum(v.l3)]; + return [0, 0, 0]; +}; + +const normalizeStatusText = (rawStatus) => { + const s = String(rawStatus || '').toLowerCase(); + + if (s.includes('charging')) return '⚡ Charging'; + if (s.includes('ready')) return '🟢 Ready'; + if (s.includes('fault') || s.includes('error')) return '⚠️ Fault'; + if (s.includes('wait')) return '⚡ Wait'; + if (s.includes('not conn') || s.includes('disconnected')) return '🔌 Not Conn.'; + if (s.includes('vent')) return '💨 Vent'; + + return rawStatus || '—'; +}; + +/** + * Normaliza eventos de status (realtime) vindos do mqtt -> socket + */ +function normalizeChargingStatus(data = {}) { + const chargerId = data.charger_id || data.chargerId || data.id; + + const powerArr = toArr3(data.power || data.raw?.power); + const voltageArr = toArr3(data.voltage || data.raw?.voltage); + const currentArr = toArr3(data.current || data.raw?.current); + + const status = normalizeStatusText(data.status || data.state || data.raw?.state); + + const chargingTime = + toNum(data.charging_time) || + toNum(data.chargingTime) || + toNum(data.raw?.chargingTime) || + toNum(data.raw?.sessionTime); + + const consumption = toNum(data.consumption) || toNum(data.raw?.consumption); + + const chargingCurrent = + toNum(data.charging_current) || + toNum(data.chargingCurrent) || + currentArr[0]; + + return { + charger_id: chargerId, + mqtt_topic: data.mqtt_topic || data.mqttTopic, + + status, + stateCode: data.stateCode || data.raw?.stateCode || undefined, + + consumption, + charging_time: chargingTime, + charging_current: chargingCurrent, + + power: powerArr, + voltage: voltageArr, + current: currentArr, + + raw: data.raw || data, + updated_at: new Date().toISOString(), + }; +} + +module.exports = { normalizeChargingStatus }; diff --git a/src/middleware/auth.js b/src/middleware/auth.js new file mode 100755 index 0000000..b03adb7 --- /dev/null +++ b/src/middleware/auth.js @@ -0,0 +1,36 @@ +// src/middleware/auth.js +const jwt = require('jsonwebtoken'); +const config = require('../config'); + +function verifyToken(req, res, next) { + const authHeader = req.headers['authorization'] || req.headers['Authorization']; + + if (!authHeader) { + return res.status(401).json({ error: 'Token não fornecido' }); + } + + const match = authHeader.match(/^Bearer\s+(.+)$/i); + if (!match) { + return res.status(401).json({ error: 'Token malformado. Use "Bearer "' }); + } + + const token = match[1]; + + jwt.verify(token, config.jwtSecret, (err, payload) => { + if (err) { + if (err.name === 'TokenExpiredError') { + return res.status(401).json({ error: 'Sessão expirada' }); + } + return res.status(401).json({ error: 'Token inválido' }); + } + + if (!payload?.id) { + return res.status(401).json({ error: 'Token inválido' }); + } + + req.user = payload; + next(); + }); +} + +module.exports = verifyToken; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js new file mode 100755 index 0000000..a8502f2 --- /dev/null +++ b/src/middleware/errorHandler.js @@ -0,0 +1,20 @@ +// src/middleware/errorHandler.js +const config = require('../config'); + +function errorHandler(err, req, res, next) { + console.error('[errorHandler]', err); + + if (res.headersSent) return next(err); + + const status = err.statusCode || err.status || 500; + + // ✅ em produção, 500+ devolve mensagem genérica + const message = + status >= 500 && config.env === 'production' + ? 'Erro interno do servidor' + : err.message || 'Erro interno do servidor'; + + res.status(status).json({ success: false, message }); +} + +module.exports = errorHandler; diff --git a/src/middleware/validate.js b/src/middleware/validate.js new file mode 100755 index 0000000..40cef78 --- /dev/null +++ b/src/middleware/validate.js @@ -0,0 +1,12 @@ +// src/middleware/validate.js +const { validationResult } = require('express-validator'); + +function handleValidation(req, res, next) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ success: false, errors: errors.array() }); + } + next(); +} + +module.exports = handleValidation; diff --git a/src/mqtt/context.js b/src/mqtt/context.js new file mode 100644 index 0000000..24b55e7 --- /dev/null +++ b/src/mqtt/context.js @@ -0,0 +1,174 @@ +// src/mqtt/context.js +function createMqttContext({ db, config, emitter, sendPushToUser }) { + // -------------------- + // Helpers + // -------------------- + const toNum = (v) => { + if (v === null || v === undefined || v === '') return 0; + const n = typeof v === 'number' ? v : parseFloat(v); + return Number.isFinite(n) ? n : 0; + }; + + const roundN = (v, n) => { + const num = toNum(v); + const m = Math.pow(10, n); + return Math.round(num * m) / m; + }; + + // ✅ mantém round1 por compatibilidade (legacy usa) + const round1 = (v) => roundN(v, 1); + + // ✅ 2 casas (para DB/telemetria) + const round2 = (v) => roundN(v, 2); + + // W -> kW com 2 casas + const wToKw2 = (w) => round2(toNum(w) / 1000); + + // ✅ arrays com 2 casas (evita lixo e mantém consistência) + const toArr3 = (v) => { + if (Array.isArray(v)) return [round2(v[0]), round2(v[1]), round2(v[2])]; + if (v && typeof v === 'object') return [round2(v.l1), round2(v.l2), round2(v.l3)]; + return [0, 0, 0]; + }; + + const toArr3Kw2 = (v) => { + if (Array.isArray(v)) return [wToKw2(v[0]), wToKw2(v[1]), wToKw2(v[2])]; + if (v && typeof v === 'object') return [wToKw2(v.l1), wToKw2(v.l2), wToKw2(v.l3)]; + return [0, 0, 0]; + }; + + function safeJsonParse(buf) { + try { + return JSON.parse(buf.toString()); + } catch { + return null; + } + } + + function shallowEqual(a, b) { + if (a === b) return true; + if (!a || !b) return false; + + const ak = Object.keys(a); + const bk = Object.keys(b); + if (ak.length !== bk.length) return false; + + for (const k of ak) { + const av = a[k]; + const bv = b[k]; + + if (Array.isArray(av) || Array.isArray(bv)) { + if (!Array.isArray(av) || !Array.isArray(bv)) return false; + if (av.length !== bv.length) return false; + for (let i = 0; i < av.length; i++) { + if (av[i] !== bv[i]) return false; + } + } else if (av !== bv) { + return false; + } + } + + return true; + } + + function inferStateCode(raw) { + const s = String(raw || '').trim(); + if (!s) return ''; + return s.split(/\s+/)[0] || ''; + } + + function getStatusFromStateCode(code) { + const map = { + A: '🔌 Not Conn.', + A1: '🔌 Not Conn.', + B1: '🟡 Unauth.', + B2: '🟢 Ready', + C1: '⚡ Wait', + C2: '⚡ Charging', + D1: '💨 Vent (req)', + D2: '💨 Vent', + E: '❌ CP Error', + F: '⚠️ Fault', + }; + return map[code] || '❓ Unknown'; + } + + function stripUndef(obj) { + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); + } + + // -------------------- + // Caches + // -------------------- + const lastDbStateByChargerId = new Map(); // chargerId -> { ...dbFields } + const lastMetaByChargerId = new Map(); // chargerId -> { status, stateCode, rawStatus } + const lastTotalEnergyByChargerId = new Map(); // chargerId -> totalEnergy (kWh) + const sessionStartEnergyByChargerId = new Map(); // chargerId -> totalEnergy at session start + const lastEnabled = {}; // chargerId -> boolean + + const chargerCache = new Map(); // mqttTopic -> { charger, fetchedAt } + const CHARGER_CACHE_TTL_MS = config.chargerCacheTtlMs; + + // -------------------- + // DB helpers + // -------------------- + async function getChargerByMqttTopic(mqttTopic) { + const cached = chargerCache.get(mqttTopic); + const now = Date.now(); + + if (cached && now - cached.fetchedAt < CHARGER_CACHE_TTL_MS) { + return cached.charger; + } + + const charger = await db('chargers').where({ mqtt_topic: mqttTopic }).first(); + if (charger) chargerCache.set(mqttTopic, { charger, fetchedAt: now }); + return charger; + } + + async function updateChargerDbIfChanged(chargerId, partialUpdate) { + const prev = lastDbStateByChargerId.get(chargerId) || null; + + const cleanPartial = stripUndef(partialUpdate); + const next = { ...(prev || {}), ...cleanPartial }; + + const changed = !prev || !shallowEqual(prev, next); + if (!changed) return false; + + await db('chargers').where({ id: chargerId }).update(cleanPartial); + lastDbStateByChargerId.set(chargerId, next); + return true; + } + + return { + db, + config, + emitter, + sendPushToUser, + + // helpers + toNum, + round1, // ✅ não remover (legacy) + round2, + wToKw2, + toArr3, + toArr3Kw2, + safeJsonParse, + shallowEqual, + inferStateCode, + getStatusFromStateCode, + stripUndef, + + // caches + lastDbStateByChargerId, + lastMetaByChargerId, + lastTotalEnergyByChargerId, + sessionStartEnergyByChargerId, + lastEnabled, + + // db helpers + getChargerByMqttTopic, + updateChargerDbIfChanged, + }; +} + +module.exports = { createMqttContext }; diff --git a/src/mqtt/handlers/evse.handler.js b/src/mqtt/handlers/evse.handler.js new file mode 100755 index 0000000..f04a71c --- /dev/null +++ b/src/mqtt/handlers/evse.handler.js @@ -0,0 +1,182 @@ +// src/mqtt/handlers/evse.handler.js +async function handleStateEvse(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) { + console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); + return; + } + + const chargerId = charger.id; + const now = new Date(); + + const rawStatus = payload?.status || payload?.state || ''; + const stateCode = ctx.inferStateCode(rawStatus); + if (!stateCode) { + console.warn(`[MQTT] state/evse sem stateCode válido (charger ${chargerId})`); + return; + } + + const status = ctx.getStatusFromStateCode(stateCode); + ctx.lastMetaByChargerId.set(chargerId, { status, stateCode, rawStatus }); + + const isCharging = stateCode === 'C2'; + + // do firmware: chargers[0].current / power (W) + const ch0 = Array.isArray(payload?.chargers) ? payload.chargers[0] : null; + + // ✅ current guardado com 2 casas (e só quando está a carregar) + const currentA = isCharging ? ctx.round2(ch0?.current) : 0; + + // ✅ power vem em W -> guardar kW com 2 casas (e só quando está a carregar) + const powerKw = isCharging ? ctx.wToKw2(ch0?.power) : 0; + + const dbUpdate = { + status, + charging_current: currentA, + + power_l1: powerKw, + power_l2: 0, + power_l3: 0, + + current_l1: currentA, + current_l2: 0, + current_l3: 0, + + updated_at: now.toISOString(), + }; + + await ctx.updateChargerDbIfChanged(chargerId, dbUpdate); + + // Sessões start/stop baseado em C2 + const previouslyEnabled = ctx.lastEnabled[chargerId] || false; + const currentlyEnabled = isCharging; + + if (!previouslyEnabled && currentlyEnabled) { + const startEnergy = ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0; + ctx.sessionStartEnergyByChargerId.set(chargerId, startEnergy); + + const activeSession = await ctx.db('charger_sessions') + .where({ charger_id: chargerId }) + .whereNull('ended_at') + .first(); + + if (!activeSession) { + await ctx.db('charger_sessions').insert({ + charger_id: chargerId, + started_at: now, + kwh: 0, + }); + console.log(`[DB] Sessão iniciada para charger ID ${chargerId}`); + } + } + + if (previouslyEnabled && !currentlyEnabled) { + const session = await ctx.db('charger_sessions') + .where({ charger_id: chargerId }) + .whereNull('ended_at') + .first(); + + if (session) { + const endEnergy = ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0; + const startEnergy = ctx.sessionStartEnergyByChargerId.get(chargerId) ?? 0; + const delta = Math.max(0, ctx.round2(endEnergy - startEnergy)); + + await ctx.db('charger_sessions') + .where({ id: session.id }) + .update({ ended_at: now, kwh: delta }); + + console.log(`[DB] Sessão finalizada para charger ID ${chargerId} (kWh=${delta})`); + } + + ctx.sessionStartEnergyByChargerId.delete(chargerId); + + await ctx.sendPushToUser(charger.user_id, { + title: '✅ Carregamento concluído', + body: `${charger.location || 'Carregador'} terminou o carregamento.`, + url: `/history`, + }); + } + + ctx.lastEnabled[chargerId] = currentlyEnabled; + + // ✅ também normalizamos o que vai para o socket (opcional mas consistente) + const consumption = ctx.round2(ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0); + const chargingTime = ctx.round2(ctx.toNum(payload?.chargingTime) || 0); + + ctx.emitter.emit('charging-status', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + status, + stateCode, + consumption, + chargingTime, + power: [powerKw, 0, 0], + voltage: [0, 0, 0], + current: [currentA, 0, 0], + raw: payload, + }); + + if (status === '⚠️ Fault' || status === '❌ CP Error') { + await ctx.sendPushToUser(charger.user_id, { + title: '⚠️ Erro no carregador', + body: `${charger.location || 'Carregador'} entrou em falha.`, + url: `/charger/${charger.id}`, + }); + } +} + +async function handleStateScheduler(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('scheduler-state', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +async function handleStateLoadbalancing(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('loadbalancing-state', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +async function handleStateAuth(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('auth-state', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +async function handleStateMetersConfig(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + ctx.emitter.emit('meters-config', { + charger_id: charger.id, + mqtt_topic: mqttTopic, + ...payload, + updated_at: new Date().toISOString(), + }); +} + +module.exports = { + handleStateEvse, + handleStateScheduler, + handleStateLoadbalancing, + handleStateAuth, + handleStateMetersConfig, +}; diff --git a/src/mqtt/handlers/legacy.handler.js b/src/mqtt/handlers/legacy.handler.js new file mode 100755 index 0000000..bb00742 --- /dev/null +++ b/src/mqtt/handlers/legacy.handler.js @@ -0,0 +1,95 @@ +// src/mqtt/handlers/legacy.handler.js +async function handleLegacyState(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) { + console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); + return; + } + + const chargerId = charger.id; + const now = new Date(); + + const stateCode = ctx.inferStateCode(payload?.state); + if (!stateCode) return; + + const status = ctx.getStatusFromStateCode(stateCode); + ctx.lastMetaByChargerId.set(chargerId, { status, stateCode, rawStatus: payload?.state }); + + // legacy arrays (assumimos W -> converter p/ kW) + const powerArrKw = ctx.toArr3Kw2(payload?.power); + const voltageArr = ctx.toArr3(payload?.voltage); + const currentArr = ctx.toArr3(payload?.current); + + // ✅ 2 casas + const consumption = ctx.round2(payload?.consumption); + + // ⚠️ se o teu charging_time no DB ainda for INTEGER, mete Math.round aqui. + // Como disseste que ajustaste a DB para decimais, mantemos 2 casas: + const chargingTime = ctx.round2(payload?.chargingTime ?? payload?.sessionTime); + + const dbUpdate = { + status, + charging_current: currentArr[0], + consumption, + charging_time: chargingTime, + + power_l1: powerArrKw[0], + power_l2: powerArrKw[1], + power_l3: powerArrKw[2], + + voltage_l1: voltageArr[0], + voltage_l2: voltageArr[1], + voltage_l3: voltageArr[2], + + current_l1: currentArr[0], + current_l2: currentArr[1], + current_l3: currentArr[2], + + updated_at: now.toISOString(), + }; + + await ctx.updateChargerDbIfChanged(chargerId, dbUpdate); + + ctx.emitter.emit('charging-status', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + status, + stateCode, + consumption, + chargingTime, + power: powerArrKw, + voltage: voltageArr, + current: currentArr, + raw: payload, + }); +} + +async function handleLegacyConfigResponse(ctx, mqttTopic, payload) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) return; + + const configData = { + charger_id: charger.id, + max_charging_current: payload?.maxChargingCurrent || 32, + require_auth: !!payload?.requireAuth, + rcm_enabled: !!payload?.rcm, + temperature_limit: payload?.temperatureThreshold || 60, + config_received_at: new Date().toISOString(), + }; + + const existingConfig = await ctx.db('charger_configs').where({ charger_id: charger.id }).first(); + + if (existingConfig) { + await ctx.db('charger_configs').where({ charger_id: charger.id }).update(configData); + } else { + await ctx.db('charger_configs').insert(configData); + } + + ctx.emitter.emit('charging-config', { + ...configData, + mqtt_topic: mqttTopic, + raw: payload, + }); +} + +module.exports = { handleLegacyState, handleLegacyConfigResponse }; diff --git a/src/mqtt/handlers/meter.handler.js b/src/mqtt/handlers/meter.handler.js new file mode 100755 index 0000000..d9d0d12 --- /dev/null +++ b/src/mqtt/handlers/meter.handler.js @@ -0,0 +1,94 @@ +// src/mqtt/handlers/meter.handler.js +async function handleStateMeter(ctx, mqttTopic, payload, meterKind /* 'evse'|'grid' */) { + const charger = await ctx.getChargerByMqttTopic(mqttTopic); + if (!charger) { + console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`); + return; + } + + const chargerId = charger.id; + const now = new Date(); + + const vrms = ctx.toArr3(payload?.vrms); // 2 casas + const irms = ctx.toArr3(payload?.irms); // 2 casas + + // watt em W -> kW com 2 casas + const wattKw = ctx.toArr3Kw2(payload?.watt); + + const totalEnergy = ctx.round2(payload?.totalEnergy); // kWh acumulado (2 casas) + const source = String(payload?.source || meterKind || '').toUpperCase(); + + if (Number.isFinite(totalEnergy) && totalEnergy >= 0) { + ctx.lastTotalEnergyByChargerId.set(chargerId, totalEnergy); + } + + if (meterKind === 'evse' || source === 'EVSE') { + const dbUpdate = { + consumption: totalEnergy, + charging_current: ctx.round2(irms[0]), + + power_l1: wattKw[0], + power_l2: wattKw[1], + power_l3: wattKw[2], + + voltage_l1: vrms[0], + voltage_l2: vrms[1], + voltage_l3: vrms[2], + + current_l1: irms[0], + current_l2: irms[1], + current_l3: irms[2], + + updated_at: now.toISOString(), + }; + + await ctx.updateChargerDbIfChanged(chargerId, dbUpdate); + + // atualiza sessão ativa com delta (sem fechar) + const currentlyEnabled = ctx.lastEnabled[chargerId] || false; + if (currentlyEnabled) { + const session = await ctx.db('charger_sessions') + .where({ charger_id: chargerId }) + .whereNull('ended_at') + .first(); + + if (session) { + const startEnergy = ctx.sessionStartEnergyByChargerId.get(chargerId) ?? 0; + const delta = Math.max( + 0, + ctx.round2((ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0) - startEnergy) + ); + + await ctx.db('charger_sessions').where({ id: session.id }).update({ kwh: delta }); + } + } + + const meta = ctx.lastMetaByChargerId.get(chargerId) || { status: '—', stateCode: undefined }; + ctx.emitter.emit('charging-status', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + status: meta.status, + stateCode: meta.stateCode, + consumption: totalEnergy, + chargingTime: 0, + power: wattKw, + voltage: vrms, + current: irms, + raw: payload, + }); + } + + ctx.emitter.emit('meter-live', { + charger_id: chargerId, + mqtt_topic: mqttTopic, + meter: meterKind, + vrms, + irms, + watt: wattKw, + totalEnergy, + raw: payload, + updated_at: now.toISOString(), + }); +} + +module.exports = { handleStateMeter }; diff --git a/src/mqtt/index.js b/src/mqtt/index.js new file mode 100755 index 0000000..413dad1 --- /dev/null +++ b/src/mqtt/index.js @@ -0,0 +1,204 @@ +// src/mqtt/index.js +const mqtt = require('mqtt'); +const EventEmitter = require('events'); + +const db = require('../db/knex'); +const config = require('../config'); +const { sendPushToUser } = require('../services/push.service'); + +const { createPublishers } = require('./publishers'); +const { createMqttContext } = require('./context'); + +const { + handleStateEvse, + handleStateScheduler, + handleStateLoadbalancing, + handleStateAuth, + handleStateMetersConfig, +} = require('./handlers/evse.handler'); +const { handleStateMeter } = require('./handlers/meter.handler'); +const { handleLegacyState, handleLegacyConfigResponse } = require('./handlers/legacy.handler'); + +const emitter = new EventEmitter(); + +// -------------------- +// MQTT client +// -------------------- +const MQTT_URL = config.mqtt.url; +const mqttUser = config.mqtt.user; +const mqttPass = config.mqtt.pass; + +let connected = false; + +const client = mqtt.connect(MQTT_URL, { + username: mqttUser, + password: mqttPass, + reconnectPeriod: 2000, +}); + +client.on('connect', () => { + connected = true; +}); + +client.on('offline', () => { + connected = false; +}); + +client.on('close', () => { + connected = false; +}); + +// publishers separados +const publishers = createPublishers(client); + +// ctx separado (helpers + caches + db helpers) +const ctx = createMqttContext({ db, config, emitter, sendPushToUser }); + +// -------------------- +// Subscribe +// -------------------- +client.on('connect', () => { + console.log('[MQTT] Conectado ao broker:', MQTT_URL); + + const fixedTopics = [ + '+/state/#', + '+/state', + '+/response/#', + '+/response/config/evse', + ]; + + const envTopics = config.mqtt.subTopics || []; + const topicsToSub = [...new Set([...fixedTopics, ...envTopics])]; + + topicsToSub.forEach((t) => { + client.subscribe(t, { qos: 0 }, (err, granted) => { + if (err) { + console.error('[MQTT] Falha ao subscrever', t, err.message); + } else { + console.log('[MQTT] Subscrito:', granted?.map((g) => g.topic).join(', ') || t); + } + }); + }); +}); + +// -------------------- +// Messages +// -------------------- +client.on('message', async (topic, message) => { + const parts = topic.split('/'); + const mqttTopic = parts[0]; + const subtopic = parts.slice(1).join('/'); + + const payload = ctx.safeJsonParse(message); + if (!payload) { + console.warn('[MQTT] JSON inválido em', topic, 'payload=', message.toString()); + return; + } + + try { + if (subtopic === 'state/evse') return await handleStateEvse(ctx, mqttTopic, payload); + if (subtopic === 'state/scheduler') return await handleStateScheduler(ctx, mqttTopic, payload); + if (subtopic === 'state/loadbalancing') + return await handleStateLoadbalancing(ctx, mqttTopic, payload); + if (subtopic === 'state/auth') return await handleStateAuth(ctx, mqttTopic, payload); + if (subtopic === 'state/meters-config') + return await handleStateMetersConfig(ctx, mqttTopic, payload); + if (subtopic === 'state/meter/evse') + return await handleStateMeter(ctx, mqttTopic, payload, 'evse'); + if (subtopic === 'state/meter/grid') + return await handleStateMeter(ctx, mqttTopic, payload, 'grid'); + + if (subtopic === 'state') return await handleLegacyState(ctx, mqttTopic, payload); + if (subtopic === 'response/config/evse') + return await handleLegacyConfigResponse(ctx, mqttTopic, payload); + } catch (err) { + console.error('[MQTT] Erro ao processar', topic, err); + } +}); + +// -------------------- +// Broker offline / checker +// -------------------- +client.on('offline', async () => { + console.warn('[MQTT] Broker offline'); + + try { + const chargers = await ctx.db('chargers').select('id', 'user_id', 'location'); + const uniqueUsers = [...new Set(chargers.map((c) => c.user_id))]; + + await Promise.allSettled( + uniqueUsers.map((userId) => + ctx.sendPushToUser(userId, { + title: '📡 Broker MQTT offline', + body: 'O sistema perdeu ligação ao broker. Alguns estados podem estar desatualizados.', + url: '/', + }) + ) + ); + } catch (err) { + console.error('[MQTT] erro offline push:', err.message); + } +}); + +const offlineTimer = setInterval(async () => { + try { + const timeoutMinutes = config.chargerOfflineMinutes; + const limitDate = new Date(Date.now() - timeoutMinutes * 60 * 1000); + + const offlineChargers = await ctx.db('chargers') + .where('updated_at', '<', limitDate.toISOString()) + .andWhereNot({ status: 'offline' }) + .select('*'); + + for (const ch of offlineChargers) { + await ctx.db('chargers').where({ id: ch.id }).update({ status: 'offline' }); + + ctx.lastDbStateByChargerId.delete(ch.id); + ctx.lastMetaByChargerId.delete(ch.id); + ctx.lastTotalEnergyByChargerId.delete(ch.id); + ctx.sessionStartEnergyByChargerId.delete(ch.id); + + ctx.lastEnabled[ch.id] = false; + + await ctx.sendPushToUser(ch.user_id, { + title: '🔌 Carregador offline', + body: `${ch.location || 'Carregador'} está offline há mais de ${timeoutMinutes} min.`, + url: `/charger/${ch.id}`, + }); + } + } catch (err) { + console.error('[MQTT] offline checker erro:', err.message); + } +}, 60 * 1000); + +// -------------------- +// API pública +// -------------------- +function on(event, handler) { + emitter.on(event, handler); +} + +function isConnected() { + return connected; +} + +async function shutdown() { + try { + clearInterval(offlineTimer); + } catch { } + + await new Promise((resolve) => { + try { + client.end(true, {}, resolve); + } catch { + resolve(); + } + }); +} + +module.exports = { + on, + isConnected, + shutdown, + ...publishers, +}; diff --git a/src/mqtt/publishers.js b/src/mqtt/publishers.js new file mode 100755 index 0000000..02d2bf3 --- /dev/null +++ b/src/mqtt/publishers.js @@ -0,0 +1,55 @@ +// src/mqtt/publishers.js +function createPublishers(client) { + /** + * ✅ Firmware novo: + * Para settings: /cmd/evse/settings + * payload: { currentLimit, temperatureLimit } + */ + function sendEvseSettings(chargerTopic, settings = {}) { + const payload = {}; + if (settings.currentLimit !== undefined) payload.currentLimit = Number(settings.currentLimit); + if (settings.temperatureLimit !== undefined) + payload.temperatureLimit = Number(settings.temperatureLimit); + + if (!Object.keys(payload).length) return; + + client.publish(`${chargerTopic}/cmd/evse/settings`, JSON.stringify(payload), { qos: 1 }); + } + + // compat + mapeamento + function sendConfig(chargerTopic, property, value) { + const map = { + maxChargingCurrent: 'currentLimit', + temperatureThreshold: 'temperatureLimit', + }; + + if (map[property]) { + return sendEvseSettings(chargerTopic, { [map[property]]: value }); + } + + // fallback legacy + const payload = { [property]: value }; + client.publish(`${chargerTopic}/set/config/evse`, JSON.stringify(payload), { qos: 1 }); + } + + // legacy (mantidos) + function sendEnable(chargerTopic, enable) { + client.publish(`${chargerTopic}/enable`, JSON.stringify({ enable: !!enable }), { qos: 1 }); + } + + function requestConfig(chargerTopic) { + client.publish(`${chargerTopic}/request/config/evse`, null, { qos: 1 }); + } + + // helper genérico novo + function sendCmd(chargerTopic, cmdSubtopic, obj) { + const t = `${chargerTopic}/cmd/${cmdSubtopic}`; + const msg = obj ? JSON.stringify(obj) : ''; + client.publish(t, msg, { qos: 1 }); + } + + return { sendConfig, sendEvseSettings, sendEnable, requestConfig, sendCmd }; + } + + module.exports = { createPublishers }; + \ No newline at end of file diff --git a/src/repositories/chargers.repo.js b/src/repositories/chargers.repo.js new file mode 100755 index 0000000..eafcebe --- /dev/null +++ b/src/repositories/chargers.repo.js @@ -0,0 +1,83 @@ +// src/repositories/chargers.repo.js +const db = require('../db/knex'); + +async function listByUser(userId) { + return db('chargers').where({ user_id: userId }).select('*'); +} + +async function findByIdForUser(id, userId) { + return db('chargers').where({ id, user_id: userId }).first(); +} + +async function findByMqttTopic(mqtt_topic) { + return db('chargers').where({ mqtt_topic }).first(); +} + +async function insertCharger(data) { + const [row] = await db('chargers').insert(data).returning('*'); + return row; +} + +async function updateChargerForUser(id, userId, patch) { + const [row] = await db('chargers') + .where({ id, user_id: userId }) + .update(patch) + .returning('*'); + return row; +} + +async function deleteChargerForUser(id, userId) { + // devolve o charger antes para poderes usar mqtt_user/pass/topic etc + const charger = await findByIdForUser(id, userId); + if (!charger) return null; + + await db('chargers').where({ id, user_id: userId }).del(); + return charger; +} + +async function getConfig(charger_id) { + return db('charger_configs').where({ charger_id }).first(); +} + +async function insertConfig(data) { + const [row] = await db('charger_configs').insert(data).returning('*'); + return row; +} + +async function updateConfig(charger_id, patch) { + const [row] = await db('charger_configs') + .where({ charger_id }) + .update(patch) + .returning('*'); + return row; +} + +async function upsertConfig(charger_id, patch) { + const existing = await getConfig(charger_id); + if (existing) return updateConfig(charger_id, patch); + return insertConfig({ charger_id, ...patch }); +} + +async function listSchedules(charger_id) { + return db('charger_schedules').where({ charger_id }).orderBy('created_at', 'desc'); +} + +async function insertSchedule(data) { + const [row] = await db('charger_schedules').insert(data).returning('*'); + return row; +} + +module.exports = { + listByUser, + findByIdForUser, + findByMqttTopic, + insertCharger, + updateChargerForUser, + deleteChargerForUser, + getConfig, + insertConfig, + updateConfig, + upsertConfig, + listSchedules, + insertSchedule, +}; diff --git a/src/repositories/push.repo.js b/src/repositories/push.repo.js new file mode 100755 index 0000000..6d98e96 --- /dev/null +++ b/src/repositories/push.repo.js @@ -0,0 +1,38 @@ +// src/repositories/push.repo.js +const db = require('../db/knex'); + +async function findByUserAndEndpoint(user_id, endpoint) { + return db('push_subscriptions').where({ user_id, endpoint }).first(); +} + +async function findByEndpoint(endpoint) { + return db('push_subscriptions').where({ endpoint }).first(); +} + +async function listByUser(user_id) { + return db('push_subscriptions') + .where({ user_id }) + .select('id', 'endpoint', 'p256dh', 'auth'); +} + +async function insertSubscription(data) { + const [row] = await db('push_subscriptions').insert(data).returning('*'); + return row; +} + +async function deleteByUserAndEndpoint(user_id, endpoint) { + return db('push_subscriptions').where({ user_id, endpoint }).del(); +} + +async function deleteById(id) { + return db('push_subscriptions').where({ id }).del(); +} + +module.exports = { + findByUserAndEndpoint, + findByEndpoint, + listByUser, + insertSubscription, + deleteByUserAndEndpoint, + deleteById, +}; diff --git a/src/repositories/sessions.repo.js b/src/repositories/sessions.repo.js new file mode 100755 index 0000000..fc4fa82 --- /dev/null +++ b/src/repositories/sessions.repo.js @@ -0,0 +1,85 @@ +// src/repositories/sessions.repo.js +const db = require('../db/knex'); + +async function listByCharger(charger_id) { + return db('charger_sessions') + .where({ charger_id }) + .orderBy('started_at', 'desc'); +} + +async function findByIdForUser(id, userId) { + return db('charger_sessions') + .join('chargers', 'charger_sessions.charger_id', 'chargers.id') + .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) + .select('charger_sessions.*') + .first(); +} + +async function insertSession(data) { + const [row] = await db('charger_sessions').insert(data).returning('*'); + return row; +} + +async function updateById(id, patch) { + const [row] = await db('charger_sessions').where({ id }).update(patch).returning('*'); + return row; +} + +async function deleteByIdForUser(id, userId) { + return db('charger_sessions') + .join('chargers', 'charger_sessions.charger_id', 'chargers.id') + .where({ 'charger_sessions.id': id, 'chargers.user_id': userId }) + .del(); +} + +async function historyAgg(chargerId, viewMode) { + let qb = db('charger_sessions') + .where({ charger_id: chargerId }) + .sum('kwh as total_kwh'); + + switch (viewMode) { + case 'Day': + qb = qb + .select(db.raw('DATE(started_at) AS period')) + .groupBy(db.raw('DATE(started_at)')) + .orderBy('period', 'desc'); + break; + + case 'Week': + qb = qb + .select( + db.raw('EXTRACT(ISOYEAR FROM started_at) AS y'), + db.raw('EXTRACT(WEEK FROM started_at) AS w'), + db.raw( + "EXTRACT(ISOYEAR FROM started_at)||'-'||LPAD(EXTRACT(WEEK FROM started_at)::text,2,'0') AS period" + ) + ) + .groupBy('y', 'w') + .orderBy([{ column: 'y', order: 'desc' }, { column: 'w', order: 'desc' }]); + break; + + case 'Month': + qb = qb + .select( + db.raw('EXTRACT(YEAR FROM started_at) AS y'), + db.raw('EXTRACT(MONTH FROM started_at) AS m'), + db.raw( + "EXTRACT(YEAR FROM started_at)||'-'||LPAD(EXTRACT(MONTH FROM started_at)::text,2,'0') AS period" + ) + ) + .groupBy('y', 'm') + .orderBy([{ column: 'y', order: 'desc' }, { column: 'm', order: 'desc' }]); + break; + } + + return qb; +} + +module.exports = { + listByCharger, + findByIdForUser, + insertSession, + updateById, + deleteByIdForUser, + historyAgg, +}; diff --git a/src/repositories/users.repo.js b/src/repositories/users.repo.js new file mode 100644 index 0000000..3d85691 --- /dev/null +++ b/src/repositories/users.repo.js @@ -0,0 +1,17 @@ +// src/repositories/users.repo.js +const db = require('../db/knex'); + +async function findByUsername(username) { + return db('users').where({ username }).first(); +} + +async function insertUser({ username, passwordHash }) { + const [row] = await db('users') + .insert({ username, password: passwordHash }) + .returning('id'); + + // pg pode devolver {id} ou o valor direto (depende config) + return row?.id ?? row; +} + +module.exports = { findByUsername, insertUser }; diff --git a/src/routes/chargers.routes.js b/src/routes/chargers.routes.js new file mode 100755 index 0000000..8a696ad --- /dev/null +++ b/src/routes/chargers.routes.js @@ -0,0 +1,149 @@ +// src/routes/chargers.routes.js +const express = require('express'); +const { body, param } = require('express-validator'); + +const verifyToken = require('../middleware/auth'); +const handleValidation = require('../middleware/validate'); +const chargersService = require('../services/chargers.service'); + +const router = express.Router(); +router.use(verifyToken); + +router.get('/', async (req, res, next) => { + try { + const data = await chargersService.list(req.user.id); + res.json({ success: true, data }); + } catch (err) { + next(err); + } +}); + +router.get( + '/:id', + [param('id').isUUID()], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.getOne(req.user.id, req.params.id); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.post( + '/', + [body('location').exists().isString().isLength({ min: 1 }).trim()], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.create(req.user.id, req.body.location); + res.status(201).json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.put( + '/:id', + [ + param('id').isUUID(), + body('charger').optional().isObject(), + body('config').optional().isObject(), + body('location').optional().isString(), + ], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.update(req.user.id, req.params.id, req.body); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.delete( + '/:id', + [param('id').isUUID()], + handleValidation, + async (req, res, next) => { + try { + await chargersService.remove(req.user.id, req.params.id); + res.json({ success: true, message: 'Carregador excluído com sucesso' }); + } catch (err) { + next(err); + } + } +); + +router.put( + '/:id/config', + [param('id').isUUID(), body('config').isObject()], + handleValidation, + async (req, res, next) => { + try { + const out = await chargersService.updateConfig(req.user.id, req.params.id, req.body.config); + res.json({ success: true, data: out.data, message: out.message }); + } catch (err) { + next(err); + } + } +); + +router.get( + '/:id/schedule', + [param('id').isUUID()], + handleValidation, + async (req, res, next) => { + try { + const data = await chargersService.getSchedules(req.user.id, req.params.id); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.post( + '/:id/schedule', + [ + param('id').isUUID(), + body('start').matches(/^\d{2}:\d{2}$/), + body('end').matches(/^\d{2}:\d{2}$/), + body('repeat').isIn(['everyday', 'weekdays', 'weekends']), + ], + handleValidation, + async (req, res, next) => { + try { + const { start, end, repeat } = req.body; + const data = await chargersService.createSchedule(req.user.id, req.params.id, start, end, repeat); + res.status(201).json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.post( + '/:id/action', + [ + param('id').isUUID(), + body('action').isIn(['start', 'stop']), + body('ampLimit').optional().isInt({ min: 6, max: 64 }), + ], + handleValidation, + async (req, res, next) => { + try { + const { action, ampLimit } = req.body; + await chargersService.action(req.user.id, req.params.id, action, ampLimit); + res.json({ success: true, message: `Comando '${action}' enviado com sucesso` }); + } catch (err) { + next(err); + } + } +); + +module.exports = router; diff --git a/src/routes/push.routes.js b/src/routes/push.routes.js new file mode 100755 index 0000000..e832d54 --- /dev/null +++ b/src/routes/push.routes.js @@ -0,0 +1,78 @@ +// src/routes/push.routes.js +const express = require('express'); +const { body } = require('express-validator'); + +const verifyToken = require('../middleware/auth'); +const handleValidation = require('../middleware/validate'); +const config = require('../config'); + +const pushHttpService = require('../services/pushHttp.service'); +const { sendPushToUser } = require('../services/push.service'); + +const router = express.Router(); +router.use(verifyToken); + +// GET /api/push/vapid-public-key +router.get('/vapid-public-key', (req, res) => { + if (!config.vapid.publicKey) { + return res.status(503).json({ success: false, message: 'Push indisponível' }); + } + res.json({ success: true, data: { key: config.vapid.publicKey } }); +}); + +// POST /api/push/subscribe +router.post( + '/subscribe', + [ + body('endpoint').isString(), + body('keys.p256dh').isString(), + body('keys.auth').isString(), + ], + handleValidation, + async (req, res, next) => { + try { + const userId = req.user.id; + const { endpoint, keys } = req.body; + const ua = req.headers['user-agent'] || null; + + const out = await pushHttpService.subscribe(userId, endpoint, keys, ua); + res.status(out.created ? 201 : 200).json({ success: true, data: out.row }); + } catch (err) { + next(err); + } + } +); + +// POST /api/push/unsubscribe +router.post( + '/unsubscribe', + [body('endpoint').optional().isString()], + handleValidation, + async (req, res, next) => { + try { + const userId = req.user.id; + const { endpoint } = req.body || {}; + const out = await pushHttpService.unsubscribe(userId, endpoint); + res.json({ success: true, message: out.message }); + } catch (err) { + next(err); + } + } +); + +// POST /api/push/test +router.post('/test', async (req, res, next) => { + try { + const userId = req.user.id; + await sendPushToUser(userId, { + title: '📬 Teste EV Station', + body: 'Push notifications estão a funcionar!', + url: '/', + }); + res.json({ success: true, message: 'Push enviado' }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/src/routes/sessions.routes.js b/src/routes/sessions.routes.js new file mode 100755 index 0000000..de0b591 --- /dev/null +++ b/src/routes/sessions.routes.js @@ -0,0 +1,113 @@ +// src/routes/sessions.routes.js +const express = require('express'); +const { param, query, body } = require('express-validator'); + +const verifyToken = require('../middleware/auth'); +const handleValidation = require('../middleware/validate'); +const sessionsService = require('../services/sessions.service'); + +const router = express.Router(); +router.use(verifyToken); + +// GET /api/charger_sessions?chargerId=... +router.get( + '/', + [query('chargerId').isUUID().withMessage('chargerId deve ser UUID válido')], + handleValidation, + async (req, res, next) => { + try { + const { chargerId } = req.query; + const data = await sessionsService.listByCharger(req.user.id, chargerId); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +// ✅ /history antes de /:id +router.get( + '/history/:chargerId', + [ + param('chargerId').isUUID().withMessage('chargerId deve ser UUID válido'), + query('viewMode').isIn(['Day', 'Week', 'Month']).withMessage('viewMode inválido'), + ], + handleValidation, + async (req, res, next) => { + try { + const { chargerId } = req.params; + const { viewMode } = req.query; + + const data = await sessionsService.history(req.user.id, chargerId, viewMode); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.get( + '/:id', + [param('id').isInt().withMessage('ID de sessão inválido')], + handleValidation, + async (req, res, next) => { + try { + const data = await sessionsService.getById(req.user.id, Number(req.params.id)); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.post( + '/', + [body('charger_id').isUUID().withMessage('charger_id deve ser UUID válido')], + handleValidation, + async (req, res, next) => { + try { + const data = await sessionsService.create(req.user.id, req.body.charger_id); + res.status(201).json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.put( + '/:id', + [ + param('id').isInt().withMessage('ID de sessão inválido'), + body('ended_at').optional().isISO8601().toDate(), + body('kwh').optional().isFloat({ min: 0 }), + body('cost').optional().isFloat({ min: 0 }), + ], + handleValidation, + async (req, res, next) => { + try { + const id = Number(req.params.id); + const { ended_at, kwh, cost } = req.body; + + const data = await sessionsService.update(req.user.id, id, { ended_at, kwh, cost }); + res.json({ success: true, data }); + } catch (err) { + next(err); + } + } +); + +router.delete( + '/:id', + [param('id').isInt().withMessage('ID de sessão inválido')], + handleValidation, + async (req, res, next) => { + try { + const ok = await sessionsService.remove(req.user.id, Number(req.params.id)); + res.json({ success: true, message: ok ? 'Sessão excluída com sucesso' : 'OK' }); + } catch (err) { + next(err); + } + } +); + +module.exports = router; diff --git a/src/routes/users.routes.js b/src/routes/users.routes.js new file mode 100755 index 0000000..7723694 --- /dev/null +++ b/src/routes/users.routes.js @@ -0,0 +1,35 @@ +// src/routes/users.routes.js +const express = require('express'); +const rateLimit = require('express-rate-limit'); +const usersService = require('../services/users.service'); + +const router = express.Router(); + +const authLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 10, + standardHeaders: true, + legacyHeaders: false, +}); + +router.post('/login', authLimiter, async (req, res, next) => { + try { + const { username, password } = req.body; + const data = await usersService.login(username, password); + res.json({ success: true, data }); + } catch (err) { + next(err); + } +}); + +router.post('/register', authLimiter, async (req, res, next) => { + try { + const { username, password } = req.body; + const data = await usersService.register(username, password); + res.status(201).json({ success: true, data }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/src/server.js b/src/server.js new file mode 100755 index 0000000..d1161e2 --- /dev/null +++ b/src/server.js @@ -0,0 +1,192 @@ +// src/server.js +const http = require('http'); +const { Server } = require('socket.io'); +const jwt = require('jsonwebtoken'); + +const app = require('./app'); +const db = require('./db/knex'); +const config = require('./config'); + +// mqtt exports { on, isConnected, shutdown, ... } +const mqttClient = require('./mqtt'); + +const { normalizeChargingStatus } = require('./domain/normalize/chargingStatus'); +const { normalizeChargingConfig } = require('./domain/normalize/chargingConfig'); + +const server = http.createServer(app); + +const io = new Server(server, { + cors: { + origin: config.corsOrigins, + methods: ['GET', 'POST'], + credentials: true, + }, +}); + +console.log('MQTT client exports=', Object.keys(mqttClient || {})); +if (typeof mqttClient?.on !== 'function') { + console.error('[server] mqttClient.on não existe. Verifica src/mqtt/index.js'); +} + +// --------------------------- +// auth middleware do socket +// --------------------------- +io.use((socket, next) => { + const token = socket.handshake.auth?.token; + if (!token) return next(new Error('Authentication error: token required')); + + try { + const payload = jwt.verify(token, config.jwtSecret); + socket.user = payload; + next(); + } catch (err) { + next(new Error('Authentication error')); + } +}); + +io.on('connection', (socket) => { + console.log(`Client connected: ${socket.id}, user: ${socket.user.username}`); + + socket.on('joinChargers', async (chargerIds = []) => { + try { + if (!Array.isArray(chargerIds) || chargerIds.length === 0) return; + + const rows = await db('chargers') + .whereIn('id', chargerIds) + .andWhere({ user_id: socket.user.id }) + .select('id'); + + const allowed = rows.map((r) => r.id); + allowed.forEach((id) => socket.join(id)); + + console.log(`Socket ${socket.id} joined chargers: ${allowed}`); + } catch (err) { + console.error('joinChargers error:', err); + } + }); + + socket.on('disconnect', (reason) => { + console.log(`Client disconnected: ${socket.id}, reason: ${reason}`); + }); +}); + +// --------------------------- +// Relay MQTT -> Socket.IO +// --------------------------- +if (typeof mqttClient?.on === 'function') { + mqttClient.on('charging-status', (data) => { + const normalized = normalizeChargingStatus(data); + const chargerId = normalized.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('charging-status', normalized); + }); + + mqttClient.on('charging-config', (data) => { + const normalized = normalizeChargingConfig(data); + const chargerId = normalized.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('charging-config', normalized); + }); + + mqttClient.on('scheduler-state', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('evse-scheduler', evt); + io.to(chargerId).emit('scheduler-state', evt); + }); + + mqttClient.on('loadbalancing-state', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + + io.to(chargerId).emit('evse-loadbalancing', evt); + io.to(chargerId).emit('loadbalancing-state', evt); + }); + + mqttClient.on('meter-live', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + io.to(chargerId).emit('meter-live', evt); + }); + + mqttClient.on('auth-state', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + io.to(chargerId).emit('auth-state', evt); + }); + + mqttClient.on('meters-config', (evt) => { + const chargerId = evt?.charger_id; + if (!chargerId) return; + io.to(chargerId).emit('meters-config', evt); + }); +} + +server.listen(config.port, () => { + console.log(`Server listening on http://localhost:${config.port}`); +}); + +// --------------------------- +// Graceful shutdown +// --------------------------- +let shuttingDown = false; + +async function shutdown(signal) { + if (shuttingDown) return; + shuttingDown = true; + + console.log(`[shutdown] recebido ${signal}. A encerrar...`); + + // para aceitar novos requests/sockets + await new Promise((resolve) => { + try { + server.close(resolve); + } catch { + resolve(); + } + }); + + // fecha sockets + await new Promise((resolve) => { + try { + io.close(() => resolve()); + } catch { + resolve(); + } + }); + + // fecha mqtt + try { + if (typeof mqttClient?.shutdown === 'function') { + await mqttClient.shutdown(); + } + } catch (e) { + console.error('[shutdown] mqtt shutdown error:', e?.message || e); + } + + // fecha db + try { + await db.destroy(); + } catch (e) { + console.error('[shutdown] db destroy error:', e?.message || e); + } + + console.log('[shutdown] concluído.'); + process.exit(0); +} + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); + +process.on('unhandledRejection', (err) => { + console.error('[unhandledRejection]', err); + shutdown('unhandledRejection'); +}); + +process.on('uncaughtException', (err) => { + console.error('[uncaughtException]', err); + shutdown('uncaughtException'); +}); diff --git a/src/services/chargers.service.js b/src/services/chargers.service.js new file mode 100755 index 0000000..a5eda30 --- /dev/null +++ b/src/services/chargers.service.js @@ -0,0 +1,338 @@ +// src/services/chargers.service.js +const crypto = require('crypto'); +const axios = require('axios'); + +const chargersRepo = require('../repositories/chargers.repo'); +const { httpError } = require('../utils/httpError'); +const mqttClient = require('../mqtt'); +const config = require('../config'); + +function stripUndef(obj) { + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); +} + +// throttle in-memory (por charger) +const lastConfigUpdateAt = new Map(); + +function clampAmp(v) { + const n = Number(v); + return Math.max(6, Math.min(n, 64)); +} + +function clampTemp(v) { + const n = Number(v); + return Math.max(0, Math.min(n, 120)); +} + +function normalizeNumericFields(charger) { + const numericFields = [ + 'power_l1', + 'power_l2', + 'power_l3', + 'voltage_l1', + 'voltage_l2', + 'voltage_l3', + 'current_l1', + 'current_l2', + 'current_l3', + 'charging_current', + 'consumption', + ]; + + numericFields.forEach((field) => { + const v = charger[field]; + charger[field] = v === null || v === undefined || v === '' ? 0 : Number(v); + if (Number.isNaN(charger[field])) charger[field] = 0; + }); + + return charger; +} + +function mosquittoUrl(path) { + const base = config.mosquittoMgmt.baseUrl; + if (!base) return ''; + return `${base.replace(/\/+$/, '')}${path.startsWith('/') ? '' : '/'}${path}`; +} + +async function mosquittoCreateClient(charger) { + const url = mosquittoUrl('/client/create'); + if (!url) { + console.warn('[MosquittoMgmt] MOSQUITTO_MGMT_URL não definido. Skip create.'); + return; + } + + try { + await axios.post( + url, + { + client_name: charger.mqtt_user, + chargeID: charger.mqtt_topic, + password: charger.mqtt_pass, + }, + { timeout: config.mosquittoMgmt.timeoutMs } + ); + } catch (err) { + console.error('[MosquittoMgmt] Erro ao criar cliente:', err?.response?.data || err.message); + } +} + +async function mosquittoDeleteClient(charger) { + const url = mosquittoUrl('/client/delete'); + if (!url) { + console.warn('[MosquittoMgmt] MOSQUITTO_MGMT_URL não definido. Skip delete.'); + return; + } + + try { + await axios.post( + url, + { + client_name: charger.mqtt_user, + chargeID: charger.mqtt_topic, + }, + { timeout: config.mosquittoMgmt.timeoutMs } + ); + } catch (err) { + console.error('[MosquittoMgmt] Erro ao deletar cliente:', err?.response?.data || err.message); + } +} + +async function list(userId) { + return chargersRepo.listByUser(userId); +} + +async function getOne(userId, id) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + + let cfg = await chargersRepo.getConfig(charger.id); + if (!cfg) { + cfg = { + charger_id: charger.id, + max_charging_current: 32, + require_auth: false, + rcm_enabled: false, + temperature_limit: 60, + }; + } + + return { ...normalizeNumericFields(charger), config: cfg }; +} + +async function create(userId, location) { + if (!location || typeof location !== 'string' || location.trim().length < 1) { + throw httpError(400, 'O campo location é obrigatório'); + } + + const now = new Date().toISOString(); + + let chargerID; + do { + chargerID = crypto.randomBytes(6).toString('hex'); + } while (await chargersRepo.findByMqttTopic(chargerID)); + + const mqtt_topic = chargerID; + const mqtt_user = chargerID; + const mqtt_pass = crypto.randomBytes(6).toString('hex'); + + const charger = await chargersRepo.insertCharger({ + user_id: userId, + location: location.trim(), + status: 'offline', + charging_current: 0, + charging_time: 0, + consumption: 0, + power_l1: 0.0, + power_l2: 0.0, + power_l3: 0.0, + voltage_l1: 0.0, + voltage_l2: 0.0, + voltage_l3: 0.0, + current_l1: 0.0, + current_l2: 0.0, + current_l3: 0.0, + mqtt_user, + mqtt_pass, + mqtt_topic, + updated_at: now, + }); + + await chargersRepo.insertConfig({ + charger_id: charger.id, + max_charging_current: 32, + require_auth: false, + rcm_enabled: false, + temperature_limit: 60, + config_received_at: now, + }); + + await mosquittoCreateClient(charger); + + return charger; +} + +async function update(userId, id, payload = {}) { + let { charger = {}, config: cfgPatch = {} } = payload; + + if (payload.location && !charger.location) charger.location = payload.location; + + const safeChargerUpdate = {}; + if (charger.location !== undefined) safeChargerUpdate.location = charger.location; + + let updatedCharger = null; + + if (Object.keys(safeChargerUpdate).length > 0) { + updatedCharger = await chargersRepo.updateChargerForUser(id, userId, { + ...safeChargerUpdate, + updated_at: new Date().toISOString(), + }); + } else { + updatedCharger = await chargersRepo.findByIdForUser(id, userId); + } + + if (!updatedCharger) throw httpError(404, 'Carregador não encontrado'); + + // config patch com whitelist + if (cfgPatch && Object.keys(cfgPatch).length > 0) { + const ALLOWED = ['max_charging_current', 'require_auth', 'rcm_enabled', 'temperature_limit']; + let safeConfig = Object.fromEntries( + Object.entries(cfgPatch || {}).filter(([k]) => ALLOWED.includes(k)) + ); + + if (safeConfig.max_charging_current !== undefined) { + safeConfig.max_charging_current = clampAmp(safeConfig.max_charging_current); + } + if (safeConfig.temperature_limit !== undefined) { + safeConfig.temperature_limit = clampTemp(safeConfig.temperature_limit); + } + + await chargersRepo.upsertConfig(id, { + ...stripUndef(safeConfig), + config_received_at: new Date().toISOString(), + }); + } + + return updatedCharger; +} + +async function remove(userId, id) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + + await mosquittoDeleteClient(charger); + + await chargersRepo.deleteChargerForUser(id, userId); + return true; +} + +async function updateConfig(userId, id, incomingConfig = {}) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Charger not found or unauthorized'); + + const existing = await chargersRepo.getConfig(id); + + const nowMs = Date.now(); + const lastMs = lastConfigUpdateAt.get(id) || 0; + const tooSoon = nowMs - lastMs < 800; + + const ALLOWED = ['max_charging_current', 'require_auth', 'rcm_enabled', 'temperature_limit']; + let safeConfig = Object.fromEntries( + Object.entries(incomingConfig || {}).filter(([k]) => ALLOWED.includes(k)) + ); + + if (safeConfig.max_charging_current !== undefined) { + safeConfig.max_charging_current = clampAmp(safeConfig.max_charging_current); + } + if (safeConfig.temperature_limit !== undefined) { + safeConfig.temperature_limit = clampTemp(safeConfig.temperature_limit); + } + + const onlyAmp = + Object.keys(safeConfig).length === 1 && safeConfig.max_charging_current !== undefined; + + if ( + existing && + onlyAmp && + Number(existing.max_charging_current) === Number(safeConfig.max_charging_current) + ) { + return { data: existing, message: 'Config unchanged' }; + } + + if (tooSoon && existing && onlyAmp) { + return { data: existing, message: 'Throttled' }; + } + + const updated = await chargersRepo.upsertConfig(id, { + ...safeConfig, + config_received_at: new Date().toISOString(), + }); + + lastConfigUpdateAt.set(id, nowMs); + + const evseSettings = {}; + if (safeConfig.max_charging_current !== undefined) { + evseSettings.currentLimit = Number(safeConfig.max_charging_current); + } + if (safeConfig.temperature_limit !== undefined) { + evseSettings.temperatureLimit = Number(safeConfig.temperature_limit); + } + if (Object.keys(evseSettings).length > 0) { + mqttClient.sendEvseSettings(charger.mqtt_topic, evseSettings); + } + + return { data: updated }; +} + +async function getSchedules(userId, id) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + return chargersRepo.listSchedules(id); +} + +async function createSchedule(userId, id, start, end, repeat) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado'); + + const row = await chargersRepo.insertSchedule({ + charger_id: id, + start, + end, + repeat, + created_at: new Date().toISOString(), + }); + + return row; +} + +async function action(userId, id, actionName, ampLimit) { + const charger = await chargersRepo.findByIdForUser(id, userId); + if (!charger) throw httpError(404, 'Carregador não encontrado ou não autorizado'); + + if (ampLimit !== undefined) { + const safeAmp = clampAmp(ampLimit); + + await chargersRepo.upsertConfig(id, { + max_charging_current: safeAmp, + config_received_at: new Date().toISOString(), + }); + + mqttClient.sendEvseSettings(charger.mqtt_topic, { currentLimit: safeAmp }); + } + + const enable = actionName === 'start'; + mqttClient.sendEnable(charger.mqtt_topic, enable); + + return true; +} + +module.exports = { + list, + getOne, + create, + update, + remove, + updateConfig, + getSchedules, + createSchedule, + action, +}; diff --git a/src/services/configs.service.js b/src/services/configs.service.js new file mode 100755 index 0000000..e69de29 diff --git a/utils/pushService.js b/src/services/push.service.js old mode 100644 new mode 100755 similarity index 55% rename from utils/pushService.js rename to src/services/push.service.js index 61f36ad..58c0bf7 --- a/utils/pushService.js +++ b/src/services/push.service.js @@ -1,21 +1,16 @@ -// utils/pushService.js +// src/services/push.service.js const webpush = require('web-push'); -const db = require('../db'); +const config = require('../config'); +const pushRepo = require('../repositories/push.repo'); -const hasVapid = - !!process.env.VAPID_PUBLIC_KEY && !!process.env.VAPID_PRIVATE_KEY; +const hasVapid = !!config.vapid.publicKey && !!config.vapid.privateKey; if (!hasVapid) { console.warn('[Push] VAPID keys não definidas. Push desativado.'); } else { - webpush.setVapidDetails( - process.env.VAPID_SUBJECT || 'mailto:admin@evstation.local', - process.env.VAPID_PUBLIC_KEY, - process.env.VAPID_PRIVATE_KEY - ); + webpush.setVapidDetails(config.vapid.subject, config.vapid.publicKey, config.vapid.privateKey); } -// retry simples p/ 429 e 5xx async function sendWithRetry(subscription, message, tries = 2) { try { return await webpush.sendNotification(subscription, message); @@ -32,10 +27,7 @@ async function sendWithRetry(subscription, message, tries = 2) { async function sendPushToUser(userId, payload) { if (!hasVapid) return; - const subs = await db('push_subscriptions') - .where({ user_id: userId }) - .select('id', 'endpoint', 'p256dh', 'auth'); - + const subs = await pushRepo.listByUser(userId); if (!subs.length) return; const message = JSON.stringify(payload); @@ -44,26 +36,15 @@ async function sendPushToUser(userId, payload) { subs.map(async (s) => { const subscription = { endpoint: s.endpoint, - keys: { - p256dh: s.p256dh, - auth: s.auth, - }, + keys: { p256dh: s.p256dh, auth: s.auth }, }; try { await sendWithRetry(subscription, message); - - // TTL opcional (default 15min) - // se quiseres usar TTL do payload, descomenta e passa options no sendWithRetry - // await webpush.sendNotification(subscription, message, { - // TTL: payload.ttl ?? 60 * 15, - // }); } catch (err) { const code = err?.statusCode; - - // remove subscriptions mortas if (code === 404 || code === 410) { - await db('push_subscriptions').where({ id: s.id }).del(); + await pushRepo.deleteById(s.id); } else { console.error('[Push] erro ao enviar:', err.message); } diff --git a/src/services/pushHttp.service.js b/src/services/pushHttp.service.js new file mode 100644 index 0000000..282ab65 --- /dev/null +++ b/src/services/pushHttp.service.js @@ -0,0 +1,38 @@ +// src/services/pushHttp.service.js +const pushRepo = require('../repositories/push.repo'); +const { httpError } = require('../utils/httpError'); + +async function subscribe(userId, endpoint, keys, userAgent) { + if (!endpoint || !keys?.p256dh || !keys?.auth) { + throw httpError(400, 'Subscription inválida'); + } + + // dedupe (mesmo user) + const existing = await pushRepo.findByUserAndEndpoint(userId, endpoint); + if (existing) return { row: existing, created: false }; + + // como endpoint é UNIQUE na tabela, evita conflito com outro user + const usedByOther = await pushRepo.findByEndpoint(endpoint); + if (usedByOther && usedByOther.user_id !== userId) { + throw httpError(409, 'Este endpoint já está associado a outro utilizador'); + } + + const inserted = await pushRepo.insertSubscription({ + user_id: userId, + endpoint, + p256dh: keys.p256dh, + auth: keys.auth, + user_agent: userAgent || null, + created_at: new Date().toISOString(), + }); + + return { row: inserted, created: true }; +} + +async function unsubscribe(userId, endpoint) { + if (!endpoint) return { ok: true, message: 'No subscription' }; + await pushRepo.deleteByUserAndEndpoint(userId, endpoint); + return { ok: true, message: 'Unsubscribed' }; +} + +module.exports = { subscribe, unsubscribe }; diff --git a/src/services/sessions.service.js b/src/services/sessions.service.js new file mode 100755 index 0000000..08e6f21 --- /dev/null +++ b/src/services/sessions.service.js @@ -0,0 +1,57 @@ +// src/services/sessions.service.js +const chargersRepo = require('../repositories/chargers.repo'); +const sessionsRepo = require('../repositories/sessions.repo'); +const { httpError } = require('../utils/httpError'); + +function stripUndef(obj) { + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); +} + +async function listByCharger(userId, chargerId) { + const charger = await chargersRepo.findByIdForUser(chargerId, userId); + if (!charger) throw httpError(403, 'Acesso não autorizado'); + + return sessionsRepo.listByCharger(chargerId); +} + +async function history(userId, chargerId, viewMode) { + const charger = await chargersRepo.findByIdForUser(chargerId, userId); + if (!charger) throw httpError(403, 'Acesso não autorizado'); + + const rows = await sessionsRepo.historyAgg(chargerId, viewMode); + if (!rows.length) return []; + + return rows.map((r) => ({ + started_at: r.period, + kwh: parseFloat(r.total_kwh) || 0, + })); +} + +async function getById(userId, sessionId) { + const session = await sessionsRepo.findByIdForUser(sessionId, userId); + if (!session) throw httpError(404, 'Sessão não encontrada'); + return session; +} + +async function create(userId, charger_id) { + const charger = await chargersRepo.findByIdForUser(charger_id, userId); + if (!charger) throw httpError(403, 'Acesso não autorizado'); + + return sessionsRepo.insertSession({ charger_id, started_at: new Date() }); +} + +async function update(userId, sessionId, patch) { + const session = await sessionsRepo.findByIdForUser(sessionId, userId); + if (!session) throw httpError(404, 'Sessão não encontrada'); + + const clean = stripUndef(patch); + return sessionsRepo.updateById(sessionId, clean); +} + +async function remove(userId, sessionId) { + const deleted = await sessionsRepo.deleteByIdForUser(sessionId, userId); + if (!deleted) throw httpError(404, 'Sessão não encontrada'); + return true; +} + +module.exports = { listByCharger, history, getById, create, update, remove }; diff --git a/src/services/users.service.js b/src/services/users.service.js new file mode 100644 index 0000000..0b45403 --- /dev/null +++ b/src/services/users.service.js @@ -0,0 +1,51 @@ +// src/services/users.service.js +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const config = require('../config'); +const usersRepo = require('../repositories/users.repo'); +const { httpError } = require('../utils/httpError'); + +async function login(username, password) { + if (!username || !password) { + throw httpError(400, 'Usuário e senha são obrigatórios'); + } + + const user = await usersRepo.findByUsername(username); + if (!user) throw httpError(401, 'Credenciais inválidas'); + + const ok = await bcrypt.compare(password, user.password); + if (!ok) throw httpError(401, 'Credenciais inválidas'); + + const token = jwt.sign({ id: user.id, username: user.username }, config.jwtSecret, { + expiresIn: '24h', + }); + + return { token }; +} + +async function register(username, password) { + if ( + !username || + !password || + typeof username !== 'string' || + typeof password !== 'string' || + username.length < 3 || + password.length < 4 + ) { + throw httpError( + 400, + 'Nome de usuário deve ter pelo menos 3 caracteres e senha pelo menos 4 caracteres' + ); + } + + const existing = await usersRepo.findByUsername(username); + if (existing) throw httpError(409, 'Nome de usuário já está em uso'); + + const hashed = await bcrypt.hash(password, 10); + const id = await usersRepo.insertUser({ username, passwordHash: hashed }); + + const token = jwt.sign({ id, username }, config.jwtSecret, { expiresIn: '24h' }); + return { token }; +} + +module.exports = { login, register }; diff --git a/src/utils/httpError.js b/src/utils/httpError.js new file mode 100644 index 0000000..29e31e6 --- /dev/null +++ b/src/utils/httpError.js @@ -0,0 +1,13 @@ +// src/utils/httpError.js +class HttpError extends Error { + constructor(statusCode, message) { + super(message); + this.statusCode = statusCode; + } +} + +function httpError(statusCode, message) { + return new HttpError(statusCode, message); +} + +module.exports = { HttpError, httpError };