diff --git a/backend/controllers/AuthController.js b/backend/controllers/AuthController.js index 13cb5d3..b6a5ebc 100644 --- a/backend/controllers/AuthController.js +++ b/backend/controllers/AuthController.js @@ -1,68 +1,57 @@ -require('dotenv').config(); -const jwt = require('jsonwebtoken'); - -const ADMIN_ENABLED = process.env.ADMIN_ENABLED === 'true'; -const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; -const JWT_SECRET = process.env.JWT_SECRET || 'streamhub-jwt-secret'; -const JWT_EXPIRY = '24h'; +require("dotenv").config(); +const authService = require("../services/auth/JwtService"); module.exports = { adminLogin(req, res) { - if (!ADMIN_ENABLED || ADMIN_PASSWORD === undefined) { - return res.status(403).json({ + if (!authService.isAdminEnabled()) { + return res.status(403).json({ success: false, - message: 'Admin mode is disabled on this server' + message: "Admin mode is disabled on this server", }); } const { password } = req.body; - if (password === ADMIN_PASSWORD) { - // Generate JWT token - const token = jwt.sign( - { isAdmin: true }, - JWT_SECRET, - { expiresIn: JWT_EXPIRY } - ); + if (authService.verifyAdminPassword(password)) { + const token = authService.generateAdminToken(); - return res.json({ + return res.json({ success: true, - token + token, }); } else { - return res.status(401).json({ + return res.status(401).json({ success: false, - message: 'Invalid password' + message: "Invalid password", }); } }, checkAdminStatus(req, res) { - res.json({ - enabled: ADMIN_ENABLED + res.json({ + enabled: authService.isAdminEnabled(), }); }, - // Verify JWT token middleware verifyToken(req, res, next) { - const token = req.headers.authorization?.split(' ')[1]; + const token = req.headers.authorization?.split(" ")[1]; if (!token) { - return res.status(401).json({ - success: false, - message: 'Access denied. No token provided.' + return res.status(401).json({ + success: false, + message: "Access denied. No token provided.", }); } - try { - const decoded = jwt.verify(token, JWT_SECRET); - req.user = decoded; - next(); - } catch (error) { - return res.status(401).json({ - success: false, - message: 'Invalid token.' + const decoded = authService.verifyToken(token); + if (!decoded) { + return res.status(401).json({ + success: false, + message: "Invalid token.", }); } - } -}; \ No newline at end of file + + req.user = decoded; + next(); + }, +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index 799a3fb..c7caa45 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "child_process": "^1.0.2", "cookie-parser": "^1.4.6", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.21.1", "iptv-playlist-parser": "^0.13.0", @@ -275,6 +276,12 @@ "node": ">= 0.10" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", diff --git a/backend/package.json b/backend/package.json index 8614de6..d50abcd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ "dependencies": { "child_process": "^1.0.2", "cookie-parser": "^1.4.6", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.21.1", "iptv-playlist-parser": "^0.13.0", diff --git a/backend/services/auth/JwtService.js b/backend/services/auth/JwtService.js new file mode 100644 index 0000000..1b7a7e9 --- /dev/null +++ b/backend/services/auth/JwtService.js @@ -0,0 +1,73 @@ +const jwt = require("jsonwebtoken"); +const crypto = require("crypto"); +require("dotenv").config(); + +/** + * Service for handling JWT authentication + */ +class AuthService { + constructor() { + this.ADMIN_ENABLED = process.env.ADMIN_ENABLED === "true"; + this.ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; + this.JWT_EXPIRY = process.env.JWT_EXPIRY || "24h"; + + // Validate admin password if admin mode is enabled + if ( + this.ADMIN_ENABLED && + (!this.ADMIN_PASSWORD || this.ADMIN_PASSWORD.length < 12) + ) { + throw new Error( + "ADMIN_PASSWORD must be set and at least 12 characters long for security." + ); + } + + // Generate a secure JWT secret from the admin password + // or use a random value if admin mode is disabled + this.JWT_SECRET = crypto + .createHash("sha256") + .update(this.ADMIN_PASSWORD || "") + .digest("hex"); + } + + /** + * Generate a JWT token for an admin user + * @returns {string} JWT token + */ + generateAdminToken() { + return jwt.sign({ isAdmin: true }, this.JWT_SECRET, { + expiresIn: this.JWT_EXPIRY, + }); + } + + /** + * Verify a JWT token + * @param {string} token - The JWT token to verify + * @returns {Object|null} Decoded token payload or null if invalid + */ + verifyToken(token) { + try { + return jwt.verify(token, this.JWT_SECRET); + } catch (error) { + return null; + } + } + + /** + * Check if admin mode is enabled + * @returns {boolean} True if admin mode is enabled + */ + isAdminEnabled() { + return this.ADMIN_ENABLED; + } + + /** + * Verify admin password + * @param {string} password - Password to verify + * @returns {boolean} True if password matches + */ + verifyAdminPassword(password) { + return this.ADMIN_PASSWORD === password; + } +} + +module.exports = new AuthService(); diff --git a/backend/socket/middleware/jwt.js b/backend/socket/middleware/jwt.js index c9627eb..75824ad 100644 --- a/backend/socket/middleware/jwt.js +++ b/backend/socket/middleware/jwt.js @@ -1,7 +1,4 @@ -const jwt = require('jsonwebtoken'); -require('dotenv').config(); - -const JWT_SECRET = process.env.JWT_SECRET || 'streamhub-jwt-secret'; +const authService = require("../../services/auth/JwtService"); /** * Socket.io middleware to authenticate users via JWT token @@ -16,18 +13,11 @@ function socketAuthMiddleware(socket, next) { return next(); } - try { - const decoded = jwt.verify(token, JWT_SECRET); - - // Attach the decoded user info to the socket for use in handlers - socket.user = decoded; - - return next(); - } catch (error) { - // If token is invalid, connect without admin privileges - socket.user = { isAdmin: false }; - return next(); - } + const decoded = authService.verifyToken(token); + + // Attach the decoded user info (or default non-admin) to the socket + socket.user = decoded || { isAdmin: false }; + return next(); } -module.exports = socketAuthMiddleware; \ No newline at end of file +module.exports = socketAuthMiddleware; diff --git a/docker-compose.yml b/docker-compose.yml index 6774638..3eb88ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,8 +33,6 @@ services: # Admin mode configuration ADMIN_ENABLED: "true" ADMIN_PASSWORD: "your_secure_password" - # JWT Secret for admin authentication - JWT_SECRET: "change_this_to_a_secure_random_string" networks: - app-network