Page MenuHomeDevCentral

D3963.diff
No OneTemporary

D3963.diff

diff --git a/.env.example b/.env.example
--- a/.env.example
+++ b/.env.example
@@ -6,6 +6,7 @@
# Backend
EXPRESS_PORT=3000
+JWT_SECRET=change-this-to-a-random-string
# Frontend
VITE_API_URL=http://localhost:3000/api
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -65,6 +65,7 @@
├── backend/ # Express.js API server
│ ├── config/ # App and database configuration
│ ├── controllers/ # Request handlers
+│ ├── middleware/ # Authentication middleware
│ ├── models/ # Data access layer
│ └── routes/ # API route definitions
├── frontend/ # Vue.js application
@@ -79,12 +80,31 @@
## API Endpoints
-| Method | Endpoint | Description |
-|--------|-----------------------|----------------------|
-| GET | `/api/service/getAll` | List all services |
-| POST | `/api/service` | Create a service |
-| GET | `/api/incident/getAll`| List all incidents |
-| GET | `/api/config/getAll` | Get app configuration|
+### Public (no authentication)
+
+| Method | Endpoint | Description |
+|--------|-------------------------|------------------------|
+| GET | `/api/services` | List all services |
+| GET | `/api/services/:id` | Get a service |
+| GET | `/api/incidents` | List all incidents |
+| GET | `/api/incidents/:id` | Get incident + updates |
+| GET | `/api/maintenances` | List maintenances |
+| GET | `/api/maintenances/:id` | Get a maintenance |
+| GET | `/api/config/getAll` | Get app configuration |
+
+### Admin (requires JWT Bearer token)
+
+| Method | Endpoint | Description |
+|--------|--------------------------------|------------------------|
+| POST | `/api/services` | Create a service |
+| PUT | `/api/services/:id` | Update a service |
+| DELETE | `/api/services/:id` | Delete a service |
+| POST | `/api/incidents` | Create an incident |
+| PUT | `/api/incidents/:id` | Update an incident |
+| PUT | `/api/incidents/:id/resolve` | Resolve an incident |
+| POST | `/api/maintenances` | Create maintenance |
+| PUT | `/api/maintenances/:id` | Update maintenance |
+| DELETE | `/api/maintenances/:id` | Delete maintenance |
## Contributing
diff --git a/backend/Dockerfile b/backend/Dockerfile
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -9,4 +9,4 @@
EXPOSE 3000
-CMD ["node", "app.js"]
+CMD ["npm", "start"]
diff --git a/backend/app.js b/backend/app.js
--- a/backend/app.js
+++ b/backend/app.js
@@ -21,11 +21,13 @@
const serviceRoutes = require('./routes/serviceRoutes.js');
const incidentRoutes = require('./routes/incidentRoutes.js');
const configRoutes = require('./routes/configRoutes.js');
+const maintenanceRoutes = require('./routes/maintenanceRoutes.js');
// Use the routes
app.use('/api', serviceRoutes);
app.use('/api', incidentRoutes);
app.use('/api', configRoutes);
+app.use('/api', maintenanceRoutes);
// Start the server
const PORT = process.env.EXPRESS_PORT || 3000;
diff --git a/backend/controllers/incidentController.js b/backend/controllers/incidentController.js
--- a/backend/controllers/incidentController.js
+++ b/backend/controllers/incidentController.js
@@ -1,12 +1,71 @@
const incidentModel = require('../models/incidentModel.js');
+const incidentUpdateModel = require('../models/incidentUpdateModel.js');
+
+const createIncident = async (req, res) => {
+ try {
+ const result = await incidentModel.createIncident(req.body);
+ res.status(201).json(result.rows[0]);
+ } catch (error) {
+ res.status(500).json({ message: 'Error creating incident', error: error.message });
+ }
+};
const getIncidents = async (req, res) => {
try {
- const incidents = await incidentModel.getIncidents();
- res.status(200).json(incidents);
+ const result = await incidentModel.getIncidents();
+ res.status(200).json(result.rows);
+ } catch (error) {
+ res.status(500).json({ message: 'Error fetching incidents', error: error.message });
+ }
+};
+
+const getIncidentById = async (req, res) => {
+ try {
+ const incident = await incidentModel.getIncidentById(req.params.id);
+ if (incident.rows.length === 0) {
+ return res.status(404).json({ message: 'Incident not found' });
+ }
+ const updates = await incidentUpdateModel.getUpdatesByIncidentId(req.params.id);
+ res.status(200).json({ ...incident.rows[0], updates: updates.rows });
+ } catch (error) {
+ res.status(500).json({ message: 'Error fetching incident', error: error.message });
+ }
+};
+
+const updateIncident = async (req, res) => {
+ try {
+ const result = await incidentModel.updateIncident(req.params.id, req.body);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Incident not found' });
+ }
+ if (req.body.message) {
+ await incidentUpdateModel.addUpdate({
+ incident_id: req.params.id,
+ status: req.body.status,
+ message: req.body.message
+ });
+ }
+ res.status(200).json(result.rows[0]);
+ } catch (error) {
+ res.status(500).json({ message: 'Error updating incident', error: error.message });
+ }
+};
+
+const resolveIncident = async (req, res) => {
+ try {
+ const result = await incidentModel.resolveIncident(req.params.id);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Incident not found' });
+ }
+ await incidentUpdateModel.addUpdate({
+ incident_id: req.params.id,
+ status: 'resolved',
+ message: req.body.message || 'Incident resolved'
+ });
+ res.status(200).json(result.rows[0]);
} catch (error) {
- res.status(500).json({ message: 'Error fetching services', error: error.message });
+ res.status(500).json({ message: 'Error resolving incident', error: error.message });
}
};
-module.exports = { getIncidents };
+module.exports = { createIncident, getIncidents, getIncidentById, updateIncident, resolveIncident };
diff --git a/backend/controllers/maintenanceController.js b/backend/controllers/maintenanceController.js
new file mode 100644
--- /dev/null
+++ b/backend/controllers/maintenanceController.js
@@ -0,0 +1,57 @@
+const maintenanceModel = require('../models/maintenanceModel.js');
+
+const createMaintenance = async (req, res) => {
+ try {
+ const result = await maintenanceModel.createMaintenance(req.body);
+ res.status(201).json(result.rows[0]);
+ } catch (error) {
+ res.status(500).json({ message: 'Error creating maintenance', error: error.message });
+ }
+};
+
+const getMaintenances = async (req, res) => {
+ try {
+ const result = await maintenanceModel.getMaintenances();
+ res.status(200).json(result.rows);
+ } catch (error) {
+ res.status(500).json({ message: 'Error fetching maintenances', error: error.message });
+ }
+};
+
+const getMaintenanceById = async (req, res) => {
+ try {
+ const result = await maintenanceModel.getMaintenanceById(req.params.id);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Maintenance not found' });
+ }
+ res.status(200).json(result.rows[0]);
+ } catch (error) {
+ res.status(500).json({ message: 'Error fetching maintenance', error: error.message });
+ }
+};
+
+const updateMaintenance = async (req, res) => {
+ try {
+ const result = await maintenanceModel.updateMaintenance(req.params.id, req.body);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Maintenance not found' });
+ }
+ res.status(200).json(result.rows[0]);
+ } catch (error) {
+ res.status(500).json({ message: 'Error updating maintenance', error: error.message });
+ }
+};
+
+const deleteMaintenance = async (req, res) => {
+ try {
+ const result = await maintenanceModel.deleteMaintenance(req.params.id);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Maintenance not found' });
+ }
+ res.status(200).json({ message: 'Maintenance deleted' });
+ } catch (error) {
+ res.status(500).json({ message: 'Error deleting maintenance', error: error.message });
+ }
+};
+
+module.exports = { createMaintenance, getMaintenances, getMaintenanceById, updateMaintenance, deleteMaintenance };
diff --git a/backend/controllers/serviceController.js b/backend/controllers/serviceController.js
--- a/backend/controllers/serviceController.js
+++ b/backend/controllers/serviceController.js
@@ -2,10 +2,8 @@
const addService = async (req, res) => {
try {
- const serviceData = req.body;
- const result = await serviceModel.addService(serviceData);
-
- res.status(201).json(result);
+ const result = await serviceModel.addService(req.body);
+ res.status(201).json(result.rows[0]);
} catch (error) {
res.status(500).json({ message: 'Error creating service', error: error.message });
}
@@ -13,11 +11,47 @@
const getServices = async (req, res) => {
try {
- const services = await serviceModel.getServices();
- res.status(200).json(services);
+ const result = await serviceModel.getServices();
+ res.status(200).json(result.rows);
} catch (error) {
res.status(500).json({ message: 'Error fetching services', error: error.message });
}
};
-module.exports = { addService, getServices };
+const getServiceById = async (req, res) => {
+ try {
+ const result = await serviceModel.getServiceById(req.params.id);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Service not found' });
+ }
+ res.status(200).json(result.rows[0]);
+ } catch (error) {
+ res.status(500).json({ message: 'Error fetching service', error: error.message });
+ }
+};
+
+const updateService = async (req, res) => {
+ try {
+ const result = await serviceModel.updateService(req.params.id, req.body);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Service not found' });
+ }
+ res.status(200).json(result.rows[0]);
+ } catch (error) {
+ res.status(500).json({ message: 'Error updating service', error: error.message });
+ }
+};
+
+const deleteService = async (req, res) => {
+ try {
+ const result = await serviceModel.deleteService(req.params.id);
+ if (result.rows.length === 0) {
+ return res.status(404).json({ message: 'Service not found' });
+ }
+ res.status(200).json({ message: 'Service deleted' });
+ } catch (error) {
+ res.status(500).json({ message: 'Error deleting service', error: error.message });
+ }
+};
+
+module.exports = { addService, getServices, getServiceById, updateService, deleteService };
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
new file mode 100644
--- /dev/null
+++ b/backend/middleware/auth.js
@@ -0,0 +1,25 @@
+const jwt = require('jsonwebtoken');
+
+const SECRET = process.env.JWT_SECRET || 'servpulse-dev-secret';
+
+const generateToken = (payload) => {
+ return jwt.sign(payload, SECRET, { expiresIn: '24h' });
+};
+
+const authenticate = (req, res, next) => {
+ const header = req.headers.authorization;
+
+ if (!header || !header.startsWith('Bearer ')) {
+ return res.status(401).json({ message: 'Authentication required' });
+ }
+
+ try {
+ const token = header.split(' ')[1];
+ req.user = jwt.verify(token, SECRET);
+ next();
+ } catch (error) {
+ return res.status(401).json({ message: 'Invalid or expired token' });
+ }
+};
+
+module.exports = { generateToken, authenticate };
diff --git a/backend/models/incidentModel.js b/backend/models/incidentModel.js
--- a/backend/models/incidentModel.js
+++ b/backend/models/incidentModel.js
@@ -2,16 +2,40 @@
const createIncident = async (data) => {
return await pool.query(`
- INSERT INTO incident (title, start_date, update_date, type_id, status)
- VALUES (\$1, \$2, \$3, \$4, \$5)
+ INSERT INTO incident (title, start_date, type_id, status, impact)
+ VALUES ($1, NOW(), $2, $3, $4)
RETURNING *
- `, [data.title, data.start_date, data.update_date, data.type_id, data.status]);
- };
+ `, [data.title, data.type_id || 1, data.status || 'investigating', data.impact || 'none']);
+};
const getIncidents = async () => {
return await pool.query(`
- SELECT * FROM incident;
+ SELECT * FROM incident ORDER BY start_date DESC;
`);
};
-module.exports = { createIncident, getIncidents };
+const getIncidentById = async (id) => {
+ return await pool.query(`
+ SELECT * FROM incident WHERE id = $1;
+ `, [id]);
+};
+
+const updateIncident = async (id, data) => {
+ return await pool.query(`
+ UPDATE incident
+ SET title = $1, status = $2, impact = $3, update_date = NOW()
+ WHERE id = $4
+ RETURNING *
+ `, [data.title, data.status, data.impact, id]);
+};
+
+const resolveIncident = async (id) => {
+ return await pool.query(`
+ UPDATE incident
+ SET status = 'resolved', end_date = NOW(), update_date = NOW()
+ WHERE id = $1
+ RETURNING *
+ `, [id]);
+};
+
+module.exports = { createIncident, getIncidents, getIncidentById, updateIncident, resolveIncident };
diff --git a/backend/models/incidentUpdateModel.js b/backend/models/incidentUpdateModel.js
new file mode 100644
--- /dev/null
+++ b/backend/models/incidentUpdateModel.js
@@ -0,0 +1,19 @@
+const pool = require('../config/database.js');
+
+const addUpdate = async (data) => {
+ return await pool.query(`
+ INSERT INTO incident_update (incident_id, status, message)
+ VALUES ($1, $2, $3)
+ RETURNING *
+ `, [data.incident_id, data.status, data.message]);
+};
+
+const getUpdatesByIncidentId = async (incidentId) => {
+ return await pool.query(`
+ SELECT * FROM incident_update
+ WHERE incident_id = $1
+ ORDER BY created_at DESC;
+ `, [incidentId]);
+};
+
+module.exports = { addUpdate, getUpdatesByIncidentId };
diff --git a/backend/models/maintenanceModel.js b/backend/models/maintenanceModel.js
new file mode 100644
--- /dev/null
+++ b/backend/models/maintenanceModel.js
@@ -0,0 +1,38 @@
+const pool = require('../config/database.js');
+
+const createMaintenance = async (data) => {
+ return await pool.query(`
+ INSERT INTO maintenance (title, description, scheduled_start, scheduled_end)
+ VALUES ($1, $2, $3, $4)
+ RETURNING *
+ `, [data.title, data.description, data.scheduled_start, data.scheduled_end]);
+};
+
+const getMaintenances = async () => {
+ return await pool.query(`
+ SELECT * FROM maintenance ORDER BY scheduled_start DESC;
+ `);
+};
+
+const getMaintenanceById = async (id) => {
+ return await pool.query(`
+ SELECT * FROM maintenance WHERE id = $1;
+ `, [id]);
+};
+
+const updateMaintenance = async (id, data) => {
+ return await pool.query(`
+ UPDATE maintenance
+ SET title = $1, description = $2, scheduled_start = $3, scheduled_end = $4, status = $5
+ WHERE id = $6
+ RETURNING *
+ `, [data.title, data.description, data.scheduled_start, data.scheduled_end, data.status, id]);
+};
+
+const deleteMaintenance = async (id) => {
+ return await pool.query(`
+ DELETE FROM maintenance WHERE id = $1 RETURNING *;
+ `, [id]);
+};
+
+module.exports = { createMaintenance, getMaintenances, getMaintenanceById, updateMaintenance, deleteMaintenance };
diff --git a/backend/models/serviceModel.js b/backend/models/serviceModel.js
--- a/backend/models/serviceModel.js
+++ b/backend/models/serviceModel.js
@@ -3,16 +3,37 @@
const addService = async (data) => {
return await pool.query(`
INSERT INTO service
- (name, "group", description, status)
- VALUES (\$1, \$2, \$3, \$4)
+ (name, "group", description, status, "order")
+ VALUES ($1, $2, $3, $4, $5)
RETURNING *
- `, [data.name, data.group, data.description, data.status]);
+ `, [data.name, data.group, data.description, data.status || 'operational', data.order || 0]);
};
const getServices = async () => {
return await pool.query(`
- SELECT * FROM service;
+ SELECT * FROM service ORDER BY "order", "group", name;
`);
};
-module.exports = { addService, getServices };
+const getServiceById = async (id) => {
+ return await pool.query(`
+ SELECT * FROM service WHERE id = $1;
+ `, [id]);
+};
+
+const updateService = async (id, data) => {
+ return await pool.query(`
+ UPDATE service
+ SET name = $1, "group" = $2, description = $3, status = $4, "order" = $5, updated_at = NOW()
+ WHERE id = $6
+ RETURNING *
+ `, [data.name, data.group, data.description, data.status, data.order, id]);
+};
+
+const deleteService = async (id) => {
+ return await pool.query(`
+ DELETE FROM service WHERE id = $1 RETURNING *;
+ `, [id]);
+};
+
+module.exports = { addService, getServices, getServiceById, updateService, deleteService };
diff --git a/backend/package-lock.json b/backend/package-lock.json
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -9,43 +9,14 @@
"version": "1.0.0",
"license": "BSD-2-Clause",
"dependencies": {
+ "bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
- "pg": "^8.11.0",
- "sequelize": "^6.31.1"
+ "jsonwebtoken": "^9.0.0",
+ "pg": "^8.11.0"
}
},
- "node_modules/@types/debug": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
- "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
- "license": "MIT",
- "dependencies": {
- "@types/ms": "*"
- }
- },
- "node_modules/@types/ms": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
- "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "25.2.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
- "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~7.16.0"
- }
- },
- "node_modules/@types/validator": {
- "version": "13.15.10",
- "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
- "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
- "license": "MIT"
- },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -65,6 +36,12 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@@ -89,6 +66,12 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -220,12 +203,6 @@
"url": "https://dotenvx.com"
}
},
- "node_modules/dottie": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
- "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
- "license": "MIT"
- },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -240,6 +217,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -496,15 +482,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/inflection": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
- "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
- "engines": [
- "node >= 0.4.0"
- ],
- "license": "MIT"
- },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -520,10 +497,95 @@
"node": ">= 0.10"
}
},
- "node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
@@ -595,27 +657,6 @@
"node": ">= 0.6"
}
},
- "node_modules/moment": {
- "version": "2.30.1",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
- "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/moment-timezone": {
- "version": "0.5.48",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
- "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
- "license": "MIT",
- "dependencies": {
- "moment": "^2.29.4"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -859,12 +900,6 @@
"node": ">= 0.8"
}
},
- "node_modules/retry-as-promised": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
- "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
- "license": "MIT"
- },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -933,100 +968,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
- "node_modules/sequelize": {
- "version": "6.37.7",
- "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
- "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/sequelize"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@types/debug": "^4.1.8",
- "@types/validator": "^13.7.17",
- "debug": "^4.3.4",
- "dottie": "^2.0.6",
- "inflection": "^1.13.4",
- "lodash": "^4.17.21",
- "moment": "^2.29.4",
- "moment-timezone": "^0.5.43",
- "pg-connection-string": "^2.6.1",
- "retry-as-promised": "^7.0.4",
- "semver": "^7.5.4",
- "sequelize-pool": "^7.1.0",
- "toposort-class": "^1.0.1",
- "uuid": "^8.3.2",
- "validator": "^13.9.0",
- "wkx": "^0.5.0"
- },
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependenciesMeta": {
- "ibm_db": {
- "optional": true
- },
- "mariadb": {
- "optional": true
- },
- "mysql2": {
- "optional": true
- },
- "oracledb": {
- "optional": true
- },
- "pg": {
- "optional": true
- },
- "pg-hstore": {
- "optional": true
- },
- "snowflake-sdk": {
- "optional": true
- },
- "sqlite3": {
- "optional": true
- },
- "tedious": {
- "optional": true
- }
- }
- },
- "node_modules/sequelize-pool": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
- "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
- "license": "MIT",
- "engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/sequelize/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/sequelize/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
@@ -1147,12 +1088,6 @@
"node": ">=0.6"
}
},
- "node_modules/toposort-class": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
- "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
- "license": "MIT"
- },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1166,12 +1101,6 @@
"node": ">= 0.6"
}
},
- "node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
- "license": "MIT"
- },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1190,24 +1119,6 @@
"node": ">= 0.4.0"
}
},
- "node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/validator": {
- "version": "13.15.26",
- "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
- "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -1217,15 +1128,6 @@
"node": ">= 0.8"
}
},
- "node_modules/wkx": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
- "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/backend/package.json b/backend/package.json
--- a/backend/package.json
+++ b/backend/package.json
@@ -2,17 +2,20 @@
"name": "servpulse-backend",
"version": "1.0.0",
"description": "Backend API for the ServPulse frontend.",
- "main": "index.js",
+ "main": "app.js",
"scripts": {
+ "start": "node app.js",
+ "dev": "node --watch app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "BSD-2-Clause",
"dependencies": {
+ "bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
- "pg": "^8.11.0",
- "sequelize": "^6.31.1"
+ "jsonwebtoken": "^9.0.0",
+ "pg": "^8.11.0"
}
}
diff --git a/backend/routes/incidentRoutes.js b/backend/routes/incidentRoutes.js
--- a/backend/routes/incidentRoutes.js
+++ b/backend/routes/incidentRoutes.js
@@ -1,7 +1,15 @@
const express = require('express');
const router = express.Router();
const incidentController = require('../controllers/incidentController.js');
+const { authenticate } = require('../middleware/auth.js');
+router.get('/incidents', incidentController.getIncidents);
+router.get('/incidents/:id', incidentController.getIncidentById);
+router.post('/incidents', authenticate, incidentController.createIncident);
+router.put('/incidents/:id', authenticate, incidentController.updateIncident);
+router.put('/incidents/:id/resolve', authenticate, incidentController.resolveIncident);
+
+// Legacy endpoint
router.get('/incident/getAll', incidentController.getIncidents);
module.exports = router;
diff --git a/backend/routes/maintenanceRoutes.js b/backend/routes/maintenanceRoutes.js
new file mode 100644
--- /dev/null
+++ b/backend/routes/maintenanceRoutes.js
@@ -0,0 +1,12 @@
+const express = require('express');
+const router = express.Router();
+const maintenanceController = require('../controllers/maintenanceController.js');
+const { authenticate } = require('../middleware/auth.js');
+
+router.get('/maintenances', maintenanceController.getMaintenances);
+router.get('/maintenances/:id', maintenanceController.getMaintenanceById);
+router.post('/maintenances', authenticate, maintenanceController.createMaintenance);
+router.put('/maintenances/:id', authenticate, maintenanceController.updateMaintenance);
+router.delete('/maintenances/:id', authenticate, maintenanceController.deleteMaintenance);
+
+module.exports = router;
diff --git a/backend/routes/serviceRoutes.js b/backend/routes/serviceRoutes.js
--- a/backend/routes/serviceRoutes.js
+++ b/backend/routes/serviceRoutes.js
@@ -1,8 +1,16 @@
const express = require('express');
const router = express.Router();
const serviceController = require('../controllers/serviceController.js');
+const { authenticate } = require('../middleware/auth.js');
+router.get('/services', serviceController.getServices);
+router.get('/services/:id', serviceController.getServiceById);
+router.post('/services', authenticate, serviceController.addService);
+router.put('/services/:id', authenticate, serviceController.updateService);
+router.delete('/services/:id', authenticate, serviceController.deleteService);
+
+// Legacy endpoints
router.get('/service/getAll', serviceController.getServices);
-router.post('/service', serviceController.addService);
+router.post('/service', authenticate, serviceController.addService);
module.exports = router;
diff --git a/database/init.sql b/database/init.sql
--- a/database/init.sql
+++ b/database/init.sql
@@ -6,19 +6,69 @@
-- License: MIT
-- -------------------------------------------------------------
+--
+-- Services
+--
+
CREATE TABLE IF NOT EXISTS service (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
"group" VARCHAR(255),
description TEXT,
- status VARCHAR(50) NOT NULL DEFAULT 'operational'
+ status VARCHAR(50) NOT NULL DEFAULT 'operational',
+ "order" INTEGER DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
+--
+-- Incidents
+--
+
CREATE TABLE IF NOT EXISTS incident (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
start_date TIMESTAMP NOT NULL DEFAULT NOW(),
update_date TIMESTAMP,
- type_id INTEGER,
- status VARCHAR(50) NOT NULL DEFAULT 'investigating'
+ end_date TIMESTAMP,
+ type_id INTEGER DEFAULT 1,
+ status VARCHAR(50) NOT NULL DEFAULT 'investigating',
+ impact VARCHAR(50) NOT NULL DEFAULT 'none',
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS incident_update (
+ id SERIAL PRIMARY KEY,
+ incident_id INTEGER NOT NULL REFERENCES incident(id) ON DELETE CASCADE,
+ status VARCHAR(50) NOT NULL,
+ message TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS incident_service (
+ id SERIAL PRIMARY KEY,
+ incident_id INTEGER NOT NULL REFERENCES incident(id) ON DELETE CASCADE,
+ service_id INTEGER NOT NULL REFERENCES service(id) ON DELETE CASCADE,
+ UNIQUE(incident_id, service_id)
+);
+
+--
+-- Scheduled maintenance
+--
+
+CREATE TABLE IF NOT EXISTS maintenance (
+ id SERIAL PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ description TEXT,
+ scheduled_start TIMESTAMP NOT NULL,
+ scheduled_end TIMESTAMP NOT NULL,
+ status VARCHAR(50) NOT NULL DEFAULT 'scheduled',
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS maintenance_service (
+ id SERIAL PRIMARY KEY,
+ maintenance_id INTEGER NOT NULL REFERENCES maintenance(id) ON DELETE CASCADE,
+ service_id INTEGER NOT NULL REFERENCES service(id) ON DELETE CASCADE,
+ UNIQUE(maintenance_id, service_id)
);

File Metadata

Mime Type
text/plain
Expires
Tue, Feb 17, 03:01 (18 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3454384
Default Alt Text
D3963.diff (36 KB)

Event Timeline