Restrict server detail access for guests
All checks were successful
Deploy GSM / deploy (push) Successful in 26s
All checks were successful
Deploy GSM / deploy (push) Successful in 26s
- Add isGuest flag to UserContext - Block guests from navigating to /server/:id route - Make ServerCards non-clickable for guests - Add rejectGuest middleware to backend - Protect server detail endpoints (/:id, /metrics/history, /whitelist) Guests can now only view the dashboard overview without accessing individual server details. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,10 +48,20 @@ export function requireRole(minRole) {
|
|||||||
const userRole = req.user?.role || 'user';
|
const userRole = req.user?.role || 'user';
|
||||||
const userLevel = ROLE_HIERARCHY[userRole] || 0;
|
const userLevel = ROLE_HIERARCHY[userRole] || 0;
|
||||||
const requiredLevel = ROLE_HIERARCHY[minRole] || 0;
|
const requiredLevel = ROLE_HIERARCHY[minRole] || 0;
|
||||||
|
|
||||||
if (userLevel < requiredLevel) {
|
if (userLevel < requiredLevel) {
|
||||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rejectGuest(req, res, next) {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
|
}
|
||||||
|
if (req.user.isGuest || req.user.role === 'guest') {
|
||||||
|
return res.status(403).json({ error: 'Guests cannot access server details' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Router } from 'express';
|
|||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { authenticateToken, optionalAuth, requireRole } from '../middleware/auth.js';
|
import { authenticateToken, optionalAuth, requireRole, rejectGuest } from '../middleware/auth.js';
|
||||||
import { getServerStatus, startServer, stopServer, restartServer, getConsoleLog, getProcessUptime, listFactorioSaves, createFactorioWorld, deleteFactorioSave, getFactorioCurrentSave, isHostFailed, listZomboidConfigs, readZomboidConfig, writeZomboidConfig, listPalworldConfigs, readPalworldConfig, writePalworldConfig, readTerrariaConfig, writeTerrariaConfig, readOpenTTDConfig, writeOpenTTDConfig } from '../services/ssh.js';
|
import { getServerStatus, startServer, stopServer, restartServer, getConsoleLog, getProcessUptime, listFactorioSaves, createFactorioWorld, deleteFactorioSave, getFactorioCurrentSave, isHostFailed, listZomboidConfigs, readZomboidConfig, writeZomboidConfig, listPalworldConfigs, readPalworldConfig, writePalworldConfig, readTerrariaConfig, writeTerrariaConfig, readOpenTTDConfig, writeOpenTTDConfig } from '../services/ssh.js';
|
||||||
import { sendRconCommand, getPlayers, getPlayerList } from '../services/rcon.js';
|
import { sendRconCommand, getPlayers, getPlayerList } from '../services/rcon.js';
|
||||||
import { getServerMetricsHistory, getCurrentMetrics } from '../services/prometheus.js';
|
import { getServerMetricsHistory, getCurrentMetrics } from '../services/prometheus.js';
|
||||||
@@ -501,8 +501,8 @@ router.put("/openttd/config", authenticateToken, requireRole("moderator"), async
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Get single server
|
// Get single server (guests not allowed)
|
||||||
router.get('/:id', optionalAuth, async (req, res) => {
|
router.get('/:id', authenticateToken, rejectGuest, async (req, res) => {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const server = config.servers.find(s => s.id === req.params.id);
|
const server = config.servers.find(s => s.id === req.params.id);
|
||||||
if (!server) {
|
if (!server) {
|
||||||
@@ -578,8 +578,8 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get metrics history from Prometheus
|
// Get metrics history from Prometheus (guests not allowed)
|
||||||
router.get('/:id/metrics/history', optionalAuth, async (req, res) => {
|
router.get('/:id/metrics/history', authenticateToken, rejectGuest, async (req, res) => {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const server = config.servers.find(s => s.id === req.params.id);
|
const server = config.servers.find(s => s.id === req.params.id);
|
||||||
if (!server) return res.status(404).json({ error: 'Server not found' });
|
if (!server) return res.status(404).json({ error: 'Server not found' });
|
||||||
@@ -706,8 +706,8 @@ router.post('/:id/restart', authenticateToken, requireRole('moderator'), async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get whitelist (with server-side caching)
|
// Get whitelist (with server-side caching, guests not allowed)
|
||||||
router.get('/:id/whitelist', optionalAuth, async (req, res) => {
|
router.get('/:id/whitelist', authenticateToken, rejectGuest, async (req, res) => {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const server = config.servers.find(s => s.id === req.params.id);
|
const server = config.servers.find(s => s.id === req.params.id);
|
||||||
if (!server) return res.status(404).json({ error: 'Server not found' });
|
if (!server) return res.status(404).json({ error: 'Server not found' });
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { UserProvider } from './context/UserContext'
|
import { UserProvider, useUser } from './context/UserContext'
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
import ServerDetail from './pages/ServerDetail'
|
import ServerDetail from './pages/ServerDetail'
|
||||||
import LoginPage from './pages/LoginPage'
|
import LoginPage from './pages/LoginPage'
|
||||||
import AuthCallback from './pages/AuthCallback'
|
import AuthCallback from './pages/AuthCallback'
|
||||||
|
|
||||||
|
function ProtectedServerDetail({ onLogout }) {
|
||||||
|
const { isGuest, loading } = useUser()
|
||||||
|
|
||||||
|
if (loading) return null
|
||||||
|
if (isGuest) return <Navigate to="/" replace />
|
||||||
|
|
||||||
|
return <ServerDetail onLogout={onLogout} />
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [token, setToken] = useState(localStorage.getItem('gsm_token'))
|
const [token, setToken] = useState(localStorage.getItem('gsm_token'))
|
||||||
|
|
||||||
@@ -24,7 +33,7 @@ export default function App() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={token ? <Dashboard onLogout={handleLogout} /> : <Navigate to="/login" replace />} />
|
<Route path="/" element={token ? <Dashboard onLogout={handleLogout} /> : <Navigate to="/login" replace />} />
|
||||||
<Route path="/server/:serverId" element={token ? <ServerDetail onLogout={handleLogout} /> : <Navigate to="/login" replace />} />
|
<Route path="/server/:serverId" element={token ? <ProtectedServerDetail onLogout={handleLogout} /> : <Navigate to="/login" replace />} />
|
||||||
<Route path="/login" element={<LoginPage onLogin={handleLogin} />} />
|
<Route path="/login" element={<LoginPage onLogin={handleLogin} />} />
|
||||||
<Route path="/auth/callback" element={<AuthCallback onLogin={handleLogin} />} />
|
<Route path="/auth/callback" element={<AuthCallback onLogin={handleLogin} />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const getServerInfo = (serverName) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ServerCard({ server, onClick, isAuthenticated, displaySettings }) {
|
export default function ServerCard({ server, onClick, isAuthenticated, isGuest, displaySettings }) {
|
||||||
const defaultInfo = getServerInfo(server.name)
|
const defaultInfo = getServerInfo(server.name)
|
||||||
|
|
||||||
// Merge default info with database display settings (database takes priority)
|
// Merge default info with database display settings (database takes priority)
|
||||||
@@ -117,10 +117,12 @@ export default function ServerCard({ server, onClick, isAuthenticated, displaySe
|
|||||||
const statusBadge = getStatusBadge()
|
const statusBadge = getStatusBadge()
|
||||||
const isUnreachable = server.status === 'unreachable'
|
const isUnreachable = server.status === 'unreachable'
|
||||||
|
|
||||||
|
const isClickable = !isUnreachable && !isGuest && onClick
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={isUnreachable ? "card p-5 opacity-50 cursor-not-allowed" : "card card-clickable p-5"}
|
className={isUnreachable ? "card p-5 opacity-50 cursor-not-allowed" : (isClickable ? "card card-clickable p-5" : "card p-5")}
|
||||||
onClick={isUnreachable ? undefined : onClick}
|
onClick={isClickable ? onClick : undefined}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function UserProvider({ children, token, onLogout }) {
|
|||||||
token,
|
token,
|
||||||
loading,
|
loading,
|
||||||
role: user?.role || 'user',
|
role: user?.role || 'user',
|
||||||
|
isGuest: user?.isGuest || user?.role === 'guest',
|
||||||
isModerator: ['moderator', 'superadmin'].includes(user?.role),
|
isModerator: ['moderator', 'superadmin'].includes(user?.role),
|
||||||
isSuperadmin: user?.role === 'superadmin'
|
isSuperadmin: user?.role === 'superadmin'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,8 +255,9 @@ export default function Dashboard({ onLogout }) {
|
|||||||
>
|
>
|
||||||
<ServerCard
|
<ServerCard
|
||||||
server={server}
|
server={server}
|
||||||
onClick={() => navigate('/server/' + server.id)}
|
onClick={isGuest ? undefined : () => navigate('/server/' + server.id)}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
|
isGuest={isGuest}
|
||||||
displaySettings={displaySettings[server.id]}
|
displaySettings={displaySettings[server.id]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user