Can sign out
This commit is contained in:
		@@ -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>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user