notification context and attack page

This commit is contained in:
Vomitblood 2024-11-12 17:47:26 +08:00
parent 0afbdd97d9
commit a838742882
17 changed files with 395 additions and 145 deletions

View file

@ -22,7 +22,8 @@
"jotai": "^2.10.1",
"next": "^15.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"uuid": "^11.0.3"
},
"devDependencies": {
"@tauri-apps/cli": "^2.1.0",
@ -5553,6 +5554,19 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -24,7 +24,8 @@
"jotai": "^2.10.1",
"next": "^15.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"uuid": "^11.0.3"
},
"devDependencies": {
"@tauri-apps/cli": "^2.1.0",

View file

@ -18,7 +18,7 @@ export const NavigationButtons = () => {
};
const goHome = () => {
setRoute("index");
setRoute("home");
};
return (
@ -29,15 +29,27 @@ export const NavigationButtons = () => {
flexDirection: "row",
}}
>
<Stack direction="row" spacing={1}>
<IconButton onClick={goBack} size="small">
<ArrowBack fontSize="inherit" />
<Stack
direction='row'
spacing={1}
>
<IconButton
onClick={goBack}
size='small'
>
<ArrowBack fontSize='inherit' />
</IconButton>
<IconButton onClick={goForward} size="small">
<ArrowForward fontSize="inherit" />
<IconButton
onClick={goForward}
size='small'
>
<ArrowForward fontSize='inherit' />
</IconButton>
<IconButton onClick={goHome} size="small">
<HomeOutlined fontSize="inherit" />
<IconButton
onClick={goHome}
size='small'
>
<HomeOutlined fontSize='inherit' />
</IconButton>
</Stack>
</Box>

View file

@ -1,5 +1,37 @@
import { Box } from "@mui/material";
import { useAtom } from "jotai";
import { routeAtom } from "../../lib/jotai";
import { useEffect, useState } from "react";
export const RouteDisplay = () => {
return <Box>Home</Box>;
// atoms
const [route, setRoute] = useAtom(routeAtom);
// states
const [routeName, setRouteName] = useState("Home");
// update the route name based on the route value
useEffect(() => {
switch (route) {
case "home":
setRouteName("Home");
break;
case "sql_injection":
setRouteName("SQL Injection");
break;
case "xss":
setRouteName("Cross-site Scripting");
break;
case "cmd_injection":
setRouteName("Command Injection");
break;
case "testing":
setRouteName("Testing");
break;
default:
setRouteName("Home");
}
}, [route]);
return <Box>{routeName}</Box>;
};

View file

@ -5,6 +5,7 @@ import { MouseEvent, useEffect, useState } from "react";
import { serverConnectionAtom, serverUrlAtom } from "../../lib/jotai";
import { defaultSettings } from "../../lib/settings";
import { ServerUrlInput } from "./ServerUrlInput";
import { sleep } from "../../lib/utility";
export const ServerStatus = () => {
// atoms
@ -24,6 +25,8 @@ export const ServerStatus = () => {
// function to check server health
const checkServerConnection = async () => {
setServerConnection("connecting");
// remove trailing slash
setServerUrl(serverUrl.replace(/\/$/, ""));
@ -34,10 +37,12 @@ export const ServerStatus = () => {
setServerConnection("connected");
} else {
console.log("disconnected");
await sleep(500);
setServerConnection("disconnected");
}
} catch (e) {
console.log("disconnected", e);
await sleep(500);
setServerConnection("disconnected");
}
};

View file

@ -1,37 +0,0 @@
import {
Card,
CardActionArea,
CardContent,
Typography,
useTheme,
} from "@mui/material";
import { FC } from "react";
type AttackItemProps = {
attackName: string;
};
export const AttackItem: FC<AttackItemProps> = ({ attackName }) => {
// contexts
const theme = useTheme();
return (
<Card
sx={{
backgroundColor: theme.palette.background.default,
borderRadius: "8px",
}}
variant="outlined"
>
<CardActionArea
onClick={() => {
console.log("clicked");
}}
>
<CardContent>
<Typography component="div">{attackName}</Typography>
</CardContent>
</CardActionArea>
</Card>
);
};

View file

@ -1,89 +0,0 @@
import { Box, Button, Container, Grid2, Switch, TextField, Typography, useTheme } from "@mui/material";
import { HeaderBar } from "../HeaderBar/HeaderBar";
import { AttackItem } from "./AttackItem";
import { ServerUrlInput } from "../HeaderBar/ServerUrlInput";
export const Layout = () => {
// contexts
const theme = useTheme();
return (
<Box
sx={{
backgroundColor: theme.palette.background.default,
display: "flex",
flexDirection: "column",
height: "100vh",
}}
>
<HeaderBar />
<Container
maxWidth='lg'
sx={{
justifyContent: "center",
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
p: 1,
}}
>
{/* main content goes here buddy */}
<Box
sx={{
mb: 2,
}}
>
<Box
sx={{
alignItems: "center",
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Typography
sx={{
mb: 2,
}}
variant='h3'
>
CSPJ Application Attack Simulator
</Typography>
</Box>
<Button href='https://github.com/cspj-nyp/cspj-application'>Need help getting started?</Button>
</Box>
<Grid2
container
spacing={2}
>
<Grid2 size={3}>
<AttackItem attackName='SQL Injection' />
</Grid2>
<Grid2 size={3}>
<AttackItem attackName='Cross Site Scripting' />
</Grid2>
<Grid2 size={3}>
<AttackItem attackName='Command Injection' />
</Grid2>
<Grid2 size={3}>
<AttackItem attackName='File Inclusion Attacks' />
</Grid2>
<Grid2 size={3}>
<AttackItem attackName='CSRF' />
</Grid2>
<Grid2 size={3}>
<AttackItem attackName='Directory Traversal' />
</Grid2>
<Grid2 size={3}>
<AttackItem attackName='Insecure Desrialization' />
</Grid2>
<Grid2 size={3}>
<AttackItem attackName='Session Hijacking' />
</Grid2>
</Grid2>
</Container>
</Box>
);
};

View file

@ -0,0 +1,37 @@
import { Card, CardActionArea, CardContent, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { routeAtom, Routes } from "../../../lib/jotai";
import { useAtom } from "jotai";
type AttackItemProps = {
attackName: string;
routeTarget: Routes;
};
export const AttackItem: FC<AttackItemProps> = ({ attackName, routeTarget }) => {
// contexts
const theme = useTheme();
// atoms
const [route, setRoute] = useAtom(routeAtom);
return (
<Card
sx={{
backgroundColor: theme.palette.background.default,
borderRadius: "8px",
}}
variant='outlined'
>
<CardActionArea
onClick={() => {
setRoute(routeTarget);
}}
>
<CardContent>
<Typography component='div'>{attackName}</Typography>
</CardContent>
</CardActionArea>
</Card>
);
};

View file

@ -0,0 +1,96 @@
import { Box, Typography, Button, Grid2 } from "@mui/material";
import { AttackItem } from "./AttackItem";
export const Home = () => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
justifyContent: "center",
p: 2,
}}
>
<Box>
<Box
sx={{
alignItems: "center",
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Typography
sx={{
mb: 2,
}}
variant='h3'
>
CSPJ Application Attack Simulator
</Typography>
</Box>
<Button href='https://github.com/cspj-nyp/cspj-application'>Need help getting started?</Button>
</Box>
<Grid2
container
spacing={2}
>
<Grid2 size={3}>
<AttackItem
attackName='SQL Injection'
routeTarget='sql_injection'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='Cross Site Scripting'
routeTarget='xss'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='Command Injection'
routeTarget='cmd_injection'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='File Inclusion Attacks'
routeTarget='cmd_injection'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='CSRF'
routeTarget='cmd_injection'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='Directory Traversal'
routeTarget='cmd_injection'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='Insecure Desrialization'
routeTarget='cmd_injection'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='Session Hijacking'
routeTarget='cmd_injection'
/>
</Grid2>
<Grid2 size={3}>
<AttackItem
attackName='Testing page'
routeTarget='testing'
/>
</Grid2>
</Grid2>
</Box>
);
};

View file

@ -0,0 +1,15 @@
import { Box } from "@mui/material";
export const AttackPage = ({}) => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "start",
}}
>
asdf
</Box>
);
};

View file

@ -0,0 +1,21 @@
import { Box, Button } from "@mui/material";
import { useNotification } from "../../../contexts/NotificationContext";
export const SqlInjection = () => {
// contexts
const { openNotification } = useNotification();
return (
<Box>
<h1>SQL Injection</h1>
<p>SQL Injection is a code injection technique</p>
<Button
onClick={() => {
openNotification("deez nuts", "", 3000);
}}
>
clicky
</Button>
</Box>
);
};

View file

@ -0,0 +1,10 @@
import { Box } from "@mui/material";
export const Xss = () => {
return (
<Box>
<h1>XSS</h1>
<p>Cross-site scripting (XSS) is a type of security vulnerability</p>
</Box>
);
};

View file

@ -0,0 +1,67 @@
import { Alert, Snackbar, useTheme } from "@mui/material";
import { FC, ReactNode, createContext, useContext, useState } from "react";
import { v4 } from "uuid";
type Notification = {
id: string;
message: string;
color?: string;
duration?: number;
};
type NotificationContextProps = {
notifications: Notification[];
openNotification: (message: string, color?: string, duration?: number) => void;
closeNotification: (id: string) => void;
};
const NotificationContext = createContext<NotificationContextProps | undefined>(undefined);
export const NotificationProvider: FC<{ children: ReactNode }> = ({ children }) => {
const theme = useTheme();
const [notifications, setNotifications] = useState<Notification[]>([]);
const openNotification = (message: string, color?: string, duration: number = 3000) => {
const id = v4();
setNotifications((prevNotifications) => [...prevNotifications, { id, message, color, duration }]);
setTimeout(() => {
closeNotification(id);
}, duration);
};
const closeNotification = (id: string) => {
setNotifications((prevNotifications) => prevNotifications.filter((notification) => notification.id !== id));
};
return (
<NotificationContext.Provider value={{ notifications, openNotification, closeNotification }}>
{children}
{notifications.map((notification) => (
<Snackbar
key={notification.id}
autoHideDuration={notification.duration}
open={true}
>
<Alert
icon={false}
variant='filled'
sx={{
backgroundColor: notification.color || theme.palette.primary.main,
}}
>
{notification.message}
</Alert>
</Snackbar>
))}
</NotificationContext.Provider>
);
};
export const useNotification = () => {
const context = useContext(NotificationContext);
if (context === undefined) {
throw new Error("useNotification must be used within a NotificationProvider");
}
return context;
};

View file

@ -4,7 +4,10 @@ import { atom } from "jotai";
// store which page the user is currently on
// no actual routing is done here,
// full page components are used render the different pages
export const routeAtom = atom("index");
// TODO: incomplete, to be updated as more pages are added
// TODO: also remove testing when done
export type Routes = "home" | "sql_injection" | "xss" | "cmd_injection" | "testing";
export const routeAtom = atom<Routes>("home");
// store the status of connection to backend
type ServerConnection = "connected" | "connecting" | "disconnected";

View file

@ -0,0 +1,3 @@
export const sleep = async (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};

View file

@ -5,6 +5,7 @@ import Head from "next/head";
import { UserThemeProvider } from "../contexts/ThemeContext";
import createEmotionCache from "../lib/createEmotionCache";
import "../styles/global.css";
import { NotificationProvider } from "../contexts/NotificationContext";
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
@ -19,12 +20,17 @@ export default function MyApp(props: MyAppProps) {
<CacheProvider value={emotionCache}>
<Head>
<title>CSPJ Application</title>
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
name='viewport'
content='initial-scale=1, width=device-width'
/>
</Head>
<UserThemeProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...pageProps} />
<NotificationProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...pageProps} />
</NotificationProvider>
</UserThemeProvider>
</CacheProvider>
);

View file

@ -1,5 +1,59 @@
import { Layout } from "../components/Home/Layout";
import { Box, Container, useTheme } from "@mui/material";
import { useAtom } from "jotai";
import { HeaderBar } from "../components/HeaderBar/HeaderBar";
import { Home } from "../components/Pages/Home/Home";
import { SqlInjection } from "../components/Pages/SqlInjection/SqlInjection";
import { Xss } from "../components/Pages/Xss/Xss";
import { routeAtom } from "../lib/jotai";
import { AttackPage } from "../components/Pages/Shared/AttackPage";
export default function Home() {
return <Layout />;
export default function Index() {
// contexts
const theme = useTheme();
// atoms
const [route, setRoute] = useAtom(routeAtom);
// render different components based on the route value
const renderComponent = () => {
switch (route) {
case "home":
return <Home />;
case "sql_injection":
return <SqlInjection />;
case "xss":
return <Xss />;
// TODO: remove testing when done
case "testing":
return <AttackPage />;
default:
return <Home />;
}
};
return (
<Box
sx={{
backgroundColor: theme.palette.background.default,
display: "flex",
flexDirection: "column",
height: "100vh",
}}
>
<HeaderBar />
<Container
maxWidth='lg'
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
p: 1,
}}
>
{/* main content goes here buddy */}
{renderComponent()}
</Container>
</Box>
);
}