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()); 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()) let redis_store = RedisSessionStore::new(AppConfig::get().redis_connection_string())
.await .await
.expect("Failed to connect to Redis!"); .expect("Failed to connect to Redis!");

View File

@@ -1,6 +1,6 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
export interface AuthInfo { export interface UserInfo {
id: number; id: number;
time_create: number; time_create: number;
time_update: 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 ( return (
await APIClient.exec({ await APIClient.exec({
uri: "/auth/info", uri: "/auth/info",

View File

@@ -3,13 +3,39 @@ import { useTheme } from "@mui/material/styles";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
import * as React from "react"; import * as React from "react";
import { Outlet } from "react-router"; import { Outlet, useNavigate } from "react-router";
import DashboardHeader from "./DashboardHeader"; import DashboardHeader from "./DashboardHeader";
import DashboardSidebar from "./DashboardSidebar"; 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 { export default function BaseAuthenticatedPage(): React.ReactElement {
const theme = useTheme(); 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] = const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] =
React.useState(false); React.useState(false);
const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] =
@@ -46,6 +72,23 @@ export default function BaseAuthenticatedPage(): React.ReactElement {
const layoutRef = React.useRef<HTMLDivElement>(null); const layoutRef = React.useRef<HTMLDivElement>(null);
return ( return (
<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,
}}
>
<Box <Box
ref={layoutRef} ref={layoutRef}
sx={{ sx={{
@@ -88,5 +131,12 @@ export default function BaseAuthenticatedPage(): React.ReactElement {
</Box> </Box>
</Box> </Box>
</Box> </Box>
</UserInfoContextK>
)}
/>
); );
} }
export function userUserInfo(): UserInfoContext {
return React.use(UserInfoContextK)!;
}

View File

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

View File

@@ -34,7 +34,6 @@ export default function ThemeSwitcher() {
} mode`} } mode`}
onClick={toggleMode} onClick={toggleMode}
> >
<React.Fragment>
<LightModeIcon <LightModeIcon
sx={{ sx={{
display: "inline", display: "inline",
@@ -51,7 +50,6 @@ export default function ThemeSwitcher() {
}, },
}} }}
/> />
</React.Fragment>
</IconButton> </IconButton>
</div> </div>
</Tooltip> </Tooltip>