Can sign out
This commit is contained in:
parent
44d565c6da
commit
d67f42abc5
@ -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();
|
||||||
|
@ -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(),
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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)!;
|
||||||
|
}
|
@ -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}>
|
||||||
<LoadServerConfig>
|
<LoadingMessageProvider>
|
||||||
<App />
|
<LoadServerConfig>
|
||||||
</LoadServerConfig>
|
<App />
|
||||||
|
</LoadServerConfig>
|
||||||
|
</LoadingMessageProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
61
virtweb_frontend/src/widgets/VirtWebAppBar.tsx
Normal file
61
virtweb_frontend/src/widgets/VirtWebAppBar.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user