import { Router, Response } from 'express'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import sharp from 'sharp'; import { queryOne, queryAll, execute } from '../db'; import { authMiddleware, AuthRequest } from '../middleware/auth.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const uploadsDir = path.join(__dirname, '..', '..', 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } const ALLOWED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.heif', '.avif']); const ALLOWED_MIMES = new Set([ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', 'image/avif', ]); const storage = multer.diskStorage({ destination: (_req, _file, cb) => cb(null, uploadsDir), filename: (_req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase() || '.jpg'; cb(null, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`); }, }); const upload = multer({ storage, limits: { fileSize: 20 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); if (!ALLOWED_EXTENSIONS.has(ext) || !ALLOWED_MIMES.has(file.mimetype)) { cb(new Error('Неподдерживаемый формат файла')); return; } cb(null, true); }, }); const router = Router(); router.get('/', (req: AuthRequest, res: Response) => { const page = Math.max(1, parseInt(req.query.page as string) || 1); const limit = 12; const offset = (page - 1) * limit; const cats = queryAll( `SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id, u.username, u.points AS user_points, u.avatar_url AS user_avatar_url, (SELECT COUNT(*) FROM likes WHERE cat_id = c.id) AS likes_count, (SELECT COUNT(*) FROM comments WHERE cat_id = c.id) AS comments_count FROM cats c JOIN users u ON u.id = c.user_id ORDER BY c.created_at DESC LIMIT ? OFFSET ?`, [limit, offset] ); const countResult = queryOne('SELECT COUNT(*) as count FROM cats'); const total = countResult?.count || 0; res.json({ cats, total, page, totalPages: Math.ceil(total / limit) }); }); router.get('/user/:userId', (req: AuthRequest, res: Response) => { const userId = parseInt(req.params.userId); const cats = queryAll( `SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id, u.username, u.points AS user_points, u.avatar_url AS user_avatar_url, (SELECT COUNT(*) FROM likes WHERE cat_id = c.id) AS likes_count, (SELECT COUNT(*) FROM comments WHERE cat_id = c.id) AS comments_count FROM cats c JOIN users u ON u.id = c.user_id WHERE c.user_id = ? ORDER BY c.created_at DESC`, [userId] ); res.json({ cats }); }); router.get('/:id', (req: AuthRequest, res: Response) => { const cat = queryOne( `SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id, u.username, u.points AS user_points, u.avatar_url AS user_avatar_url, (SELECT COUNT(*) FROM likes WHERE cat_id = c.id) AS likes_count, (SELECT COUNT(*) FROM comments WHERE cat_id = c.id) AS comments_count FROM cats c JOIN users u ON u.id = c.user_id WHERE c.id = ?`, [req.params.id] ); if (!cat) { res.status(404).json({ error: 'Cat not found' }); return; } res.json({ cat }); }); router.post('/', authMiddleware, upload.single('image'), async (req: AuthRequest, res: Response) => { if (!req.file) { res.status(400).json({ error: 'Нужно выбрать изображение (jpg, png, gif, webp, heic, avif, до 20MB)' }); return; } const caption = (req.body.caption || '').trim(); if (caption.length > 500) { fs.unlinkSync(req.file.path); res.status(400).json({ error: 'Подпись не может быть длиннее 500 символов' }); return; } const origPath = req.file.path; const origExt = path.extname(req.file.filename).toLowerCase(); const isGif = origExt === '.gif'; let finalFilename = req.file.filename; let finalPath = origPath; if (!isGif) { finalFilename = req.file.filename.replace(/\.[^.]+$/, '.jpg'); finalPath = path.join(uploadsDir, finalFilename); try { await sharp(origPath).jpeg({ quality: 85 }).toFile(finalPath); if (origPath !== finalPath) fs.unlinkSync(origPath); } catch { try { fs.unlinkSync(origPath); } catch {} res.status(400).json({ error: 'Не удалось обработать изображение. Попробуйте другой формат.' }); return; } } const image_url = `/uploads/${finalFilename}`; execute( 'INSERT INTO cats (user_id, image_url, caption) VALUES (?, ?, ?)', [req.userId, image_url, caption] ); execute('UPDATE users SET points = points + 1 WHERE id = ?', [req.userId]); const cat = queryOne( `SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id, u.username, u.points AS user_points, u.avatar_url AS user_avatar_url, 0 AS likes_count, 0 AS comments_count FROM cats c JOIN users u ON u.id = c.user_id WHERE c.id = (SELECT MAX(id) FROM cats WHERE user_id = ?)`, [req.userId] ); res.status(201).json({ cat }); }); router.delete('/:id', authMiddleware, (req: AuthRequest, res: Response) => { const cat = queryOne('SELECT user_id, image_url FROM cats WHERE id = ?', [req.params.id]); if (!cat) { res.status(404).json({ error: 'Cat not found' }); return; } if (cat.user_id !== req.userId && !req.isAdmin) { res.status(403).json({ error: 'Not your cat' }); return; } execute('DELETE FROM comments WHERE cat_id = ?', [req.params.id]); execute('DELETE FROM likes WHERE cat_id = ?', [req.params.id]); execute('DELETE FROM cats WHERE id = ?', [req.params.id]); if (cat.image_url) { const filePath = path.join(uploadsDir, path.basename(cat.image_url)); try { fs.unlinkSync(filePath); } catch {} } res.json({ ok: true }); }); export default router;