Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F24367909
D3963.id10271.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
36 KB
Referenced Files
None
Subscribers
None
D3963.id10271.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Tue, Feb 17, 01:45 (16 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3454384
Default Alt Text
D3963.id10271.diff (36 KB)
Attached To
Mode
D3963: Phase 2: Backend refactor with full CRUD API, expanded schema, and JWT auth
Attached
Detach File
Event Timeline
Log In to Comment