Display user name in application header

This commit is contained in:
2025-11-05 08:31:47 +01:00
parent f9fb99cdb5
commit a44327ddb0
5 changed files with 147 additions and 68 deletions

View File

@@ -17,6 +17,7 @@ async fn main() -> std::io::Result<()> {
let secret_key = Key::from(AppConfig::get().secret().as_bytes());
log::info!("Connect to Redis session store...");
let redis_store = RedisSessionStore::new(AppConfig::get().redis_connection_string())
.await
.expect("Failed to connect to Redis!");

View File

@@ -1,6 +1,6 @@
import { APIClient } from "./ApiClient";
export interface AuthInfo {
export interface UserInfo {
id: number;
time_create: number;
time_update: number;
@@ -58,9 +58,9 @@ export class AuthApi {
}
/**
* Get auth information
* Get user information
*/
static async GetAuthInfo(): Promise<AuthInfo> {
static async GetUserInfo(): Promise<UserInfo> {
return (
await APIClient.exec({
uri: "/auth/info",

View File

@@ -3,13 +3,39 @@ import { useTheme } from "@mui/material/styles";
import Toolbar from "@mui/material/Toolbar";
import useMediaQuery from "@mui/material/useMediaQuery";
import * as React from "react";
import { Outlet } from "react-router";
import { Outlet, useNavigate } from "react-router";
import DashboardHeader from "./DashboardHeader";
import DashboardSidebar from "./DashboardSidebar";
import { AuthApi, type UserInfo } from "../../api/AuthApi";
import { AsyncWidget } from "../AsyncWidget";
import { Button } from "@mui/material";
import { useAuth } from "../../App";
interface UserInfoContext {
info: UserInfo;
reloadUserInfo: () => void;
signOut: () => void;
}
const UserInfoContextK = React.createContext<UserInfoContext | null>(null);
export default function BaseAuthenticatedPage(): React.ReactElement {
const theme = useTheme();
const [userInfo, setuserInfo] = React.useState<null | UserInfo>(null);
const loadUserInfo = async () => {
setuserInfo(await AuthApi.GetUserInfo());
};
const auth = useAuth();
const navigate = useNavigate();
const signOut = () => {
AuthApi.SignOut();
navigate("/");
auth.setSignedIn(false);
};
const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] =
React.useState(false);
const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] =
@@ -46,47 +72,71 @@ export default function BaseAuthenticatedPage(): React.ReactElement {
const layoutRef = React.useRef<HTMLDivElement>(null);
return (
<Box
ref={layoutRef}
sx={{
position: "relative",
display: "flex",
overflow: "hidden",
height: "100%",
width: "100%",
}}
>
<DashboardHeader
menuOpen={isNavigationExpanded}
onToggleMenu={handleToggleHeaderMenu}
/>
<DashboardSidebar
expanded={isNavigationExpanded}
setExpanded={setIsNavigationExpanded}
container={layoutRef?.current ?? undefined}
/>
<Box
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
minWidth: 0,
}}
>
<Toolbar sx={{ displayPrint: "none" }} />
<Box
component="main"
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
overflow: "auto",
padding: "50px",
<AsyncWidget
loadKey="1"
load={loadUserInfo}
errMsg="Failed to load user information!"
errAdditionalElement={() => (
<>
<Button onClick={signOut}>Sign out</Button>
</>
)}
build={() => (
<UserInfoContextK
value={{
info: userInfo!,
reloadUserInfo: loadUserInfo,
signOut,
}}
>
<Outlet />
</Box>
</Box>
</Box>
<Box
ref={layoutRef}
sx={{
position: "relative",
display: "flex",
overflow: "hidden",
height: "100%",
width: "100%",
}}
>
<DashboardHeader
menuOpen={isNavigationExpanded}
onToggleMenu={handleToggleHeaderMenu}
/>
<DashboardSidebar
expanded={isNavigationExpanded}
setExpanded={setIsNavigationExpanded}
container={layoutRef?.current ?? undefined}
/>
<Box
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
minWidth: 0,
}}
>
<Toolbar sx={{ displayPrint: "none" }} />
<Box
component="main"
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
overflow: "auto",
padding: "50px",
}}
>
<Outlet />
</Box>
</Box>
</Box>
</UserInfoContextK>
)}
/>
);
}
export function userUserInfo(): UserInfoContext {
return React.use(UserInfoContextK)!;
}

View File

@@ -1,7 +1,9 @@
import { mdiMessageTextFast } from "@mdi/js";
import Icon from "@mdi/react";
import LogoutIcon from "@mui/icons-material/Logout";
import MenuIcon from "@mui/icons-material/Menu";
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
import { Avatar } from "@mui/material";
import MuiAppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
@@ -12,6 +14,7 @@ import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import * as React from "react";
import { RouterLink } from "../RouterLink";
import { userUserInfo as useUserInfo } from "./BaseAuthenticatedPage";
import ThemeSwitcher from "./ThemeSwitcher";
const AppBar = styled(MuiAppBar)(({ theme }) => ({
@@ -42,6 +45,8 @@ export default function DashboardHeader({
menuOpen,
onToggleMenu,
}: DashboardHeaderProps) {
const user = useUserInfo();
const handleMenuOpen = React.useCallback(() => {
onToggleMenu(!menuOpen);
}, [menuOpen, onToggleMenu]);
@@ -101,6 +106,7 @@ export default function DashboardHeader({
ml: 1,
whiteSpace: "nowrap",
lineHeight: 1,
display: { xs: "none", sm: "block" },
}}
>
MatrixGW
@@ -108,17 +114,41 @@ export default function DashboardHeader({
</Stack>
</RouterLink>
</Stack>
{/* User avatar */}
<Stack
direction="row"
alignItems="center"
spacing={1}
sx={{ marginLeft: "auto" }}
sx={{
p: 2,
gap: 1,
alignItems: "center",
borderTop: "1px solid",
borderColor: "divider",
}}
>
<Stack direction="row" alignItems="center">
<ThemeSwitcher />
</Stack>
<Avatar
sizes="small"
alt={user.info.name}
sx={{ width: 36, height: 36 }}
/>
<Box sx={{ mr: "auto", display: { xs: "none", md: "block" } }}>
<Typography
variant="body2"
sx={{ fontWeight: 500, lineHeight: "16px" }}
>
{user.info.name}
</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{user.info.email}
</Typography>
</Box>
<ThemeSwitcher />
<Tooltip title="Sign out">
<IconButton size="small" onClick={user.signOut}>
<LogoutIcon />
</IconButton>
</Tooltip>
</Stack>
Hi TODO USER SIGN OUT
</Stack>
</Toolbar>
</AppBar>

View File

@@ -34,24 +34,22 @@ export default function ThemeSwitcher() {
} mode`}
onClick={toggleMode}
>
<React.Fragment>
<LightModeIcon
sx={{
display: "inline",
[theme.getColorSchemeSelector("dark")]: {
display: "none",
},
}}
/>
<DarkModeIcon
sx={{
<LightModeIcon
sx={{
display: "inline",
[theme.getColorSchemeSelector("dark")]: {
display: "none",
[theme.getColorSchemeSelector("dark")]: {
display: "inline",
},
}}
/>
</React.Fragment>
},
}}
/>
<DarkModeIcon
sx={{
display: "none",
[theme.getColorSchemeSelector("dark")]: {
display: "inline",
},
}}
/>
</IconButton>
</div>
</Tooltip>