140 lines
4.5 KiB
JavaScript
140 lines
4.5 KiB
JavaScript
|
|
const express = require('express');
|
||
|
|
const router = express.Router();
|
||
|
|
const { db } = require('../database/db');
|
||
|
|
const { authenticate, requireAdmin } = require('../middleware/auth');
|
||
|
|
const { readLimiter } = require('../middleware/rateLimiter');
|
||
|
|
const { sanitizeString } = require('../utils/inputValidator');
|
||
|
|
const logger = require('../utils/logger');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Global search endpoint
|
||
|
|
* Searches across channels, radio stations, users, settings, etc.
|
||
|
|
*/
|
||
|
|
router.get('/', authenticate, readLimiter, async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { q } = req.query;
|
||
|
|
const isAdmin = req.user.role === 'admin';
|
||
|
|
|
||
|
|
if (!q || q.trim().length < 2) {
|
||
|
|
return res.json({
|
||
|
|
channels: [],
|
||
|
|
radio: [],
|
||
|
|
users: [],
|
||
|
|
settings: [],
|
||
|
|
groups: []
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate and sanitize search query
|
||
|
|
const sanitized = sanitizeString(q.trim());
|
||
|
|
if (sanitized.length > 100) {
|
||
|
|
return res.status(400).json({ error: 'Search query too long' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const searchTerm = `%${sanitized}%`;
|
||
|
|
const results = {
|
||
|
|
channels: [],
|
||
|
|
radio: [],
|
||
|
|
users: [],
|
||
|
|
settings: [],
|
||
|
|
groups: []
|
||
|
|
};
|
||
|
|
|
||
|
|
// Search TV channels (only from user's playlists)
|
||
|
|
results.channels = await new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT DISTINCT c.id, c.name, c.url, COALESCE(c.custom_logo, c.logo) as logo, c.group_name, c.is_radio
|
||
|
|
FROM channels c
|
||
|
|
JOIN playlists p ON c.playlist_id = p.id
|
||
|
|
WHERE p.user_id = ? AND c.is_radio = 0 AND c.is_active = 1
|
||
|
|
AND (c.name LIKE ? OR c.group_name LIKE ?)
|
||
|
|
ORDER BY c.name
|
||
|
|
LIMIT 20`,
|
||
|
|
[req.user.userId, searchTerm, searchTerm],
|
||
|
|
(err, rows) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve(rows || []);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Search Radio channels (only from user's playlists)
|
||
|
|
results.radio = await new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT DISTINCT c.id, c.name, c.url, COALESCE(c.custom_logo, c.logo) as logo, c.group_name, c.is_radio
|
||
|
|
FROM channels c
|
||
|
|
JOIN playlists p ON c.playlist_id = p.id
|
||
|
|
WHERE p.user_id = ? AND c.is_radio = 1 AND c.is_active = 1
|
||
|
|
AND (c.name LIKE ? OR c.group_name LIKE ?)
|
||
|
|
ORDER BY c.name
|
||
|
|
LIMIT 20`,
|
||
|
|
[req.user.userId, searchTerm, searchTerm],
|
||
|
|
(err, rows) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve(rows || []);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Search groups (only from user's playlists)
|
||
|
|
results.groups = await new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT DISTINCT c.group_name as name, c.is_radio
|
||
|
|
FROM channels c
|
||
|
|
JOIN playlists p ON c.playlist_id = p.id
|
||
|
|
WHERE p.user_id = ? AND c.is_active = 1
|
||
|
|
AND c.group_name LIKE ?
|
||
|
|
ORDER BY c.group_name
|
||
|
|
LIMIT 10`,
|
||
|
|
[req.user.userId, searchTerm],
|
||
|
|
(err, rows) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve(rows || []);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Search users (admin only)
|
||
|
|
if (isAdmin) {
|
||
|
|
results.users = await new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT id, username, email, role, created_at
|
||
|
|
FROM users
|
||
|
|
WHERE username LIKE ? OR email LIKE ?
|
||
|
|
ORDER BY username
|
||
|
|
LIMIT 10`,
|
||
|
|
[searchTerm, searchTerm],
|
||
|
|
(err, rows) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve(rows || []);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add settings/pages results (static)
|
||
|
|
const settingsOptions = [
|
||
|
|
{ id: 'settings', name: 'Settings', path: '/settings', icon: 'settings' },
|
||
|
|
{ id: 'user-management', name: 'User Management', path: '/settings?tab=users', icon: 'people' },
|
||
|
|
{ id: 'vpn-settings', name: 'VPN Settings', path: '/settings?tab=vpn', icon: 'vpn_lock' },
|
||
|
|
{ id: '2fa', name: 'Two-Factor Authentication', path: '/settings?tab=2fa', icon: 'security' },
|
||
|
|
{ id: 'live-tv', name: 'Live TV', path: '/live', icon: 'tv' },
|
||
|
|
{ id: 'radio', name: 'Radio', path: '/radio', icon: 'radio' },
|
||
|
|
{ id: 'movies', name: 'Movies', path: '/movies', icon: 'movie' },
|
||
|
|
{ id: 'series', name: 'Series', path: '/series', icon: 'subscriptions' },
|
||
|
|
{ id: 'favorites', name: 'Favorites', path: '/favorites', icon: 'favorite' },
|
||
|
|
];
|
||
|
|
|
||
|
|
results.settings = settingsOptions.filter(option =>
|
||
|
|
option.name.toLowerCase().includes(q.toLowerCase())
|
||
|
|
);
|
||
|
|
|
||
|
|
res.json(results);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Search error:', error);
|
||
|
|
res.status(500).json({ error: 'Search failed' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
module.exports = router;
|