Image upload functionality
This commit is contained in:
@@ -1,33 +1,135 @@
|
|||||||
import { createWriteStream } from 'fs';
|
// File: src/routes/images.js
|
||||||
import { pipeline } from 'stream/promises';
|
import { pipeline } from 'stream/promises';
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
import { db } from '../db.js';
|
import { db } from '../db.js';
|
||||||
|
|
||||||
export default async function images(fastify, options) {
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
fastify.addHook('preHandler', async (request, reply) => {
|
const __dirname = dirname(__filename);
|
||||||
try {
|
|
||||||
await request.jwtVerify();
|
// Auth middleware helper
|
||||||
} catch (err) {
|
const authenticate = async (request, reply) => {
|
||||||
reply.send(err);
|
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) => {
|
// Delete image
|
||||||
const { postId } = request.params;
|
fastify.delete('/api/images/:id', {
|
||||||
const data = await request.file();
|
onRequest: [authenticate],
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
const { id } = request.params;
|
||||||
|
|
||||||
const filename = `${Date.now()}-${data.filename}`;
|
try {
|
||||||
const path = join(__dirname, '../../uploads', filename);
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
await pipeline(data.file, createWriteStream(path));
|
if (!image) {
|
||||||
|
reply.code(404).send({ error: 'Image not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const image = await db.one(
|
// Delete from database
|
||||||
`INSERT INTO images (filename, url, post_id)
|
await db.none('DELETE FROM images WHERE id = $1', [id]);
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
RETURNING *`,
|
|
||||||
[filename, `/uploads/${filename}`, postId]
|
|
||||||
);
|
|
||||||
|
|
||||||
reply.send(image);
|
// 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import { dirname } from 'path';
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import authRoutes from './routes/auth.js';
|
import authRoutes from './routes/auth.js';
|
||||||
import postRoutes from './routes/posts.js';
|
import postRoutes from './routes/posts.js';
|
||||||
|
import imageRoutes from './routes/images.js';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -17,15 +18,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: true,
|
logger: true
|
||||||
ajv: {
|
|
||||||
customOptions: {
|
|
||||||
removeAdditional: false,
|
|
||||||
useDefaults: true,
|
|
||||||
coerceTypes: true,
|
|
||||||
allErrors: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register plugins
|
// Register plugins
|
||||||
@@ -37,16 +30,25 @@ await app.register(jwt, {
|
|||||||
secret: process.env.JWT_SECRET || 'your-super-secret-key-change-this-in-production'
|
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, {
|
await app.register(fastifyStatic, {
|
||||||
root: join(__dirname, '../uploads'),
|
root: join(__dirname, '../uploads'),
|
||||||
prefix: '/uploads/'
|
prefix: '/uploads/'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create uploads directory if it doesn't exist
|
||||||
|
import { mkdirSync } from 'fs';
|
||||||
|
mkdirSync(join(__dirname, '../uploads'), { recursive: true });
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
await app.register(authRoutes);
|
await app.register(authRoutes);
|
||||||
await app.register(postRoutes);
|
await app.register(postRoutes);
|
||||||
|
await app.register(imageRoutes);
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user