From bd93641af1148259880a613334ddc08ad913e629 Mon Sep 17 00:00:00 2001 From: rishi Date: Sun, 1 Dec 2024 16:49:31 +0000 Subject: [PATCH] Image upload functionality --- server/src/routes/images.js | 148 ++++++++++++++++++++++++++++++------ server/src/server.js | 22 +++--- 2 files changed, 137 insertions(+), 33 deletions(-) diff --git a/server/src/routes/images.js b/server/src/routes/images.js index f83efc8..7f34611 100644 --- a/server/src/routes/images.js +++ b/server/src/routes/images.js @@ -1,33 +1,135 @@ -import { createWriteStream } from 'fs'; +// File: src/routes/images.js import { pipeline } from 'stream/promises'; +import { createWriteStream } from 'fs'; import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; import { db } from '../db.js'; -export default async function images(fastify, options) { - fastify.addHook('preHandler', async (request, reply) => { - try { - await request.jwtVerify(); - } catch (err) { - reply.send(err); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Auth middleware helper +const authenticate = async (request, reply) => { + try { + await request.jwtVerify(); + } catch (err) { + reply.code(401).send({ error: 'Unauthorized' }); + } +}; + +export default async function imageRoutes(fastify) { + // Upload image for a post + fastify.post('/api/posts/:postId/images', { + onRequest: [authenticate], + handler: async (request, reply) => { + const { postId } = request.params; + + try { + // Verify post exists and belongs to user + const post = await db.oneOrNone( + 'SELECT id FROM posts WHERE id = $1 AND author_id = $2', + [postId, request.user.id] + ); + + if (!post) { + reply.code(404).send({ error: 'Post not found' }); + return; + } + + // Handle file upload + const data = await request.file(); + + if (!data) { + reply.code(400).send({ error: 'No file uploaded' }); + return; + } + + // Validate file type + const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (!allowedMimeTypes.includes(data.mimetype)) { + reply.code(400).send({ error: 'Invalid file type' }); + return; + } + + // Create unique filename + const fileExt = data.filename.split('.').pop(); + const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${fileExt}`; + const uploadDir = join(__dirname, '../../uploads'); + const filepath = join(uploadDir, filename); + + // Save file to disk + await pipeline(data.file, createWriteStream(filepath)); + + // Save image record in database + const image = await db.one( + `INSERT INTO images (filename, url, post_id) + VALUES ($1, $2, $3) + RETURNING *`, + [filename, `/uploads/${filename}`, postId] + ); + + reply.code(201).send(image); + } catch (err) { + fastify.log.error(err); + reply.code(500).send({ error: 'Internal server error' }); + } } }); - fastify.post('/api/images/:postId', async (request, reply) => { - const { postId } = request.params; - const data = await request.file(); - - const filename = `${Date.now()}-${data.filename}`; - const path = join(__dirname, '../../uploads', filename); - - await pipeline(data.file, createWriteStream(path)); - - const image = await db.one( - `INSERT INTO images (filename, url, post_id) - VALUES ($1, $2, $3) - RETURNING *`, - [filename, `/uploads/${filename}`, postId] - ); + // Delete image + fastify.delete('/api/images/:id', { + onRequest: [authenticate], + handler: async (request, reply) => { + const { id } = request.params; - reply.send(image); + try { + const image = await db.oneOrNone( + `SELECT i.* FROM images i + JOIN posts p ON i.post_id = p.id + WHERE i.id = $1 AND p.author_id = $2`, + [id, request.user.id] + ); + + if (!image) { + reply.code(404).send({ error: 'Image not found' }); + return; + } + + // Delete from database + await db.none('DELETE FROM images WHERE id = $1', [id]); + + // We could also delete the file from disk here, but we might want to keep it + // for backup purposes or to handle undo operations + + reply.code(204).send(); + } catch (err) { + fastify.log.error(err); + reply.code(500).send({ error: 'Internal server error' }); + } + } + }); + + // Get images for a post + fastify.get('/api/posts/:postId/images', { + onRequest: [authenticate], + handler: async (request, reply) => { + const { postId } = request.params; + + try { + const images = await db.any( + `SELECT i.* FROM images i + JOIN posts p ON i.post_id = p.id + WHERE p.id = $1 AND p.author_id = $2 + ORDER BY i.uploaded_at DESC`, + [postId, request.user.id] + ); + + reply.send(images); + } catch (err) { + fastify.log.error(err); + reply.code(500).send({ error: 'Internal server error' }); + } + } }); } \ No newline at end of file diff --git a/server/src/server.js b/server/src/server.js index a5a4dc3..b7a8974 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -9,6 +9,7 @@ import { dirname } from 'path'; import dotenv from 'dotenv'; import authRoutes from './routes/auth.js'; import postRoutes from './routes/posts.js'; +import imageRoutes from './routes/images.js'; // Load environment variables dotenv.config(); @@ -17,15 +18,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = Fastify({ - logger: true, - ajv: { - customOptions: { - removeAdditional: false, - useDefaults: true, - coerceTypes: true, - allErrors: true - } - } + logger: true }); // Register plugins @@ -37,16 +30,25 @@ await app.register(jwt, { secret: process.env.JWT_SECRET || 'your-super-secret-key-change-this-in-production' }); -await app.register(multipart); +await app.register(multipart, { + limits: { + fileSize: 5 * 1024 * 1024 // 5MB + } +}); await app.register(fastifyStatic, { root: join(__dirname, '../uploads'), prefix: '/uploads/' }); +// Create uploads directory if it doesn't exist +import { mkdirSync } from 'fs'; +mkdirSync(join(__dirname, '../uploads'), { recursive: true }); + // Register routes await app.register(authRoutes); await app.register(postRoutes); +await app.register(imageRoutes); // Start the server const start = async () => {