From 85f191f2342d011251b9de57f98094ec61f59751 Mon Sep 17 00:00:00 2001 From: "[dyad]" Date: Thu, 6 Nov 2025 13:23:48 +0100 Subject: [PATCH] [dyad] wrote 4 file(s) --- routes/auth.routes.js | 67 ++++++++++++++ routes/notes.routes.js | 204 +++++++++++++++++++++++++++++++++++++++++ routes/users.routes.js | 102 +++++++++++++++++++++ server.js | 39 ++++++++ 4 files changed, 412 insertions(+) create mode 100644 routes/auth.routes.js create mode 100644 routes/notes.routes.js create mode 100644 routes/users.routes.js create mode 100644 server.js diff --git a/routes/auth.routes.js b/routes/auth.routes.js new file mode 100644 index 0000000..0c1c7f6 --- /dev/null +++ b/routes/auth.routes.js @@ -0,0 +1,67 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const { body, validationResult } = require('express-validator'); +const rateLimit = require('express-rate-limit'); +const { db, logger } = require('../config/database'); +const { authMiddleware } = require('../middleware/auth'); + +// Rate limiting +const loginLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 10, // 10 requests per minute + message: { message: 'Too many login attempts, please try again later' } +}); + +// Login validation +const loginValidation = [ + body('username').trim().notEmpty().escape(), + body('password').trim().notEmpty() +]; + +// Login route +router.post('/login', loginLimiter, loginValidation, async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { username, password } = req.body; + + db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => { + if (err) { + logger.error('Login error:', err); + return res.status(500).json({ message: 'Server error' }); + } + + if (!user) { + return res.status(401).json({ message: 'Invalid credentials' }); + } + + const validPassword = await bcrypt.compare(password, user.password_hash); + if (!validPassword) { + return res.status(401).json({ message: 'Invalid credentials' }); + } + + const token = jwt.sign( + { id: user.id, username: user.username, is_admin: user.is_admin }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + res.json({ token, user: { id: user.id, username: user.username, is_admin: user.is_admin } }); + }); +}); + +// Get current user +router.get('/me', authMiddleware, (req, res) => { + res.json(req.user); +}); + +// Logout (client-side only, just for completeness) +router.post('/logout', (req, res) => { + res.json({ message: 'Logged out successfully' }); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/notes.routes.js b/routes/notes.routes.js new file mode 100644 index 0000000..c929a90 --- /dev/null +++ b/routes/notes.routes.js @@ -0,0 +1,204 @@ +const express = require('express'); +const router = express.Router(); +const multer = require('multer'); +const path = require('path'); +const { v4: uuidv4 } = require('uuid'); +const { body, validationResult } = require('express-validator'); +const { db, logger } = require('../config/database'); +const { authMiddleware } = require('../middleware/auth'); + +// Configure multer for image uploads +const storage = multer.diskStorage({ + destination: 'public/uploads/', + filename: (req, file, cb) => { + const uniqueName = `${uuidv4()}${path.extname(file.originalname)}`; + cb(null, uniqueName); + } +}); + +const upload = multer({ + storage, + limits: { fileSize: 5 * 1024 * 1024 }, // 5MB + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|gif/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + if (extname && mimetype) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed')); + } + } +}); + +// Note validation +const noteValidation = [ + body('title').trim().notEmpty().escape(), + body('content').trim().optional().escape(), + body('archived').isBoolean().optional() +]; + +// Get all notes for user +router.get('/', authMiddleware, (req, res) => { + const query = ` + SELECT n.*, + GROUP_CONCAT(t.id || ':' || t.text || ':' || t.completed) as todos, + GROUP_CONCAT(i.id || ':' || i.filename) as images + FROM notes n + LEFT JOIN todos t ON n.id = t.note_id + LEFT JOIN images i ON n.id = i.note_id + WHERE n.user_id = ? + GROUP BY n.id + ORDER BY n.created_at DESC + `; + + db.all(query, [req.user.id], (err, notes) => { + if (err) { + logger.error('Error fetching notes:', err); + return res.status(500).json({ message: 'Server error' }); + } + + // Process the results to format todos and images + const processedNotes = notes.map(note => ({ + ...note, + todos: note.todos ? note.todos.split(',').map(todo => { + const [id, text, completed] = todo.split(':'); + return { id, text, completed: completed === '1' }; + }) : [], + images: note.images ? note.images.split(',').map(image => { + const [id, filename] = image.split(':'); + return { id, filename }; + }) : [] + })); + + res.json(processedNotes); + }); +}); + +// Create note +router.post('/', authMiddleware, noteValidation, (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { title, content, todos = [] } = req.body; + + db.run( + 'INSERT INTO notes (user_id, title, content) VALUES (?, ?, ?)', + [req.user.id, title, content], + function(err) { + if (err) { + logger.error('Error creating note:', err); + return res.status(500).json({ message: 'Server error' }); + } + + const noteId = this.lastID; + + // Insert todos if any + if (todos.length > 0) { + const todoValues = todos.map((todo, index) => + `(${noteId}, '${todo.text}', ${todo.completed ? 1 : 0}, ${index})` + ).join(','); + + db.run(`INSERT INTO todos (note_id, text, completed, position) VALUES ${todoValues}`); + } + + res.status(201).json({ id: noteId, title, content, todos: [] }); + } + ); +}); + +// Update note +router.put('/:id', authMiddleware, noteValidation, (req, res) => { + const noteId = req.params.id; + const { title, content, archived, todos = [] } = req.body; + + db.run( + 'UPDATE notes SET title = ?, content = ?, archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?', + [title, content, archived ? 1 : 0, noteId, req.user.id], + function(err) { + if (err) { + logger.error('Error updating note:', err); + return res.status(500).json({ message: 'Server error' }); + } + + // Update todos + db.run('DELETE FROM todos WHERE note_id = ?', [noteId], (err) => { + if (err) { + logger.error('Error deleting todos:', err); + return; + } + + if (todos.length > 0) { + const todoValues = todos.map((todo, index) => + `(${noteId}, '${todo.text}', ${todo.completed ? 1 : 0}, ${index})` + ).join(','); + + db.run(`INSERT INTO todos (note_id, text, completed, position) VALUES ${todoValues}`); + } + }); + + res.json({ message: 'Note updated successfully' }); + } + ); +}); + +// Delete note +router.delete('/:id', authMiddleware, (req, res) => { + const noteId = req.params.id; + + db.run('DELETE FROM notes WHERE id = ? AND user_id = ?', [noteId, req.user.id], (err) => { + if (err) { + logger.error('Error deleting note:', err); + return res.status(500).json({ message: 'Server error' }); + } + + // Cleanup related records + db.run('DELETE FROM todos WHERE note_id = ?', [noteId]); + db.run('DELETE FROM images WHERE note_id = ?', [noteId]); + + res.json({ message: 'Note deleted successfully' }); + }); +}); + +// Upload image +router.post('/:id/images', authMiddleware, upload.single('image'), (req, res) => { + if (!req.file) { + return res.status(400).json({ message: 'No image file provided' }); + } + + const noteId = req.params.id; + const filename = req.file.filename; + + db.run( + 'INSERT INTO images (note_id, filename) VALUES (?, ?)', + [noteId, filename], + function(err) { + if (err) { + logger.error('Error saving image record:', err); + return res.status(500).json({ message: 'Server error' }); + } + res.status(201).json({ id: this.lastID, filename }); + } + ); +}); + +// Delete image +router.delete('/:noteId/images/:imageId', authMiddleware, (req, res) => { + const { noteId, imageId } = req.params; + + db.run( + 'DELETE FROM images WHERE id = ? AND note_id = ?', + [imageId, noteId], + (err) => { + if (err) { + logger.error('Error deleting image:', err); + return res.status(500).json({ message: 'Server error' }); + } + res.json({ message: 'Image deleted successfully' }); + } + ); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/users.routes.js b/routes/users.routes.js new file mode 100644 index 0000000..9a1af0d --- /dev/null +++ b/routes/users.routes.js @@ -0,0 +1,102 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcrypt'); +const { body, validationResult } = require('express-validator'); +const { db, logger } = require('../config/database'); +const { authMiddleware, adminMiddleware } = require('../middleware/auth'); + +// User validation rules +const userValidation = [ + body('username').trim().isLength({ min: 3 }).escape(), + body('password').isLength({ min: 6 }), + body('is_admin').isBoolean().optional() +]; + +// Get all users (admin only) +router.get('/', authMiddleware, adminMiddleware, (req, res) => { + db.all('SELECT id, username, is_admin, created_at FROM users', (err, users) => { + if (err) { + logger.error('Error fetching users:', err); + return res.status(500).json({ message: 'Server error' }); + } + res.json(users); + }); +}); + +// Create user (admin only) +router.post('/', authMiddleware, adminMiddleware, userValidation, async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { username, password, is_admin } = req.body; + + try { + const hash = await bcrypt.hash(password, 10); + db.run( + 'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, ?)', + [username, hash, is_admin ? 1 : 0], + function(err) { + if (err) { + if (err.message.includes('UNIQUE constraint failed')) { + return res.status(400).json({ message: 'Username already exists' }); + } + logger.error('Error creating user:', err); + return res.status(500).json({ message: 'Server error' }); + } + res.status(201).json({ id: this.lastID, username, is_admin }); + } + ); + } catch (err) { + logger.error('Password hashing error:', err); + res.status(500).json({ message: 'Server error' }); + } +}); + +// Update user (admin only) +router.put('/:id', authMiddleware, adminMiddleware, async (req, res) => { + const userId = req.params.id; + const { password, is_admin } = req.body; + + // Prevent modifying the main admin user + if (userId === '1') { + return res.status(403).json({ message: 'Cannot modify main admin user' }); + } + + try { + if (password) { + const hash = await bcrypt.hash(password, 10); + db.run('UPDATE users SET password_hash = ? WHERE id = ?', [hash, userId]); + } + + if (typeof is_admin !== 'undefined') { + db.run('UPDATE users SET is_admin = ? WHERE id = ?', [is_admin ? 1 : 0, userId]); + } + + res.json({ message: 'User updated successfully' }); + } catch (err) { + logger.error('Error updating user:', err); + res.status(500).json({ message: 'Server error' }); + } +}); + +// Delete user (admin only) +router.delete('/:id', authMiddleware, adminMiddleware, (req, res) => { + const userId = req.params.id; + + // Prevent deleting the main admin user + if (userId === '1') { + return res.status(403).json({ message: 'Cannot delete main admin user' }); + } + + db.run('DELETE FROM users WHERE id = ?', [userId], (err) => { + if (err) { + logger.error('Error deleting user:', err); + return res.status(500).json({ message: 'Server error' }); + } + res.json({ message: 'User deleted successfully' }); + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..3bca551 --- /dev/null +++ b/server.js @@ -0,0 +1,39 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const path = require('path'); +const { logger } = require('./config/database'); + +// Create Express app +const app = express(); + +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(express.json()); +app.use(express.static('public')); + +// Routes +app.use('/api/auth', require('./routes/auth.routes')); +app.use('/api/users', require('./routes/users.routes')); +app.use('/api/notes', require('./routes/notes.routes')); + +// Serve static files +app.use('/uploads', express.static(path.join(__dirname, 'public/uploads'))); + +// SPA fallback +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'public/index.html')); +}); + +// Error handling +app.use((err, req, res, next) => { + logger.error('Unhandled error:', err); + res.status(500).json({ message: 'Internal server error' }); +}); + +// Start server +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + logger.info(`Server running on port ${PORT}`); +}); \ No newline at end of file