mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
admin logs for IP stuff
This commit is contained in:
@@ -25,6 +25,7 @@ require('./src/services/roomCameraSocketService');
|
|||||||
require('./src/services/roverSnapshotSocketService');
|
require('./src/services/roverSnapshotSocketService');
|
||||||
require('./src/services/embedHttpService');
|
require('./src/services/embedHttpService');
|
||||||
require('./src/services/logStreamService');
|
require('./src/services/logStreamService');
|
||||||
|
require('./src/services/adminLogService');
|
||||||
require('./src/services/homeAssistantService');
|
require('./src/services/homeAssistantService');
|
||||||
require('./src/services/sessionService');
|
require('./src/services/sessionService');
|
||||||
require('./src/services/batteryManager');
|
require('./src/services/batteryManager');
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-AuPBIRIK.js"></script>
|
<script type="module" crossorigin src="/assets/index-CGvoGgz3.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-GYsISCRb.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-GYsISCRb.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
function extractForwardedIp(value) {
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
return value.split(',')[0].trim();
|
||||||
|
}
|
||||||
|
if (Array.isArray(value) && value.length) {
|
||||||
|
return String(value[0]).trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSocketIp(socket) {
|
||||||
|
if (!socket) return null;
|
||||||
|
const headers = socket.handshake?.headers || {};
|
||||||
|
const forwarded = extractForwardedIp(headers['x-forwarded-for']);
|
||||||
|
if (forwarded) return forwarded;
|
||||||
|
const realIp = headers['x-real-ip'];
|
||||||
|
if (typeof realIp === 'string' && realIp.trim()) {
|
||||||
|
return realIp.trim();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
socket.handshake?.address ||
|
||||||
|
socket.conn?.remoteAddress ||
|
||||||
|
socket.request?.connection?.remoteAddress ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestIp(req, override) {
|
||||||
|
const fromOverride = extractForwardedIp(override);
|
||||||
|
if (fromOverride) return fromOverride;
|
||||||
|
if (!req) return null;
|
||||||
|
const headers = req.headers || {};
|
||||||
|
const forwarded = extractForwardedIp(headers['x-forwarded-for']);
|
||||||
|
if (forwarded) return forwarded;
|
||||||
|
const realIp = headers['x-real-ip'];
|
||||||
|
if (typeof realIp === 'string' && realIp.trim()) {
|
||||||
|
return realIp.trim();
|
||||||
|
}
|
||||||
|
return req.ip || req.connection?.remoteAddress || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getSocketIp,
|
||||||
|
getRequestIp,
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const io = require('../globals/io');
|
||||||
|
const { getRole, roleEvents } = require('./roleService');
|
||||||
|
const { getSocketIp } = require('../helpers/ipResolver');
|
||||||
|
|
||||||
|
const ADMIN_ROLES = new Set(['admin', 'lockdown', 'lockdown-admin']);
|
||||||
|
const MAX_HISTORY = 200;
|
||||||
|
const history = [];
|
||||||
|
|
||||||
|
function isAdminRole(role) {
|
||||||
|
return ADMIN_ROLES.has(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAdminSocket(socket) {
|
||||||
|
return isAdminRole(getRole(socket));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushEntry(entry) {
|
||||||
|
history.push(entry);
|
||||||
|
if (history.length > MAX_HISTORY) {
|
||||||
|
history.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitToAdmins(event, payload) {
|
||||||
|
io.sockets.sockets.forEach((socket) => {
|
||||||
|
if (!isAdminSocket(socket)) return;
|
||||||
|
socket.emit(event, payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function logAdminEvent({ label, message, ip, meta = null, socketId = null }) {
|
||||||
|
const entry = {
|
||||||
|
id: uuidv4(),
|
||||||
|
ts: Date.now(),
|
||||||
|
label: label || null,
|
||||||
|
message: message || '',
|
||||||
|
ip: ip || null,
|
||||||
|
meta: meta || null,
|
||||||
|
socketId: socketId || null,
|
||||||
|
};
|
||||||
|
pushEntry(entry);
|
||||||
|
emitToAdmins('adminlog:entry', entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateSocket(socket) {
|
||||||
|
if (!socket || !isAdminSocket(socket)) return;
|
||||||
|
socket.emit('adminlog:init', history);
|
||||||
|
}
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
hydrateSocket(socket);
|
||||||
|
const ip = getSocketIp(socket);
|
||||||
|
if (ip) {
|
||||||
|
logAdminEvent({
|
||||||
|
label: 'socket',
|
||||||
|
message: 'Socket connected',
|
||||||
|
ip,
|
||||||
|
meta: { role: getRole(socket) },
|
||||||
|
socketId: socket.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
const disconnectIp = getSocketIp(socket);
|
||||||
|
if (!disconnectIp) return;
|
||||||
|
logAdminEvent({
|
||||||
|
label: 'socket',
|
||||||
|
message: 'Socket disconnected',
|
||||||
|
ip: disconnectIp,
|
||||||
|
meta: { role: getRole(socket) },
|
||||||
|
socketId: socket.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
roleEvents.on('change', ({ socket, role }) => {
|
||||||
|
if (!socket) return;
|
||||||
|
if (!isAdminRole(role)) return;
|
||||||
|
hydrateSocket(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
logAdminEvent,
|
||||||
|
};
|
||||||
@@ -6,6 +6,8 @@ const { getMode, MODES } = require('./modeManager');
|
|||||||
const { isAdmin, isLockdownAdmin, getRole } = require('./roleService');
|
const { isAdmin, isLockdownAdmin, getRole } = require('./roleService');
|
||||||
const roverManager = require('./roverManager');
|
const roverManager = require('./roverManager');
|
||||||
const { loadConfig } = require('../helpers/configLoader');
|
const { loadConfig } = require('../helpers/configLoader');
|
||||||
|
const { getRequestIp } = require('../helpers/ipResolver');
|
||||||
|
const { logAdminEvent } = require('./adminLogService');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const mediaConfig = config.media || {};
|
const mediaConfig = config.media || {};
|
||||||
@@ -76,10 +78,18 @@ app.post('/mediamtx/auth', (req, res) => {
|
|||||||
const sessionId = body.user;
|
const sessionId = body.user;
|
||||||
const action = (body.action || '').toLowerCase();
|
const action = (body.action || '').toLowerCase();
|
||||||
const protocol = (body.protocol || '').toLowerCase();
|
const protocol = (body.protocol || '').toLowerCase();
|
||||||
const ip = body.ip || req.ip || '';
|
const ip = getRequestIp(req, body.ip);
|
||||||
const streamInfo = extractStreamInfo(path);
|
const streamInfo = extractStreamInfo(path);
|
||||||
|
|
||||||
logger.info('video auth request', { path: body.path, sessionId, stream: streamInfo, action, protocol, ip });
|
logger.info('video auth request', { path: body.path, sessionId, stream: streamInfo, action, protocol });
|
||||||
|
if (ip) {
|
||||||
|
logAdminEvent({
|
||||||
|
label: 'mediamtx',
|
||||||
|
message: 'Media auth request',
|
||||||
|
ip,
|
||||||
|
meta: { path: body.path, sessionId, stream: streamInfo, action, protocol },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'read' && protocol === 'srt' && streamInfo?.id) {
|
if (action === 'read' && protocol === 'srt' && streamInfo?.id) {
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const MODES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function AdminPanel() {
|
export default function AdminPanel() {
|
||||||
const { session, lockRover, setMode, requestControl, setCommunityGoal } = useSession();
|
const { session, lockRover, setMode, requestControl, setCommunityGoal, adminLogs } = useSession();
|
||||||
const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
|
const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
|
||||||
const [lockStates, setLockStates] = useState({});
|
const [lockStates, setLockStates] = useState({});
|
||||||
const health = session?.health || null;
|
const health = session?.health || null;
|
||||||
@@ -135,6 +135,7 @@ export default function AdminPanel() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ReplaySnapshotHealth health={health} />
|
<ReplaySnapshotHealth health={health} />
|
||||||
|
<AdminIpLogPanel entries={adminLogs} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -200,3 +201,36 @@ function ReplaySnapshotHealth({ health }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AdminIpLogPanel({ entries }) {
|
||||||
|
const logs = entries || [];
|
||||||
|
return (
|
||||||
|
<div className="panel-section space-y-0.5 text-base">
|
||||||
|
<div className="flex items-center justify-between text-sm text-slate-400">
|
||||||
|
<span>Admin IP log</span>
|
||||||
|
<span>{logs.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="surface h-64 overflow-y-auto font-mono text-xs">
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<p>No admin log entries yet.</p>
|
||||||
|
) : (
|
||||||
|
logs
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((entry) => (
|
||||||
|
<div key={entry.id} className="surface">
|
||||||
|
<span className="text-amber-400">
|
||||||
|
{entry.ts ? new Date(entry.ts).toLocaleTimeString() : '--'}
|
||||||
|
</span>{' '}
|
||||||
|
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
|
||||||
|
<span className="text-slate-200">{entry.message}</span>{' '}
|
||||||
|
{entry.ip && <span className="text-cyan-300">{entry.ip}</span>}{' '}
|
||||||
|
{entry.meta && <span className="text-slate-500">{JSON.stringify(entry.meta)}</span>}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500">Admin-only log stream; IPs never appear in user data.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const SessionContext = createContext({
|
|||||||
connected: false,
|
connected: false,
|
||||||
session: null,
|
session: null,
|
||||||
logs: [],
|
logs: [],
|
||||||
|
adminLogs: [],
|
||||||
login: async () => {},
|
login: async () => {},
|
||||||
setRole: async () => {},
|
setRole: async () => {},
|
||||||
requestControl: async () => {},
|
requestControl: async () => {},
|
||||||
@@ -40,6 +41,7 @@ export function SessionProvider({ children }) {
|
|||||||
const emitWithAck = useAckEmitter(socket);
|
const emitWithAck = useAckEmitter(socket);
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [adminLogs, setAdminLogs] = useState([]);
|
||||||
const [alerts, setAlerts] = useState([]);
|
const [alerts, setAlerts] = useState([]);
|
||||||
const [connected, setConnected] = useState(socket.connected);
|
const [connected, setConnected] = useState(socket.connected);
|
||||||
|
|
||||||
@@ -64,9 +66,17 @@ export function SessionProvider({ children }) {
|
|||||||
function handleLogEntry(entry) {
|
function handleLogEntry(entry) {
|
||||||
setLogs((prev) => [...prev.slice(-199), entry]);
|
setLogs((prev) => [...prev.slice(-199), entry]);
|
||||||
}
|
}
|
||||||
|
function handleAdminLogInit(entries = []) {
|
||||||
|
setAdminLogs(entries);
|
||||||
|
}
|
||||||
|
function handleAdminLogEntry(entry) {
|
||||||
|
setAdminLogs((prev) => [...prev.slice(-199), entry]);
|
||||||
|
}
|
||||||
socket.on('session:sync', handleSession);
|
socket.on('session:sync', handleSession);
|
||||||
socket.on('log:init', handleLogInit);
|
socket.on('log:init', handleLogInit);
|
||||||
socket.on('log:entry', handleLogEntry);
|
socket.on('log:entry', handleLogEntry);
|
||||||
|
socket.on('adminlog:init', handleAdminLogInit);
|
||||||
|
socket.on('adminlog:entry', handleAdminLogEntry);
|
||||||
socket.on('alert:new', (payload = {}) => {
|
socket.on('alert:new', (payload = {}) => {
|
||||||
setAlerts((prev) => [
|
setAlerts((prev) => [
|
||||||
...prev.slice(-49),
|
...prev.slice(-49),
|
||||||
@@ -80,6 +90,8 @@ export function SessionProvider({ children }) {
|
|||||||
socket.off('session:sync', handleSession);
|
socket.off('session:sync', handleSession);
|
||||||
socket.off('log:init', handleLogInit);
|
socket.off('log:init', handleLogInit);
|
||||||
socket.off('log:entry', handleLogEntry);
|
socket.off('log:entry', handleLogEntry);
|
||||||
|
socket.off('adminlog:init', handleAdminLogInit);
|
||||||
|
socket.off('adminlog:entry', handleAdminLogEntry);
|
||||||
socket.off('alert:new');
|
socket.off('alert:new');
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
@@ -114,10 +126,11 @@ export function SessionProvider({ children }) {
|
|||||||
connected,
|
connected,
|
||||||
session,
|
session,
|
||||||
logs,
|
logs,
|
||||||
|
adminLogs,
|
||||||
alerts,
|
alerts,
|
||||||
...actions,
|
...actions,
|
||||||
}),
|
}),
|
||||||
[actions, alerts, connected, logs, session],
|
[actions, adminLogs, alerts, connected, logs, session],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
||||||
|
|||||||
Reference in New Issue
Block a user