From 138e12daf6f64772e878b8222221d2cb58f63af4 Mon Sep 17 00:00:00 2001 From: Kushdeep Mittal Date: Wed, 10 Sep 2025 10:08:38 +0530 Subject: [PATCH] adding new feature --- README.md | 58 +++++++- config/database.js | 38 ++++- controllers/activityController.js | 239 ++++++++++++++++++++++++++++++ controllers/todoController.js | 173 +++++++++++++++------ routes/activityRoutes.js | 23 +++ server.js | 2 + 6 files changed, 483 insertions(+), 50 deletions(-) create mode 100644 controllers/activityController.js create mode 100644 routes/activityRoutes.js diff --git a/README.md b/README.md index 566cad7..268ac9e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ A modern, full-stack todo application built with Node.js, Express, and SQLite. F - 🚀 **REST API**: Clean, well-documented API endpoints - 📱 **Mobile Responsive**: Works perfectly on all device sizes - ⚡ **Real-time Updates**: Instant UI updates without page refresh +- 📝 **Activity Tracking**: Complete audit log of all user actions +- 🔍 **Activity Analytics**: Detailed statistics and activity monitoring ## Tech Stack @@ -77,6 +79,17 @@ A modern, full-stack todo application built with Node.js, Express, and SQLite. F | PUT | `/api/todos/:id` | Update a todo | `{ "title": "string", "description": "string", "completed": boolean }` | | DELETE | `/api/todos/:id` | Delete a todo | - | +### Activities + +| Method | Endpoint | Description | Query Parameters | +| ------ | -------------------------- | -------------------------- | -------------------------- | +| GET | `/api/activities` | Get all activities | `page`, `limit`, `todo_id` | +| GET | `/api/activities/stats` | Get activity statistics | - | +| GET | `/api/activities/todo/:id` | Get activities for a todo | - | +| GET | `/api/activities/:id` | Get a specific activity | - | +| DELETE | `/api/activities/:id` | Delete a specific activity | - | +| DELETE | `/api/activities` | Clear all activities | - | + ### Example API Usage **Create a new todo:** @@ -107,11 +120,36 @@ curl -X PUT http://localhost:3000/api/todos/1 \ curl -X DELETE http://localhost:3000/api/todos/1 ``` +**Get all activities:** + +```bash +curl http://localhost:3000/api/activities +``` + +**Get activities with pagination:** + +```bash +curl "http://localhost:3000/api/activities?page=1&limit=10" +``` + +**Get activities for a specific todo:** + +```bash +curl http://localhost:3000/api/activities/todo/1 +``` + +**Get activity statistics:** + +```bash +curl http://localhost:3000/api/activities/stats +``` + ## Database Schema The application uses SQLite with the following schema: ```sql +-- Todos table CREATE TABLE todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, @@ -120,6 +158,20 @@ CREATE TABLE todos ( created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); + +-- Activities table for audit logging +CREATE TABLE activities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + todo_id INTEGER, + action TEXT NOT NULL, + description TEXT, + old_value TEXT, + new_value TEXT, + user_ip TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (todo_id) REFERENCES todos (id) ON DELETE SET NULL +); ``` ## Project Structure @@ -129,9 +181,11 @@ coderabbit-review/ ├── config/ # Configuration files │ └── database.js # Database connection and setup ├── controllers/ # Business logic controllers -│ └── todoController.js # Todo-related controller functions +│ ├── todoController.js # Todo-related controller functions +│ └── activityController.js # Activity tracking controller ├── routes/ # API route definitions -│ └── todoRoutes.js # Todo API routes +│ ├── todoRoutes.js # Todo API routes +│ └── activityRoutes.js # Activity API routes ├── public/ # Frontend files │ ├── index.html # Main HTML file │ ├── style.css # CSS styles diff --git a/config/database.js b/config/database.js index 4589d22..cf54efc 100644 --- a/config/database.js +++ b/config/database.js @@ -23,7 +23,7 @@ class Database { initializeTables() { return new Promise((resolve, reject) => { - const createTableSQL = ` + const createTodosTableSQL = ` CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, @@ -34,14 +34,40 @@ class Database { ) `; - this.db.run(createTableSQL, (err) => { + const createActivityTableSQL = ` + CREATE TABLE IF NOT EXISTS activities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + todo_id INTEGER, + action TEXT NOT NULL, + description TEXT, + old_value TEXT, + new_value TEXT, + user_ip TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (todo_id) REFERENCES todos (id) ON DELETE SET NULL + ) + `; + + // Create todos table + this.db.run(createTodosTableSQL, (err) => { if (err) { - console.error('Error creating table:', err.message); + console.error('Error creating todos table:', err.message); reject(err); - } else { - console.log('Todos table ready'); - resolve(); + return; } + console.log('Todos table ready'); + + // Create activities table + this.db.run(createActivityTableSQL, (err) => { + if (err) { + console.error('Error creating activities table:', err.message); + reject(err); + return; + } + console.log('Activities table ready'); + resolve(); + }); }); }); } diff --git a/controllers/activityController.js b/controllers/activityController.js new file mode 100644 index 0000000..f4299c7 --- /dev/null +++ b/controllers/activityController.js @@ -0,0 +1,239 @@ +const database = require('../config/database'); + +class ActivityController { + // Get all activities + async getAllActivities(req, res) { + try { + const db = database.getConnection(); + const { page = 1, limit = 50, todo_id } = req.query; + const offset = (page - 1) * limit; + + let sql = ` + SELECT + a.*, + t.title as todo_title + FROM activities a + LEFT JOIN todos t ON a.todo_id = t.id + `; + let params = []; + + if (todo_id) { + sql += ' WHERE a.todo_id = ?'; + params.push(todo_id); + } + + sql += ' ORDER BY a.created_at DESC LIMIT ? OFFSET ?'; + params.push(parseInt(limit), parseInt(offset)); + + db.all(sql, params, (err, rows) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + // Get total count for pagination + let countSql = 'SELECT COUNT(*) as total FROM activities'; + let countParams = []; + + if (todo_id) { + countSql += ' WHERE todo_id = ?'; + countParams.push(todo_id); + } + + db.get(countSql, countParams, (err, countResult) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + res.json({ + activities: rows, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: countResult.total, + pages: Math.ceil(countResult.total / limit) + } + }); + }); + }); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } + } + + // Get activities for a specific todo + async getActivitiesByTodoId(req, res) { + try { + const { todoId } = req.params; + const db = database.getConnection(); + + const sql = ` + SELECT + a.*, + t.title as todo_title + FROM activities a + LEFT JOIN todos t ON a.todo_id = t.id + WHERE a.todo_id = ? + ORDER BY a.created_at DESC + `; + + db.all(sql, [todoId], (err, rows) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + res.json(rows); + }); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } + } + + // Get a single activity by id + async getActivityById(req, res) { + try { + const { id } = req.params; + const db = database.getConnection(); + + const sql = ` + SELECT + a.*, + t.title as todo_title + FROM activities a + LEFT JOIN todos t ON a.todo_id = t.id + WHERE a.id = ? + `; + + db.get(sql, [id], (err, row) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + if (!row) { + res.status(404).json({ error: 'Activity not found' }); + return; + } + res.json(row); + }); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } + } + + // Create a new activity (usually called internally) + async createActivity(activityData) { + return new Promise((resolve, reject) => { + const db = database.getConnection(); + const { + todo_id, + action, + description, + old_value, + new_value, + user_ip, + user_agent + } = activityData; + + const sql = ` + INSERT INTO activities ( + todo_id, action, description, old_value, new_value, user_ip, user_agent + ) VALUES (?, ?, ?, ?, ?, ?, ?) + `; + + db.run(sql, [ + todo_id, + action, + description, + old_value, + new_value, + user_ip, + user_agent + ], function(err) { + if (err) { + reject(err); + } else { + resolve(this.lastID); + } + }); + }); + } + + // Delete an activity + async deleteActivity(req, res) { + try { + const { id } = req.params; + const db = database.getConnection(); + + db.run('DELETE FROM activities WHERE id = ?', [id], function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + if (this.changes === 0) { + res.status(404).json({ error: 'Activity not found' }); + return; + } + res.json({ message: 'Activity deleted successfully' }); + }); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } + } + + // Clear all activities (admin function) + async clearAllActivities(req, res) { + try { + const db = database.getConnection(); + + db.run('DELETE FROM activities', function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + res.json({ + message: 'All activities cleared successfully', + deletedCount: this.changes + }); + }); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } + } + + // Get activity statistics + async getActivityStats(req, res) { + try { + const db = database.getConnection(); + + const statsQueries = [ + 'SELECT COUNT(*) as total FROM activities', + 'SELECT COUNT(*) as today FROM activities WHERE DATE(created_at) = DATE("now")', + 'SELECT action, COUNT(*) as count FROM activities GROUP BY action', + 'SELECT DATE(created_at) as date, COUNT(*) as count FROM activities GROUP BY DATE(created_at) ORDER BY date DESC LIMIT 7' + ]; + + Promise.all(statsQueries.map(query => + new Promise((resolve, reject) => { + db.all(query, [], (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }) + )).then(([total, today, byAction, byDate]) => { + res.json({ + total: total[0].total, + today: today[0].today, + byAction: byAction, + last7Days: byDate + }); + }).catch(err => { + res.status(500).json({ error: err.message }); + }); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } + } +} + +module.exports = new ActivityController(); diff --git a/controllers/todoController.js b/controllers/todoController.js index c220b80..b8e7145 100644 --- a/controllers/todoController.js +++ b/controllers/todoController.js @@ -1,6 +1,28 @@ const database = require('../config/database'); +const activityController = require('./activityController'); class TodoController { + // Helper method to log activities + async logActivity(todoId, action, description, oldValue = null, newValue = null, req = null) { + try { + const userIp = req ? req.ip || req.connection.remoteAddress : null; + const userAgent = req ? req.get('User-Agent') : null; + + await activityController.createActivity({ + todo_id: todoId, + action, + description, + old_value: oldValue ? JSON.stringify(oldValue) : null, + new_value: newValue ? JSON.stringify(newValue) : null, + user_ip: userIp, + user_agent: userAgent + }); + } catch (error) { + console.error('Failed to log activity:', error); + // Don't throw error to avoid breaking the main operation + } + } + // Get all todos async getAllTodos(req, res) { try { @@ -56,16 +78,32 @@ class TodoController { db.run( 'INSERT INTO todos (title, description) VALUES (?, ?)', [title.trim(), description ? description.trim() : ''], - function(err) { + async (err) => { if (err) { res.status(500).json({ error: err.message }); return; } - res.status(201).json({ - id: this.lastID, + + const todoId = this.lastID; + const newTodo = { + id: todoId, title: title.trim(), description: description ? description.trim() : '', - completed: false, + completed: false + }; + + // Log the creation activity + await this.logActivity( + todoId, + 'CREATE', + `Todo "${title.trim()}" was created`, + null, + newTodo, + req + ); + + res.status(201).json({ + ...newTodo, message: 'Todo created successfully' }); } @@ -80,50 +118,81 @@ class TodoController { try { const { id } = req.params; const { title, description, completed } = req.body; - - let updateFields = []; - let values = []; - - if (title !== undefined) { - if (!title || title.trim() === '') { - res.status(400).json({ error: 'Title cannot be empty' }); - return; - } - updateFields.push('title = ?'); - values.push(title.trim()); - } - - if (description !== undefined) { - updateFields.push('description = ?'); - values.push(description ? description.trim() : ''); - } - - if (completed !== undefined) { - updateFields.push('completed = ?'); - values.push(completed ? 1 : 0); - } - - if (updateFields.length === 0) { - res.status(400).json({ error: 'No fields to update' }); - return; - } - - updateFields.push('updated_at = CURRENT_TIMESTAMP'); - values.push(id); - - const sql = `UPDATE todos SET ${updateFields.join(', ')} WHERE id = ?`; const db = database.getConnection(); - db.run(sql, values, function(err) { + // First, get the current todo to compare changes + db.get('SELECT * FROM todos WHERE id = ?', [id], async (err, currentTodo) => { if (err) { res.status(500).json({ error: err.message }); return; } - if (this.changes === 0) { + if (!currentTodo) { res.status(404).json({ error: 'Todo not found' }); return; } - res.json({ message: 'Todo updated successfully' }); + + let updateFields = []; + let values = []; + let changes = []; + + if (title !== undefined) { + if (!title || title.trim() === '') { + res.status(400).json({ error: 'Title cannot be empty' }); + return; + } + if (title.trim() !== currentTodo.title) { + updateFields.push('title = ?'); + values.push(title.trim()); + changes.push(`Title changed from "${currentTodo.title}" to "${title.trim()}"`); + } + } + + if (description !== undefined) { + const newDescription = description ? description.trim() : ''; + if (newDescription !== (currentTodo.description || '')) { + updateFields.push('description = ?'); + values.push(newDescription); + changes.push(`Description changed from "${currentTodo.description || ''}" to "${newDescription}"`); + } + } + + if (completed !== undefined) { + const newCompleted = completed ? 1 : 0; + if (newCompleted !== currentTodo.completed) { + updateFields.push('completed = ?'); + values.push(newCompleted); + changes.push(`Status changed from ${currentTodo.completed ? 'completed' : 'pending'} to ${completed ? 'completed' : 'pending'}`); + } + } + + if (updateFields.length === 0) { + res.status(400).json({ error: 'No changes detected' }); + return; + } + + updateFields.push('updated_at = CURRENT_TIMESTAMP'); + values.push(id); + + const sql = `UPDATE todos SET ${updateFields.join(', ')} WHERE id = ?`; + + db.run(sql, values, async (err) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + // Log the update activity + await this.logActivity( + id, + 'UPDATE', + changes.join(', '), + currentTodo, + { ...currentTodo, ...req.body }, + req + ); + + res.json({ message: 'Todo updated successfully' }); + }); }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); @@ -136,16 +205,36 @@ class TodoController { const { id } = req.params; const db = database.getConnection(); - db.run('DELETE FROM todos WHERE id = ?', [id], function(err) { + // First, get the todo to log its details before deletion + db.get('SELECT * FROM todos WHERE id = ?', [id], async (err, todo) => { if (err) { res.status(500).json({ error: err.message }); return; } - if (this.changes === 0) { + if (!todo) { res.status(404).json({ error: 'Todo not found' }); return; } - res.json({ message: 'Todo deleted successfully' }); + + // Delete the todo + db.run('DELETE FROM todos WHERE id = ?', [id], async (err) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + // Log the deletion activity + await this.logActivity( + id, + 'DELETE', + `Todo "${todo.title}" was deleted`, + todo, + null, + req + ); + + res.json({ message: 'Todo deleted successfully' }); + }); }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); diff --git a/routes/activityRoutes.js b/routes/activityRoutes.js new file mode 100644 index 0000000..8079bbc --- /dev/null +++ b/routes/activityRoutes.js @@ -0,0 +1,23 @@ +const express = require('express'); +const router = express.Router(); +const activityController = require('../controllers/activityController'); + +// GET /api/activities - Get all activities with pagination +router.get('/', activityController.getAllActivities); + +// GET /api/activities/stats - Get activity statistics +router.get('/stats', activityController.getActivityStats); + +// GET /api/activities/todo/:todoId - Get activities for a specific todo +router.get('/todo/:todoId', activityController.getActivitiesByTodoId); + +// GET /api/activities/:id - Get a single activity by id +router.get('/:id', activityController.getActivityById); + +// DELETE /api/activities/:id - Delete a specific activity +router.delete('/:id', activityController.deleteActivity); + +// DELETE /api/activities - Clear all activities (admin function) +router.delete('/', activityController.clearAllActivities); + +module.exports = router; diff --git a/server.js b/server.js index 4361f6a..494cf72 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ const path = require('path'); // Import modules const database = require('./config/database'); const todoRoutes = require('./routes/todoRoutes'); +const activityRoutes = require('./routes/activityRoutes'); const app = express(); const PORT = process.env.PORT || 3000; @@ -18,6 +19,7 @@ app.use(express.static(path.join(__dirname, 'public'))); // Routes app.use('/api/todos', todoRoutes); +app.use('/api/activities', activityRoutes); // Serve the main page app.get('/', (req, res) => {