Can sign out

This commit is contained in:
Pierre HUBERT 2023-09-04 15:12:00 +02:00
parent 44d565c6da
commit d67f42abc5
9 changed files with 184 additions and 7 deletions

View File

@ -81,6 +81,18 @@ impl AppConfig {
&ARGS &ARGS
} }
/// Get auth cookie domain
pub fn cookie_domain(&self) -> Option<String> {
let domain = self.website_origin.split_once("://")?.1;
Some(
domain
.split_once(':')
.map(|s| s.0)
.unwrap_or(domain)
.to_string(),
)
}
/// Get app secret /// Get app secret
pub fn secret(&self) -> &str { pub fn secret(&self) -> &str {
let mut secret = self.secret.as_str(); let mut secret = self.secret.as_str();

View File

@ -34,6 +34,8 @@ async fn main() -> std::io::Result<()> {
.cookie_name(SESSION_COOKIE_NAME.to_string()) .cookie_name(SESSION_COOKIE_NAME.to_string())
.cookie_secure(AppConfig::get().cookie_secure) .cookie_secure(AppConfig::get().cookie_secure)
.cookie_same_site(SameSite::Strict) .cookie_same_site(SameSite::Strict)
.cookie_domain(AppConfig::get().cookie_domain())
.cookie_http_only(true)
.build(); .build();
let identity_middleware = IdentityMiddleware::builder() let identity_middleware = IdentityMiddleware::builder()
@ -51,11 +53,11 @@ async fn main() -> std::io::Result<()> {
.max_age(3600); .max_age(3600);
App::new() App::new()
.wrap(cors)
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(AuthChecker) .wrap(AuthChecker)
.wrap(identity_middleware) .wrap(identity_middleware)
.wrap(session_mw) .wrap(session_mw)
.wrap(cors)
.app_data(state_manager.clone()) .app_data(state_manager.clone())
.app_data(Data::new(RemoteIPConfig { .app_data(Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(), proxy: AppConfig::get().proxy_ip.clone(),

View File

@ -71,7 +71,7 @@ where
"Failed to extract authentication information from request! {e}" "Failed to extract authentication information from request! {e}"
); );
return Ok(req return Ok(req
.into_response(HttpResponse::InternalServerError().finish()) .into_response(HttpResponse::PreconditionFailed().finish())
.map_into_right_body()); .map_into_right_body());
} }
}; };
@ -81,7 +81,9 @@ where
"User attempted to access privileged route without authentication!" "User attempted to access privileged route without authentication!"
); );
return Ok(req return Ok(req
.into_response(HttpResponse::Unauthorized().json("Please authenticate!")) .into_response(
HttpResponse::PreconditionFailed().json("Please authenticate!"),
)
.map_into_right_body()); .map_into_right_body());
} }
} }

View File

@ -57,6 +57,7 @@ export class APIClient {
method: args.method, method: args.method,
body: body, body: body,
headers: headers, headers: headers,
credentials: "include",
}); });
// Process response // Process response

View File

@ -1,5 +1,9 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
export interface AuthInfo {
id: string;
}
const TokenStateKey = "auth-state"; const TokenStateKey = "auth-state";
export class AuthApi { export class AuthApi {
@ -71,6 +75,18 @@ export class AuthApi {
this.SetAuthenticated(); this.SetAuthenticated();
} }
/**
* Get auth information
*/
static async GetAuthInfo(): Promise<AuthInfo> {
return (
await APIClient.exec({
uri: "/auth/user",
method: "GET",
})
).data;
}
/** /**
* Sign out * Sign out
*/ */

View File

@ -0,0 +1,64 @@
import {
CircularProgress,
Dialog,
DialogContent,
DialogContentText,
} from "@mui/material";
import React, { PropsWithChildren } from "react";
type LoadingMessageContext = {
show: (message: string) => void;
hide: () => void;
};
const LoadingMessageContextK =
React.createContext<LoadingMessageContext | null>(null);
export function LoadingMessageProvider(
p: PropsWithChildren
): React.ReactElement {
const [open, setOpen] = React.useState(false);
const [message, setMessage] = React.useState("");
const hook: LoadingMessageContext = {
show(message) {
setMessage(message);
setOpen(true);
},
hide() {
setMessage("");
setOpen(false);
},
};
return (
<>
<LoadingMessageContextK.Provider value={hook}>
{p.children}
</LoadingMessageContextK.Provider>
<Dialog open={open}>
<DialogContent>
<DialogContentText>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress style={{ marginRight: "15px" }} />
{message}
</div>
</DialogContentText>
</DialogContent>
</Dialog>
</>
);
}
export function useLoadingMessage(): LoadingMessageContext {
return React.useContext(LoadingMessageContextK)!;
}

View File

@ -10,6 +10,7 @@ import "./index.css";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import { LoadServerConfig } from "./widgets/LoadServerConfig"; import { LoadServerConfig } from "./widgets/LoadServerConfig";
import { ThemeProvider, createTheme } from "@mui/material"; import { ThemeProvider, createTheme } from "@mui/material";
import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
const darkTheme = createTheme({ const darkTheme = createTheme({
palette: { palette: {
@ -23,9 +24,11 @@ const root = ReactDOM.createRoot(
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>
<LoadingMessageProvider>
<LoadServerConfig> <LoadServerConfig>
<App /> <App />
</LoadServerConfig> </LoadServerConfig>
</LoadingMessageProvider>
</ThemeProvider> </ThemeProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -1,3 +1,19 @@
import { Box } from "@mui/material";
import { VirtWebAppBar } from "./VirtWebAppBar";
export function BaseAuthenticatedPage(): React.ReactElement { export function BaseAuthenticatedPage(): React.ReactElement {
return <>ready with login</>; return (
<Box
component="div"
sx={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
backgroundColor: (theme) => theme.palette.grey[900],
color: (theme) => theme.palette.grey[100],
}}
>
<VirtWebAppBar />
</Box>
);
} }

View File

@ -0,0 +1,61 @@
import { mdiLogout, mdiServer } from "@mdi/js";
import Icon from "@mdi/react";
import { AppBar, IconButton, Toolbar, Typography } from "@mui/material";
import { RouterLink } from "./RouterLink";
import { AsyncWidget } from "./AsyncWidget";
import { AuthApi, AuthInfo } from "../api/AuthApi";
import React from "react";
import { useAuth } from "../App";
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
export function VirtWebAppBar(): React.ReactElement {
const loadingMessage = useLoadingMessage();
const auth = useAuth();
const [info, setInfo] = React.useState<null | AuthInfo>(null);
const load = async () => {
setInfo(await AuthApi.GetAuthInfo());
};
const signOut = async () => {
loadingMessage.show("Signing out...");
try {
await AuthApi.SignOut();
} catch (e) {
console.error(e);
}
auth.setSignedIn(false);
loadingMessage.hide();
};
return (
<AsyncWidget
loadKey={0}
load={load}
errMsg="Failed to load user information!"
build={() => (
<AppBar position="sticky">
<Toolbar>
<Icon
path={mdiServer}
style={{ height: "1.2rem", marginRight: "1rem" }}
/>
<Typography variant="h6" color="inherit" noWrap>
<RouterLink to={"/"}>VirtWeb</RouterLink>
</Typography>
<div style={{ flex: 1 }}></div>
<Typography variant="body1">{info?.id}</Typography>
<IconButton onClick={signOut}>
<Icon path={mdiLogout} size={1} />
</IconButton>
</Toolbar>
</AppBar>
)}
/>
);
}