feat: introduce CHANNEL_SELECTION_REQUIRES_ADMIN option
This commit is contained in:
@@ -30,6 +30,7 @@ module.exports = {
|
|||||||
checkAdminStatus(req, res) {
|
checkAdminStatus(req, res) {
|
||||||
res.json({
|
res.json({
|
||||||
enabled: authService.isAdminEnabled(),
|
enabled: authService.isAdminEnabled(),
|
||||||
|
channelSelectionRequiresAdmin: authService.channelSelectionRequiresAdmin(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
23
backend/package-lock.json
generated
23
backend/package-lock.json
generated
@@ -10,7 +10,6 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"child_process": "^1.0.2",
|
"child_process": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.6",
|
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
@@ -232,28 +231,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie-parser": {
|
|
||||||
"version": "1.4.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
|
||||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cookie": "0.7.2",
|
|
||||||
"cookie-signature": "1.0.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cookie-parser/node_modules/cookie": {
|
|
||||||
"version": "0.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cookie-signature": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"homepage": "https://github.com/antebrl/iptv-restream#readme",
|
"homepage": "https://github.com/antebrl/iptv-restream#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"child_process": "^1.0.2",
|
"child_process": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.6",
|
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const cookieParser = require('cookie-parser');
|
|
||||||
|
|
||||||
const ChatSocketHandler = require('./socket/ChatSocketHandler');
|
const ChatSocketHandler = require('./socket/ChatSocketHandler');
|
||||||
const ChannelSocketHandler = require('./socket/ChannelSocketHandler');
|
const ChannelSocketHandler = require('./socket/ChannelSocketHandler');
|
||||||
@@ -20,7 +19,6 @@ dotenv.config();
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
|
||||||
|
|
||||||
// CORS middleware
|
// CORS middleware
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ require("dotenv").config();
|
|||||||
class AuthService {
|
class AuthService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ADMIN_ENABLED = process.env.ADMIN_ENABLED === "true";
|
this.ADMIN_ENABLED = process.env.ADMIN_ENABLED === "true";
|
||||||
|
this.CHANNEL_SELECTION_REQUIRES_ADMIN =
|
||||||
|
process.env.CHANNEL_SELECTION_REQUIRES_ADMIN === "true";
|
||||||
this.ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
this.ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
||||||
this.JWT_EXPIRY = process.env.JWT_EXPIRY || "24h";
|
this.JWT_EXPIRY = process.env.JWT_EXPIRY || "24h";
|
||||||
|
|
||||||
@@ -28,6 +30,13 @@ class AuthService {
|
|||||||
.update(this.ADMIN_PASSWORD || "")
|
.update(this.ADMIN_PASSWORD || "")
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Check if channel selection needs admin
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
channelSelectionRequiresAdmin() {
|
||||||
|
return this.CHANNEL_SELECTION_REQUIRES_ADMIN && this.ADMIN_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a JWT token for an admin user
|
* Generate a JWT token for an admin user
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ module.exports = (io, socket) => {
|
|||||||
|
|
||||||
socket.on("set-current-channel", async (id) => {
|
socket.on("set-current-channel", async (id) => {
|
||||||
try {
|
try {
|
||||||
|
if (
|
||||||
|
authService.isAdminEnabled() &&
|
||||||
|
authService.channelSelectionRequiresAdmin() &&
|
||||||
|
!socket.user?.isAdmin
|
||||||
|
) {
|
||||||
|
return socket.emit("app-error", {
|
||||||
|
message: "Admin access required to switch channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
const nextChannel = await ChannelService.setCurrentChannel(id);
|
const nextChannel = await ChannelService.setCurrentChannel(id);
|
||||||
io.emit("channel-selected", nextChannel); // Broadcast to all clients
|
io.emit("channel-selected", nextChannel); // Broadcast to all clients
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ const Playlist = require("../models/Playlist");
|
|||||||
const authService = require("../services/auth/AuthService");
|
const authService = require("../services/auth/AuthService");
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
const ADMIN_ENABLED = process.env.ADMIN_ENABLED === "true";
|
|
||||||
|
|
||||||
async function handleAddPlaylist(
|
async function handleAddPlaylist(
|
||||||
{ playlist, playlistName, mode, playlistUpdate, headers },
|
{ playlist, playlistName, mode, playlistUpdate, headers },
|
||||||
io,
|
io,
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ services:
|
|||||||
STORAGE_PATH: /streams/
|
STORAGE_PATH: /streams/
|
||||||
# If you have problems with the playlist, set the backend url manually here
|
# If you have problems with the playlist, set the backend url manually here
|
||||||
#BACKEND_URL: http://localhost:5000
|
#BACKEND_URL: http://localhost:5000
|
||||||
|
|
||||||
# Admin mode configuration
|
# Admin mode configuration
|
||||||
ADMIN_ENABLED: "true"
|
# ADMIN_ENABLED: "true"
|
||||||
ADMIN_PASSWORD: "your_secure_password"
|
# ADMIN_PASSWORD: "your_secure_password"
|
||||||
|
# CHANNEL_SELECTION_REQUIRES_ADMIN: "true"
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function AppContent() {
|
|||||||
const [isPlaylistDropdownOpen, setIsPlaylistDropdownOpen] = useState(false);
|
const [isPlaylistDropdownOpen, setIsPlaylistDropdownOpen] = useState(false);
|
||||||
const [isGroupDropdownOpen, setIsGroupDropdownOpen] = useState(false);
|
const [isGroupDropdownOpen, setIsGroupDropdownOpen] = useState(false);
|
||||||
|
|
||||||
const { isAdmin, isAdminEnabled, setIsAdminEnabled } = useAdmin();
|
const { isAdmin, isAdminEnabled, setIsAdminEnabled, channelSelectRequiresAdmin, setChannelSelectRequiresAdmin } = useAdmin();
|
||||||
const { addToast } = useContext(ToastContext);
|
const { addToast } = useContext(ToastContext);
|
||||||
|
|
||||||
// Get unique playlists from channels
|
// Get unique playlists from channels
|
||||||
@@ -73,8 +73,11 @@ function AppContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if admin mode is enabled on the server
|
// Check if admin mode is enabled on the server
|
||||||
apiService
|
apiService
|
||||||
.request<{ enabled: boolean }>('/auth/admin-status', 'GET')
|
.request<{ enabled: boolean; channelSelectionRequiresAdmin: boolean }>('/auth/admin-status', 'GET')
|
||||||
.then((data) => setIsAdminEnabled(data.enabled))
|
.then((data) => {
|
||||||
|
setIsAdminEnabled(data.enabled);
|
||||||
|
setChannelSelectRequiresAdmin(data.channelSelectionRequiresAdmin);
|
||||||
|
})
|
||||||
.catch((error) => console.error('Error checking admin status:', error));
|
.catch((error) => console.error('Error checking admin status:', error));
|
||||||
|
|
||||||
apiService
|
apiService
|
||||||
@@ -345,6 +348,13 @@ function AppContent() {
|
|||||||
selectedChannel={selectedChannel}
|
selectedChannel={selectedChannel}
|
||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
onEditChannel={handleEditChannel}
|
onEditChannel={handleEditChannel}
|
||||||
|
onChannelSelectCheckPermission={() => {
|
||||||
|
if (isAdminEnabled && channelSelectRequiresAdmin && !isAdmin) {
|
||||||
|
setIsAdminModalOpen(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface ChannelListProps {
|
|||||||
selectedChannel: Channel | null;
|
selectedChannel: Channel | null;
|
||||||
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
|
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||||
onEditChannel: (channel: Channel) => void;
|
onEditChannel: (channel: Channel) => void;
|
||||||
|
onChannelSelectCheckPermission: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChannelList({
|
function ChannelList({
|
||||||
@@ -14,10 +15,13 @@ function ChannelList({
|
|||||||
selectedChannel,
|
selectedChannel,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
onEditChannel,
|
onEditChannel,
|
||||||
|
onChannelSelectCheckPermission,
|
||||||
}: ChannelListProps) {
|
}: ChannelListProps) {
|
||||||
|
|
||||||
const onSelectChannel = (channel: Channel) => {
|
const onSelectChannel = (channel: Channel) => {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
if (channel.id === selectedChannel?.id) return;
|
if (channel.id === selectedChannel?.id) return;
|
||||||
|
if (!onChannelSelectCheckPermission()) return;
|
||||||
socketService.setCurrentChannel(channel.id);
|
socketService.setCurrentChannel(channel.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import { jwtDecode } from 'jwt-decode';
|
|||||||
import socketService from '../../services/SocketService';
|
import socketService from '../../services/SocketService';
|
||||||
|
|
||||||
interface AdminContextType {
|
interface AdminContextType {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean | null;
|
||||||
setIsAdmin: (value: boolean) => void;
|
setIsAdmin: (value: boolean) => void;
|
||||||
isAdminEnabled: boolean;
|
isAdminEnabled: boolean;
|
||||||
setIsAdminEnabled: (value: boolean) => void;
|
setIsAdminEnabled: (value: boolean) => void;
|
||||||
|
channelSelectRequiresAdmin: boolean;
|
||||||
|
setChannelSelectRequiresAdmin: (value: boolean) => void;
|
||||||
adminToken: string | null;
|
adminToken: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +23,8 @@ const AdminContext = createContext<AdminContextType>({
|
|||||||
setIsAdmin: () => {},
|
setIsAdmin: () => {},
|
||||||
isAdminEnabled: false,
|
isAdminEnabled: false,
|
||||||
setIsAdminEnabled: () => {},
|
setIsAdminEnabled: () => {},
|
||||||
|
channelSelectRequiresAdmin: false,
|
||||||
|
setChannelSelectRequiresAdmin: () => {},
|
||||||
adminToken: null,
|
adminToken: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,21 +46,22 @@ const isTokenValid = (token: string): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AdminProvider: React.FC<AdminProviderProps> = ({ children }) => {
|
export const AdminProvider: React.FC<AdminProviderProps> = ({ children }) => {
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
|
||||||
const [isAdminEnabled, setIsAdminEnabled] = useState(false);
|
const [isAdminEnabled, setIsAdminEnabled] = useState(false);
|
||||||
|
const [channelSelectRequiresAdmin, setChannelSelectRequiresAdmin] = useState(false);
|
||||||
const [adminToken, setAdminToken] = useState<string | null>(null);
|
const [adminToken, setAdminToken] = useState<string | null>(null);
|
||||||
|
|
||||||
// Effect to handle token changes
|
// Effect to handle token changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// When admin status changes, update socket connection
|
// When admin status changes, update socket connection
|
||||||
if (isAdmin) {
|
if (isAdmin === true) {
|
||||||
// Small delay to ensure token is saved before reconnecting
|
// Small delay to ensure token is saved before reconnecting
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
socketService.updateAuthToken();
|
socketService.updateAuthToken();
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else if (isAdmin === false) {
|
||||||
// Reset token and reconnect
|
// Reset token and reconnect
|
||||||
localStorage.removeItem('admin_token');
|
localStorage.removeItem("admin_token");
|
||||||
setAdminToken(null);
|
setAdminToken(null);
|
||||||
socketService.updateAuthToken();
|
socketService.updateAuthToken();
|
||||||
}
|
}
|
||||||
@@ -72,7 +77,7 @@ export const AdminProvider: React.FC<AdminProviderProps> = ({ children }) => {
|
|||||||
setAdminToken(token);
|
setAdminToken(token);
|
||||||
} else if (token) {
|
} else if (token) {
|
||||||
// Clear invalid token
|
// Clear invalid token
|
||||||
localStorage.removeItem('admin_token');
|
setIsAdmin(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -83,6 +88,8 @@ export const AdminProvider: React.FC<AdminProviderProps> = ({ children }) => {
|
|||||||
setIsAdmin,
|
setIsAdmin,
|
||||||
isAdminEnabled,
|
isAdminEnabled,
|
||||||
setIsAdminEnabled,
|
setIsAdminEnabled,
|
||||||
|
channelSelectRequiresAdmin,
|
||||||
|
setChannelSelectRequiresAdmin,
|
||||||
adminToken,
|
adminToken,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user