664 lines
21 KiB
TypeScript
664 lines
21 KiB
TypeScript
import { Router, Response } from 'express';
|
|
import { Readable } from 'node:stream';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { pool } from '../db/pool.js';
|
|
import { authMiddleware, optionalAuthMiddleware, AuthenticatedRequest } from '../middleware/auth.js';
|
|
import { getStorageProvider } from '../services/storage/factory.js';
|
|
|
|
const router = Router();
|
|
|
|
// Helper: resolve audio URL (generates signed URL for S3)
|
|
async function resolveAudioUrl(audioUrl: string | null): Promise<string | null> {
|
|
if (!audioUrl) return null;
|
|
|
|
if (audioUrl.startsWith('s3://')) {
|
|
const storageKey = audioUrl.replace('s3://', '');
|
|
const storage = getStorageProvider();
|
|
return storage.getUrl(storageKey, 3600); // 1 hour expiry
|
|
}
|
|
|
|
return audioUrl;
|
|
}
|
|
|
|
// Helper: resolve audio URL for direct playback
|
|
async function resolveAccessibleAudioUrl(audioUrl: string | null, isPublic: boolean): Promise<string | null> {
|
|
if (!audioUrl) return null;
|
|
if (audioUrl.startsWith('s3://')) {
|
|
const storageKey = audioUrl.replace('s3://', '');
|
|
const storage = getStorageProvider();
|
|
return isPublic ? storage.getPublicUrl(storageKey) : storage.getUrl(storageKey, 3600);
|
|
}
|
|
return audioUrl;
|
|
}
|
|
|
|
// Get audio - proxies from S3 to avoid CORS issues
|
|
router.get('/:id/audio', optionalAuthMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT s.audio_url, s.is_public, s.user_id FROM songs s WHERE s.id = $1`,
|
|
[req.params.id]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
|
|
const song = result.rows[0];
|
|
|
|
if (!song.is_public && (!req.user || req.user.id !== song.user_id)) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
const audioUrl = await resolveAudioUrl(song.audio_url);
|
|
if (!audioUrl) {
|
|
res.status(404).json({ error: 'Audio not available' });
|
|
return;
|
|
}
|
|
|
|
// Local files - redirect
|
|
if (audioUrl.startsWith('/')) {
|
|
res.redirect(audioUrl);
|
|
return;
|
|
}
|
|
|
|
// S3/remote - proxy to avoid CORS
|
|
const range = req.headers.range;
|
|
const audioRes = await fetch(audioUrl, {
|
|
headers: range ? { Range: range } : undefined,
|
|
});
|
|
if (!audioRes.ok && audioRes.status !== 206) {
|
|
res.status(502).json({ error: 'Failed to fetch audio' });
|
|
return;
|
|
}
|
|
|
|
const contentType = audioRes.headers.get('content-type') || 'audio/mpeg';
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Accept-Ranges', 'bytes');
|
|
|
|
const contentLength = audioRes.headers.get('content-length');
|
|
if (contentLength) {
|
|
res.setHeader('Content-Length', contentLength);
|
|
}
|
|
|
|
const contentRange = audioRes.headers.get('content-range');
|
|
if (contentRange) {
|
|
res.status(206);
|
|
res.setHeader('Content-Range', contentRange);
|
|
}
|
|
|
|
if (audioRes.body) {
|
|
Readable.fromWeb(audioRes.body as any).pipe(res);
|
|
return;
|
|
}
|
|
|
|
const arrayBuffer = await audioRes.arrayBuffer();
|
|
res.send(Buffer.from(arrayBuffer));
|
|
} catch (error) {
|
|
console.error('Get audio error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get user's songs
|
|
router.get('/', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
|
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public,
|
|
s.like_count, s.view_count, s.user_id, s.created_at, s.generation_params,
|
|
COALESCE(u.username, 'Anonymous') as creator
|
|
FROM songs s
|
|
LEFT JOIN users u ON s.user_id = u.id
|
|
WHERE s.user_id = $1
|
|
ORDER BY s.created_at DESC`,
|
|
[req.user!.id]
|
|
);
|
|
|
|
const songs = await Promise.all(
|
|
result.rows.map(async (row) => ({
|
|
...row,
|
|
audio_url: await resolveAccessibleAudioUrl(row.audio_url, row.is_public),
|
|
}))
|
|
);
|
|
|
|
res.json({ songs });
|
|
} catch (error) {
|
|
console.error('Get songs error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get featured songs (random songs for discover page)
|
|
router.get('/public/featured', optionalAuthMiddleware, async (_req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
// Return random songs - for local app, show all songs randomly
|
|
const result = await pool.query(
|
|
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
|
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.view_count, s.created_at, s.user_id,
|
|
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params
|
|
FROM songs s
|
|
LEFT JOIN users u ON s.user_id = u.id
|
|
ORDER BY RANDOM()
|
|
LIMIT 20`
|
|
);
|
|
|
|
const songs = await Promise.all(
|
|
result.rows.map(async (row) => ({
|
|
id: row.id,
|
|
title: row.title,
|
|
lyrics: row.lyrics,
|
|
style: row.style,
|
|
caption: row.caption,
|
|
cover_url: row.cover_url,
|
|
audio_url: await resolveAccessibleAudioUrl(row.audio_url, true),
|
|
duration: row.duration,
|
|
bpm: row.bpm,
|
|
key_scale: row.key_scale,
|
|
time_signature: row.time_signature,
|
|
tags: row.tags || [],
|
|
like_count: row.like_count || 0,
|
|
view_count: row.view_count || 0,
|
|
created_at: row.created_at,
|
|
creator: row.creator,
|
|
creator_avatar: row.creator_avatar,
|
|
user_id: row.user_id,
|
|
is_public: true
|
|
}))
|
|
);
|
|
|
|
res.json({ songs });
|
|
} catch (error) {
|
|
console.error('Get featured/random songs error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get public songs (for explore/home)
|
|
router.get('/public', optionalAuthMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
|
const offset = parseInt(req.query.offset as string) || 0;
|
|
|
|
const result = await pool.query(
|
|
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
|
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.created_at,
|
|
COALESCE(u.username, 'Anonymous') as creator, s.generation_params
|
|
FROM songs s
|
|
LEFT JOIN users u ON s.user_id = u.id
|
|
WHERE s.is_public = true
|
|
ORDER BY s.created_at DESC
|
|
LIMIT $1 OFFSET $2`,
|
|
[limit, offset]
|
|
);
|
|
|
|
const songs = await Promise.all(
|
|
result.rows.map(async (row) => ({
|
|
...row,
|
|
audio_url: await resolveAccessibleAudioUrl(row.audio_url, true),
|
|
}))
|
|
);
|
|
|
|
res.json({ songs });
|
|
} catch (error) {
|
|
console.error('Get public songs error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get single song
|
|
router.get('/:id', optionalAuthMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
|
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public, s.like_count, s.view_count, s.created_at,
|
|
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params
|
|
FROM songs s
|
|
LEFT JOIN users u ON s.user_id = u.id
|
|
WHERE s.id = $1`,
|
|
[req.params.id]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
|
|
const song = result.rows[0];
|
|
|
|
// Check access
|
|
if (!song.is_public && (!req.user || req.user.id !== song.user_id)) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
const resolvedSong = {
|
|
...song,
|
|
audio_url: await resolveAccessibleAudioUrl(song.audio_url, song.is_public),
|
|
};
|
|
|
|
res.json({ song: resolvedSong });
|
|
} catch (error) {
|
|
console.error('Get song error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get full song details (including comments)
|
|
router.get('/:id/full', optionalAuthMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const [songResult, commentsResult] = await Promise.all([
|
|
pool.query(
|
|
`SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
|
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public,
|
|
s.like_count, s.view_count, s.created_at, s.generation_params,
|
|
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar
|
|
FROM songs s
|
|
LEFT JOIN users u ON s.user_id = u.id
|
|
WHERE s.id = $1`,
|
|
[req.params.id]
|
|
),
|
|
pool.query(
|
|
`SELECT c.id, c.content, c.created_at, c.updated_at,
|
|
u.id as user_id, u.username, u.avatar_url
|
|
FROM comments c
|
|
JOIN users u ON c.user_id = u.id
|
|
WHERE c.song_id = $1
|
|
ORDER BY c.created_at DESC`,
|
|
[req.params.id]
|
|
)
|
|
]);
|
|
|
|
if (songResult.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
|
|
const song = songResult.rows[0];
|
|
|
|
// Check access
|
|
if (!song.is_public && (!req.user || req.user.id !== song.user_id)) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
// Increment view count
|
|
await pool.query('UPDATE songs SET view_count = view_count + 1 WHERE id = $1', [req.params.id]);
|
|
|
|
const resolvedSong = {
|
|
...song,
|
|
audio_url: await resolveAccessibleAudioUrl(song.audio_url, song.is_public),
|
|
};
|
|
|
|
res.json({
|
|
song: resolvedSong,
|
|
comments: commentsResult.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Get full song error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Create song (manual, not from generation)
|
|
router.post('/', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const {
|
|
title,
|
|
lyrics,
|
|
style,
|
|
caption,
|
|
coverUrl,
|
|
audioUrl,
|
|
duration,
|
|
bpm,
|
|
keyScale,
|
|
timeSignature,
|
|
tags,
|
|
isPublic,
|
|
} = req.body;
|
|
|
|
const result = await pool.query(
|
|
`INSERT INTO songs (user_id, title, lyrics, style, caption, cover_url, audio_url,
|
|
duration, bpm, key_scale, time_signature, tags, is_public)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
RETURNING *`,
|
|
[
|
|
req.user!.id,
|
|
title,
|
|
lyrics,
|
|
style,
|
|
caption,
|
|
coverUrl,
|
|
audioUrl,
|
|
duration,
|
|
bpm,
|
|
keyScale,
|
|
timeSignature,
|
|
tags || [],
|
|
isPublic || false,
|
|
]
|
|
);
|
|
|
|
res.status(201).json({ song: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('Create song error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Update song
|
|
router.patch('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
// Verify ownership
|
|
const check = await pool.query('SELECT user_id FROM songs WHERE id = $1', [req.params.id]);
|
|
if (check.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
if (check.rows[0].user_id !== req.user!.id) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
const updates: string[] = [];
|
|
const values: unknown[] = [];
|
|
let paramCount = 1;
|
|
|
|
const allowedFields = ['title', 'lyrics', 'style', 'caption', 'cover_url', 'is_public', 'tags'];
|
|
for (const field of allowedFields) {
|
|
if (req.body[field] !== undefined) {
|
|
updates.push(`${field} = $${paramCount}`);
|
|
values.push(req.body[field]);
|
|
paramCount++;
|
|
}
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
res.status(400).json({ error: 'No fields to update' });
|
|
return;
|
|
}
|
|
|
|
updates.push(`updated_at = CURRENT_TIMESTAMP`);
|
|
values.push(req.params.id);
|
|
|
|
const result = await pool.query(
|
|
`UPDATE songs SET ${updates.join(', ')} WHERE id = $${paramCount} RETURNING *`,
|
|
values
|
|
);
|
|
|
|
res.json({ song: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('Update song error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Delete song
|
|
router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const check = await pool.query('SELECT user_id, audio_url, cover_url FROM songs WHERE id = $1', [req.params.id]);
|
|
if (check.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
if (check.rows[0].user_id !== req.user!.id) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
const song = check.rows[0];
|
|
const storage = getStorageProvider();
|
|
|
|
// Delete audio file from storage
|
|
if (song.audio_url) {
|
|
try {
|
|
// Handle local storage paths (/audio/filename.mp3 -> filename.mp3)
|
|
const storageKey = song.audio_url.startsWith('/audio/')
|
|
? song.audio_url.replace('/audio/', '')
|
|
: song.audio_url.replace('s3://', '');
|
|
await storage.delete(storageKey);
|
|
} catch (err) {
|
|
console.error(`Failed to delete audio file ${song.audio_url}:`, err);
|
|
}
|
|
}
|
|
|
|
// Delete cover image if it's stored locally
|
|
if (song.cover_url && song.cover_url.startsWith('/audio/')) {
|
|
try {
|
|
const coverKey = song.cover_url.replace('/audio/', '');
|
|
await storage.delete(coverKey);
|
|
} catch (err) {
|
|
console.error(`Failed to delete cover ${song.cover_url}:`, err);
|
|
}
|
|
}
|
|
|
|
await pool.query('DELETE FROM songs WHERE id = $1', [req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Delete song error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Like/unlike song
|
|
router.post('/:id/like', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
// Check if already liked
|
|
const existing = await client.query(
|
|
'SELECT 1 FROM liked_songs WHERE user_id = $1 AND song_id = $2',
|
|
[req.user!.id, req.params.id]
|
|
);
|
|
|
|
if (existing.rows.length > 0) {
|
|
// Unlike
|
|
await client.query('DELETE FROM liked_songs WHERE user_id = $1 AND song_id = $2', [
|
|
req.user!.id,
|
|
req.params.id,
|
|
]);
|
|
// Decrement like_count
|
|
await client.query(
|
|
'UPDATE songs SET like_count = GREATEST(like_count - 1, 0) WHERE id = $1',
|
|
[req.params.id]
|
|
);
|
|
await client.query('COMMIT');
|
|
res.json({ liked: false });
|
|
} else {
|
|
// Like
|
|
await client.query('INSERT INTO liked_songs (user_id, song_id) VALUES ($1, $2)', [
|
|
req.user!.id,
|
|
req.params.id,
|
|
]);
|
|
// Increment like_count
|
|
await client.query(
|
|
'UPDATE songs SET like_count = like_count + 1 WHERE id = $1',
|
|
[req.params.id]
|
|
);
|
|
await client.query('COMMIT');
|
|
res.json({ liked: true });
|
|
}
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
console.error('Like song error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
// Get liked songs
|
|
router.get('/liked/list', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT s.id, s.title, s.lyrics, s.style, s.cover_url, s.audio_url,
|
|
s.duration, s.tags, s.like_count, s.created_at, s.is_public,
|
|
COALESCE(u.username, 'Anonymous') as creator, s.generation_params
|
|
FROM liked_songs ls
|
|
JOIN songs s ON ls.song_id = s.id
|
|
LEFT JOIN users u ON s.user_id = u.id
|
|
WHERE ls.user_id = $1
|
|
ORDER BY ls.liked_at DESC`,
|
|
[req.user!.id]
|
|
);
|
|
|
|
const songs = await Promise.all(
|
|
result.rows.map(async (row) => ({
|
|
...row,
|
|
audio_url: await resolveAccessibleAudioUrl(row.audio_url, row.is_public),
|
|
}))
|
|
);
|
|
|
|
res.json({ songs });
|
|
} catch (error) {
|
|
console.error('Get liked songs error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Toggle song privacy (paid users only can make songs private)
|
|
router.patch('/:id/privacy', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
// Get user's account tier
|
|
const userResult = await pool.query('SELECT account_tier FROM users WHERE id = $1', [req.user!.id]);
|
|
const accountTier = userResult.rows[0]?.account_tier || 'free';
|
|
|
|
const check = await pool.query('SELECT user_id, is_public FROM songs WHERE id = $1', [req.params.id]);
|
|
if (check.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
if (check.rows[0].user_id !== req.user!.id) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
const newPublicState = !check.rows[0].is_public;
|
|
|
|
// Free users cannot make songs private
|
|
if (accountTier === 'free' && !newPublicState) {
|
|
res.status(403).json({ error: 'Upgrade to Pro or Unlimited to make songs private' });
|
|
return;
|
|
}
|
|
|
|
await pool.query('UPDATE songs SET is_public = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', [
|
|
newPublicState,
|
|
req.params.id,
|
|
]);
|
|
|
|
res.json({ isPublic: newPublicState });
|
|
} catch (error) {
|
|
console.error('Toggle privacy error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Track song play
|
|
router.post('/:id/play', optionalAuthMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`UPDATE songs
|
|
SET view_count = COALESCE(view_count, 0) + 1, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
RETURNING view_count`,
|
|
[req.params.id]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
|
|
res.json({ viewCount: result.rows[0].view_count });
|
|
} catch (error) {
|
|
console.error('Track play error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get comments for a song
|
|
router.get('/:id/comments', optionalAuthMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT c.id, c.content, c.created_at, u.username, u.id as user_id, u.avatar_url
|
|
FROM comments c
|
|
JOIN users u ON c.user_id = u.id
|
|
WHERE c.song_id = $1
|
|
ORDER BY c.created_at DESC`,
|
|
[req.params.id]
|
|
);
|
|
|
|
res.json({ comments: result.rows });
|
|
} catch (error) {
|
|
console.error('Get comments error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Add comment to a song
|
|
router.post('/:id/comments', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const { content } = req.body;
|
|
|
|
if (!content || content.trim().length === 0) {
|
|
res.status(400).json({ error: 'Comment content is required' });
|
|
return;
|
|
}
|
|
|
|
// Check if song exists and is public
|
|
const songCheck = await pool.query('SELECT is_public FROM songs WHERE id = $1', [req.params.id]);
|
|
if (songCheck.rows.length === 0) {
|
|
res.status(404).json({ error: 'Song not found' });
|
|
return;
|
|
}
|
|
if (!songCheck.rows[0].is_public) {
|
|
res.status(403).json({ error: 'Cannot comment on private songs' });
|
|
return;
|
|
}
|
|
|
|
const result = await pool.query(
|
|
`INSERT INTO comments (song_id, user_id, content)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, content, created_at`,
|
|
[req.params.id, req.user!.id, content.trim()]
|
|
);
|
|
|
|
const comment = {
|
|
...result.rows[0],
|
|
username: req.user!.username,
|
|
user_id: req.user!.id,
|
|
};
|
|
|
|
res.status(201).json({ comment });
|
|
} catch (error) {
|
|
console.error('Add comment error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Delete comment
|
|
router.delete('/comments/:commentId', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const check = await pool.query('SELECT user_id FROM comments WHERE id = $1', [req.params.commentId]);
|
|
if (check.rows.length === 0) {
|
|
res.status(404).json({ error: 'Comment not found' });
|
|
return;
|
|
}
|
|
if (check.rows[0].user_id !== req.user!.id) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
await pool.query('DELETE FROM comments WHERE id = $1', [req.params.commentId]);
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Delete comment error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
export default router;
|