Added Logout User Menu

This commit is contained in:
2025-09-24 15:01:17 -06:00
parent 0ef4ada84e
commit 7d7f9c384c
3 changed files with 96 additions and 24 deletions

View File

@@ -1,9 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Paper, Box, Typography } from "@mui/material"; import { Paper, Box, Typography, Button } from "@mui/material";
import { GitHub as GitHubIcon, InfoOutlined as InfoIcon } from "@mui/icons-material";
import { CreditsDialog } from "../Dialogs.jsx";
export default function ServerInfo({ isAdmin = false }) { export default function ServerInfo({ isAdmin = false }) {
const [serverTime, setServerTime] = useState(null); const [serverTime, setServerTime] = useState(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [aboutOpen, setAboutOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (!isAdmin) return; if (!isAdmin) return;
@@ -39,7 +42,32 @@ export default function ServerInfo({ isAdmin = false }) {
{error ? `Error: ${error}` : (serverTime || 'Loading...')} {error ? `Error: ${error}` : (serverTime || 'Loading...')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle1" sx={{ color: "#58a6ff", mb: 1 }}>Project Links</Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Button
variant="outlined"
color="primary"
startIcon={<GitHubIcon />}
onClick={() => window.open("https://github.com/bunny-lab-io/Borealis", "_blank")}
sx={{ borderColor: '#3a3a3a', color: '#7db7ff' }}
>
GitHub Project
</Button>
<Button
variant="outlined"
color="inherit"
startIcon={<InfoIcon />}
onClick={() => setAboutOpen(true)}
sx={{ borderColor: '#3a3a3a', color: '#ddd' }}
>
About Borealis
</Button>
</Box>
</Box>
</Box> </Box>
<CreditsDialog open={aboutOpen} onClose={() => setAboutOpen(false)} />
</Paper> </Paper>
); );
} }

View File

@@ -5,7 +5,7 @@ import React, { useState, useEffect, useCallback, useRef } from "react";
import { ReactFlowProvider } from "reactflow"; import { ReactFlowProvider } from "reactflow";
import "reactflow/dist/style.css"; import "reactflow/dist/style.css";
import { import {
CloseAllDialog, CreditsDialog, RenameTabDialog, TabContextMenu, NotAuthorizedDialog CloseAllDialog, RenameTabDialog, TabContextMenu, NotAuthorizedDialog
} from "./Dialogs"; } from "./Dialogs";
import NavigationSidebar from "./Navigation_Sidebar"; import NavigationSidebar from "./Navigation_Sidebar";
@@ -16,9 +16,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { import {
KeyboardArrowDown as KeyboardArrowDownIcon, KeyboardArrowDown as KeyboardArrowDownIcon,
InfoOutlined as InfoOutlinedIcon, Logout as LogoutIcon,
GitHub as GitHub,
People as PeopleIcon,
NavigateNext as NavigateNextIcon NavigateNext as NavigateNextIcon
} from "@mui/icons-material"; } from "@mui/icons-material";
@@ -91,8 +89,7 @@ export default function App() {
const [currentPage, setCurrentPage] = useState("devices"); const [currentPage, setCurrentPage] = useState("devices");
const [selectedDevice, setSelectedDevice] = useState(null); const [selectedDevice, setSelectedDevice] = useState(null);
const [aboutAnchorEl, setAboutAnchorEl] = useState(null); const [userMenuAnchorEl, setUserMenuAnchorEl] = useState(null);
const [creditsDialogOpen, setCreditsDialogOpen] = useState(false);
const [confirmCloseOpen, setConfirmCloseOpen] = useState(false); const [confirmCloseOpen, setConfirmCloseOpen] = useState(false);
const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [renameTabId, setRenameTabId] = useState(null); const [renameTabId, setRenameTabId] = useState(null);
@@ -102,6 +99,7 @@ export default function App() {
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [userRole, setUserRole] = useState(null); const [userRole, setUserRole] = useState(null);
const [userDisplayName, setUserDisplayName] = useState(null);
const [editingJob, setEditingJob] = useState(null); const [editingJob, setEditingJob] = useState(null);
const [jobsRefreshToken, setJobsRefreshToken] = useState(0); const [jobsRefreshToken, setJobsRefreshToken] = useState(0);
const [notAuthorizedOpen, setNotAuthorizedOpen] = useState(false); const [notAuthorizedOpen, setNotAuthorizedOpen] = useState(false);
@@ -180,6 +178,7 @@ export default function App() {
if (Date.now() - data.timestamp < 3600 * 1000) { if (Date.now() - data.timestamp < 3600 * 1000) {
setUser(data.username); setUser(data.username);
setUserRole(data.role || null); setUserRole(data.role || null);
setUserDisplayName(data.display_name || data.username);
} else { } else {
localStorage.removeItem("borealis_session"); localStorage.removeItem("borealis_session");
} }
@@ -194,9 +193,10 @@ export default function App() {
const me = await resp.json(); const me = await resp.json();
setUser(me.username); setUser(me.username);
setUserRole(me.role || null); setUserRole(me.role || null);
setUserDisplayName(me.display_name || me.username);
localStorage.setItem( localStorage.setItem(
"borealis_session", "borealis_session",
JSON.stringify({ username: me.username, role: me.role, timestamp: Date.now() }) JSON.stringify({ username: me.username, display_name: me.display_name || me.username, role: me.role, timestamp: Date.now() })
); );
} }
} catch {} } catch {}
@@ -206,10 +206,25 @@ export default function App() {
const handleLoginSuccess = ({ username, role }) => { const handleLoginSuccess = ({ username, role }) => {
setUser(username); setUser(username);
setUserRole(role || null); setUserRole(role || null);
setUserDisplayName(username);
localStorage.setItem( localStorage.setItem(
"borealis_session", "borealis_session",
JSON.stringify({ username, role: role || null, timestamp: Date.now() }) JSON.stringify({ username, display_name: username, role: role || null, timestamp: Date.now() })
); );
// Refresh full profile (to get display_name) in background
(async () => {
try {
const resp = await fetch('/api/auth/me', { credentials: 'include' });
if (resp.ok) {
const me = await resp.json();
setUserDisplayName(me.display_name || me.username);
localStorage.setItem(
"borealis_session",
JSON.stringify({ username: me.username, display_name: me.display_name || me.username, role: me.role, timestamp: Date.now() })
);
}
} catch {}
})();
}; };
useEffect(() => { useEffect(() => {
@@ -257,9 +272,17 @@ export default function App() {
); );
}, [activeTabId]); }, [activeTabId]);
const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget); const handleUserMenuOpen = (event) => setUserMenuAnchorEl(event.currentTarget);
const handleAboutMenuClose = () => setAboutAnchorEl(null); const handleUserMenuClose = () => setUserMenuAnchorEl(null);
const openCreditsDialog = () => { handleAboutMenuClose(); setCreditsDialogOpen(true); }; const handleLogout = async () => {
try {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
} catch {}
try { localStorage.removeItem('borealis_session'); } catch {}
setUser(null);
setUserRole(null);
setUserDisplayName(null);
};
const handleTabRightClick = (evt, tabId) => { const handleTabRightClick = (evt, tabId) => {
evt.preventDefault(); evt.preventDefault();
@@ -635,23 +658,19 @@ export default function App() {
})} })}
</Breadcrumbs> </Breadcrumbs>
</Box> </Box>
{/* Spacer to keep About aligned right */} {/* Spacer to keep user menu aligned right */}
<Box sx={{ flexGrow: 1 }} /> <Box sx={{ flexGrow: 1 }} />
<Button <Button
color="inherit" color="inherit"
onClick={handleAboutMenuOpen} onClick={handleUserMenuOpen}
endIcon={<KeyboardArrowDownIcon />} endIcon={<KeyboardArrowDownIcon />}
startIcon={<InfoOutlinedIcon />}
sx={{ height: "36px" }} sx={{ height: "36px" }}
> >
About {userDisplayName || user || 'User'}
</Button> </Button>
<Menu anchorEl={aboutAnchorEl} open={Boolean(aboutAnchorEl)} onClose={handleAboutMenuClose}> <Menu anchorEl={userMenuAnchorEl} open={Boolean(userMenuAnchorEl)} onClose={handleUserMenuClose}>
<MenuItem onClick={() => { handleAboutMenuClose(); window.open("https://github.com/bunny-lab-io/Borealis", "_blank"); }}> <MenuItem onClick={() => { handleUserMenuClose(); handleLogout(); }}>
<GitHub sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Github Project <LogoutIcon sx={{ fontSize: 18, color: "#ff6b6b", mr: 1 }} /> Logout
</MenuItem>
<MenuItem onClick={openCreditsDialog}>
<PeopleIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Credits
</MenuItem> </MenuItem>
</Menu> </Menu>
</Toolbar> </Toolbar>
@@ -664,7 +683,6 @@ export default function App() {
</Box> </Box>
</Box> </Box>
<CloseAllDialog open={confirmCloseOpen} onClose={() => setConfirmCloseOpen(false)} onConfirm={() => {}} /> <CloseAllDialog open={confirmCloseOpen} onClose={() => setConfirmCloseOpen(false)} onConfirm={() => {}} />
<CreditsDialog open={creditsDialogOpen} onClose={() => setCreditsDialogOpen(false)} />
<RenameTabDialog <RenameTabDialog
open={renameDialogOpen} open={renameDialogOpen}
value={renameValue} value={renameValue}

View File

@@ -281,7 +281,33 @@ def api_me():
user = _current_user() user = _current_user()
if not user: if not user:
return jsonify({"error": "unauthorized"}), 401 return jsonify({"error": "unauthorized"}), 401
return jsonify(user) # Enrich with display_name if possible
username = (user.get('username') or '').strip()
try:
conn = _db_conn()
cur = conn.cursor()
cur.execute(
"SELECT id, username, display_name, role, last_login, created_at, updated_at FROM users WHERE LOWER(username)=LOWER(?)",
(username,)
)
row = cur.fetchone()
conn.close()
if row:
info = _user_row_to_dict(row)
# Return minimal fields but include display_name
return jsonify({
"username": info['username'],
"display_name": info['display_name'],
"role": info['role']
})
except Exception:
pass
# Fallback to original shape
return jsonify({
"username": username,
"display_name": username,
"role": user.get('role') or 'User'
})
@app.route("/api/users", methods=["GET"]) @app.route("/api/users", methods=["GET"])