feat: initialize microservice architecture with auth, api, realtime, copernicus, ml, and console modules

This commit is contained in:
Giuseppe Raffa
2026-03-28 15:29:34 +01:00
commit bcfce32adb
89 changed files with 12025 additions and 0 deletions

27
api/src/routes/data.js Normal file
View File

@@ -0,0 +1,27 @@
// Collezione di tutte le api che usando influxdb e la telemetria della barca
//api.mebboat.it/data
const router = require('express').Router();
const { write, query, writeBatch } = require('../storage/influx');
router.post('/write', async (req, res) => {
const { measurement, sensor, data } = req.body;
await write(measurement, sensor, data);
res.json({ status: "ok" });
});
router.post('/write/batch', async (req, res) => {
const { points } = req.body;
await writeBatch(points);
res.json({ result: "added" });
});
router.get('/query', async (req, res) => {
const { collection, timeFromNow, measurement, sensor, field } = req.query;
const result = await query(collection, timeFromNow, measurement, sensor, field);
res.json(result);
});
module.exports = router;

216
api/src/routes/params.js Normal file
View File

@@ -0,0 +1,216 @@
const router = require('express').Router();
const { query } = require('../storage/postgres');
const sets = ['forecasts', 'marine', 'sensors', 'telemetry'];
/* ─── READS ─── */
/**
* GET /params/active?set=sensors
* Restituisce il set attivo per il tipo richiesto
*/
router.get('/active', async (req, res) => {
const { set } = req.query;
if (!set || !sets.includes(set))
return res.status(400).json({ error: 'SET parameter invalid' });
try {
const result = await query(`SELECT * FROM ${set} WHERE active = true LIMIT 1`, [], 'references');
res.json(result.rows[0] || null);
} catch (err) {
console.error('[PARAMS] Error reading active set:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /params/sets?type=sensors
* Lista tutti i set di un tipo (con paginazione)
*/
router.get('/sets', async (req, res) => {
const { type } = req.query;
if (!type || !sets.includes(type))
return res.status(400).json({ error: 'type parameter invalid' });
try {
const result = await query(
`SELECT id, name, description, mayor, minor, patch, state, active, tags, created_at, updated_at
FROM ${type} ORDER BY updated_at DESC`,
[], 'references'
);
res.json(result.rows);
} catch (err) {
console.error('[PARAMS] Error listing sets:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* GET /params/sets/:id?type=sensors
* Restituisce un singolo set completo (con content)
*/
router.get('/sets/:id', async (req, res) => {
const { type } = req.query;
const { id } = req.params;
if (!type || !sets.includes(type))
return res.status(400).json({ error: 'type parameter invalid' });
try {
const result = await query(`SELECT * FROM ${type} WHERE id = $1`, [id], 'references');
if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' });
res.json(result.rows[0]);
} catch (err) {
console.error('[PARAMS] Error reading set:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/* ─── WRITES ─── */
/**
* POST /params/sets?type=sensors
* Crea un nuovo set di regole
*/
router.post('/sets', async (req, res) => {
const { type } = req.query;
if (!type || !sets.includes(type))
return res.status(400).json({ error: 'type parameter invalid' });
const { name, description, content, tags } = req.body;
if (!name || !content) return res.status(400).json({ error: 'name and content required' });
try {
const result = await query(
`INSERT INTO ${type} (name, description, mayor, minor, patch, state, active, tags, content)
VALUES ($1, $2, 1, 0, 0, 'draft', false, $3, $4) RETURNING *`,
[name, description || '', tags || [], JSON.stringify(content)],
'references'
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error('[PARAMS] Error creating set:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* PUT /params/sets/:id?type=sensors
* Aggiorna un set esistente
*/
router.put('/sets/:id', async (req, res) => {
const { type } = req.query;
const { id } = req.params;
if (!type || !sets.includes(type))
return res.status(400).json({ error: 'type parameter invalid' });
const { name, description, content, tags, state } = req.body;
try {
const fields = [];
const values = [];
let idx = 1;
if (name !== undefined) { fields.push(`name = $${idx++}`); values.push(name); }
if (description !== undefined) { fields.push(`description = $${idx++}`); values.push(description); }
if (content !== undefined) { fields.push(`content = $${idx++}`); values.push(JSON.stringify(content)); }
if (tags !== undefined) { fields.push(`tags = $${idx++}`); values.push(tags); }
if (state !== undefined) { fields.push(`state = $${idx++}`); values.push(state); }
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
fields.push(`updated_at = NOW()`);
values.push(id);
const result = await query(
`UPDATE ${type} SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
values, 'references'
);
if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' });
res.json(result.rows[0]);
} catch (err) {
console.error('[PARAMS] Error updating set:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* POST /params/sets/:id/activate?type=sensors
* Attiva un set (disattiva tutti gli altri dello stesso tipo)
*/
router.post('/sets/:id/activate', async (req, res) => {
const { type } = req.query;
const { id } = req.params;
if (!type || !sets.includes(type))
return res.status(400).json({ error: 'type parameter invalid' });
try {
// Disattiva tutti
await query(`UPDATE ${type} SET active = false WHERE active = true`, [], 'references');
// Attiva quello richiesto
const result = await query(
`UPDATE ${type} SET active = true, state = 'active', updated_at = NOW() WHERE id = $1 RETURNING *`,
[id], 'references'
);
if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' });
res.json(result.rows[0]);
} catch (err) {
console.error('[PARAMS] Error activating set:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* DELETE /params/sets/:id?type=sensors
* Elimina un set (solo se non attivo)
*/
router.delete('/sets/:id', async (req, res) => {
const { type } = req.query;
const { id } = req.params;
if (!type || !sets.includes(type))
return res.status(400).json({ error: 'type parameter invalid' });
try {
// Controlla che non sia attivo
const check = await query(`SELECT active FROM ${type} WHERE id = $1`, [id], 'references');
if (!check.rows[0]) return res.status(404).json({ error: 'Set not found' });
if (check.rows[0].active) return res.status(409).json({ error: 'Cannot delete an active set' });
await query(`DELETE FROM ${type} WHERE id = $1`, [id], 'references');
res.json({ deleted: true });
} catch (err) {
console.error('[PARAMS] Error deleting set:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* PUT /params/sets/:id/version?type=sensors
* Incrementa la versione (bump)
*/
router.put('/sets/:id/version', async (req, res) => {
const { type } = req.query;
const { id } = req.params;
const { bump } = req.body; // 'major', 'minor', 'patch'
if (!type || !sets.includes(type))
return res.status(400).json({ error: 'type parameter invalid' });
const field = bump === 'major' ? 'mayor' : bump === 'minor' ? 'minor' : 'patch';
const resets = bump === 'major' ? ', minor = 0, patch = 0' : bump === 'minor' ? ', patch = 0' : '';
try {
const result = await query(
`UPDATE ${type} SET ${field} = ${field} + 1 ${resets}, updated_at = NOW() WHERE id = $1 RETURNING *`,
[id], 'references'
);
if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' });
res.json(result.rows[0]);
} catch (err) {
console.error('[PARAMS] Error bumping version:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@@ -0,0 +1,51 @@
const router = require('express').Router();
const crypto = require('crypto');
const { query } = require('../storage/postgres');
const sets = ['forecasts', 'sensors'];
function hashSensorCode(code) {
return crypto.createHash('sha256').update(code).digest('hex');
}
/**
* GET /params/sensor/:sensorCode/active?set=sensors
* Autenticazione tramite SENSOR_CODE (stesso meccanismo di realtime)
*/
router.get('/:sensorCode/active', async (req, res) => {
const { sensorCode } = req.params;
const { set } = req.query;
if (!set || !sets.includes(set))
return res.status(400).json({ error: 'SET parameter invalid' });
try {
const hashed = hashSensorCode(sensorCode);
const sensor = await query(
'SELECT id, is_active FROM sensors WHERE code_hash = $1',
[hashed],
'sensors'
);
if (!sensor.rows[0]) {
return res.status(401).json({ error: 'Sensor code not valid' });
}
if (!sensor.rows[0].is_active) {
return res.status(403).json({ error: 'Sensor is not active' });
}
const result = await query(
`SELECT * FROM ${set} WHERE active = true LIMIT 1`,
[],
'references'
);
res.json(result.rows[0] || null);
} catch (err) {
console.error('[PARAMS/SENSOR] Error:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

59
api/src/routes/storage.js Normal file
View File

@@ -0,0 +1,59 @@
// Collezzione di tutte le api che prendono i dati da minio
//api.mebboat.it/storage
const express = require('express');
const { getBuckets, getBucket, getObject, getObjects, getFileStream } = require('../storage/minio');
const router = express.Router();
/**
* Restituisce una lista con tutti i bucket del database,
* altrimenti restituisce i file del bucket passato come parametro
*
* @param {string} bucket - il bucket
*/
router.get('/', async (req, res) => {
const { bucket } = req.query;
if (bucket == undefined) {
const buckets = await getBuckets();
res.status(200).json(buckets);
} else {
const returnBucket = await getBucket(bucket);
res.status(200).json(returnBucket);
}
})
router.get('/files', async (req, res) => {
const { bucket, fileID } = req.query;
if (bucket == undefined) {
res.status(400).json({ error: "No bucket name in the request" });
} else {
if (fileID == undefined) {
const files = await getObjects(bucket);
res.status(200).json(files);
} else {
const file = await getObject(bucket, fileID);
res.status(200).json(file);
}
}
})
router.get('/file', async (req, res) => {
const { bucket, fileID } = req.query;
const stream = await getFileStream(bucket, fileID);
res.setHeader('Content-Type', 'application/octet-stream');
stream.pipe(res);
})
router.post('/upload', async (req, res) => {
const { bucket } = req.query;
const files = await getObjects(bucket);
res.status(200).json(files);
})
router.get('/download', async (req, res) => {
})
module.exports = router;