refactor: squash commits

This commit is contained in:
dec0dOS 2021-03-21 22:25:13 +03:00
parent 63ebcb5915
commit 1e6e237aa3
107 changed files with 20077 additions and 0 deletions

28
frontend/src/App.jsx Normal file
View file

@ -0,0 +1,28 @@
import "@fontsource/roboto";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import Theme from "./components/Theme";
import Bar from "./components/Bar";
import Home from "./routes/Home";
import NotFound from "./routes/NotFound";
import Network from "./routes/Network/Network";
function App() {
return (
<Theme>
<BrowserRouter basename="/app">
<Bar />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/network/:nwid" component={Network} />
<Route path="/404" component={NotFound} />
<Redirect to="/404" />
</Switch>
</BrowserRouter>
</Theme>
);
}
export default App;

View file

@ -0,0 +1,144 @@
import logo from "./assets/logo.png";
import { useState } from "react";
import { Link as RouterLink, useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import {
AppBar,
Toolbar,
Typography,
Box,
Button,
Divider,
Menu,
MenuItem,
Link,
} from "@material-ui/core";
import MenuIcon from "@material-ui/icons/Menu";
import LogIn from "components/LogIn";
function Bar() {
const [loggedIn, setLoggedIn] = useLocalStorage("loggedIn", false);
const [anchorEl, setAnchorEl] = useState(null);
const history = useHistory();
const openMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const closeMenu = () => {
setAnchorEl(null);
};
const onLogOutClick = () => {
setLoggedIn(false);
localStorage.clear();
history.push("/");
history.go(0);
};
const menuItems = [
// TODO: add settings page
// {
// name: "Settings",
// to: "/settings",
// },
{
name: "Log out",
divide: true,
onClick: onLogOutClick,
},
];
return (
<AppBar
color="secondary"
style={{ background: "#000000" }}
position="static"
>
<Toolbar>
<Box display="flex" flexGrow={1}>
<Typography color="inherit" variant="h6">
<Link
color="inherit"
component={RouterLink}
to="/"
underline="none"
>
<img src={logo} width="100" height="100" alt="logo" />
</Link>
</Typography>
</Box>
{loggedIn && (
<>
<Button color="inherit" onClick={openMenu}>
<MenuIcon></MenuIcon>
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeMenu}
>
{menuItems.map((menuItem, index) => {
if (
menuItem.hasOwnProperty("condition") &&
!menuItem.condition
) {
return null;
}
let component = null;
if (menuItem.to) {
component = (
<MenuItem
key={index}
component={RouterLink}
to={menuItem.to}
onClick={closeMenu}
>
{menuItem.name}
</MenuItem>
);
} else {
component = (
<MenuItem
key={index}
onClick={() => {
closeMenu();
menuItem.onClick();
}}
>
{menuItem.name}
</MenuItem>
);
}
if (menuItem.divide) {
return (
<span key={index}>
<Divider />
{component}
</span>
);
}
return component;
})}
</Menu>
</>
)}
{!loggedIn && LogIn()}
</Toolbar>
</AppBar>
);
}
export default Bar;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1 @@
export { default } from "./Bar";

View file

@ -0,0 +1,71 @@
import { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { Divider, Button, Grid, Typography, Box } from "@material-ui/core";
import useStyles from "./HomeLoggedIn.styles";
import NetworkButton from "./components/NetworkButton";
import API from "utils/API";
import { generateNetworkConfig } from "utils/NetworkConfig";
function HomeLoggedIn() {
const [networks, setNetworks] = useState([]);
const classes = useStyles();
const history = useHistory();
const createNetwork = async () => {
const network = await API.post("network", generateNetworkConfig());
console.log(network);
history.push("/network/" + network.data["config"]["id"]);
};
useEffect(() => {
async function fetchData() {
const networks = await API.get("network");
setNetworks(networks.data);
console.log("Networks:", networks.data);
}
fetchData();
}, []);
return (
<div className={classes.root}>
<Button
variant="contained"
color="primary"
className={classes.createBtn}
onClick={createNetwork}
>
Create A Network
</Button>
<Divider />
<Grid container spacing={3} className={classes.container}>
<Grid item xs={6}>
<Typography variant="h5">Controller networks</Typography>
{networks[0] && "Network controller address"}
<Box fontWeight="fontWeightBold">
{networks[0] && networks[0]["id"].slice(0, 10)}
</Box>
</Grid>
<Grid item xs="auto">
<Typography>Networks</Typography>
<Grid item>
{networks[0] ? (
networks.map((network) => (
<Grid key={network["id"]} item>
<NetworkButton network={network} />
</Grid>
))
) : (
<div>Please create at least one network</div>
)}
</Grid>
</Grid>
</Grid>
</div>
);
}
export default HomeLoggedIn;

View file

@ -0,0 +1,16 @@
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
root: {
margin: "5%",
flexGrow: 1,
},
createBtn: {
marginBottom: "1%",
},
container: {
marginTop: "1%",
},
}));
export default useStyles;

View file

@ -0,0 +1,20 @@
.netBtn {
font-size: 1em;
padding: 0 10px;
min-height: 50px;
max-height: 50px;
border-radius: 2px;
border: 1px solid #b5b5b5;
margin: 2px;
}
.netBtn:hover {
transform: translateY(0) scale(1.02);
background: rgba(0,0,0,0);
box-shadow: inset 0 0 0 3px #ffc107;
}
.netBtn:focus {
border: 1px solid white;
outline: 0;
}

View file

@ -0,0 +1,35 @@
import "./NetworkButton.css";
import { Link } from "react-router-dom";
import { List, ListItem, Hidden } from "@material-ui/core";
import useStyles from "./NetworkButton.styles";
import { getCIDRAddress } from "utils/IP";
function NetworkButton({ network }) {
const classes = useStyles();
return (
<div className="netBtn" role="button">
<Link to={"/network/" + network["id"]} className={classes.link}>
<List className={classes.flexContainer}>
<ListItem className={classes.nwid}>{network["id"]}</ListItem>
<ListItem className={classes.name}>
{network["config"]["name"]}
</ListItem>
<Hidden mdDown>
<ListItem className={classes.cidr}>
{network["config"]["ipAssignmentPools"] &&
getCIDRAddress(
network["config"]["ipAssignmentPools"][0]["ipRangeStart"],
network["config"]["ipAssignmentPools"][0]["ipRangeEnd"]
)}
</ListItem>
</Hidden>
</List>
</Link>
</div>
);
}
export default NetworkButton;

View file

@ -0,0 +1,27 @@
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
link: {
textDecoration: "none",
color: "black",
},
flexContainer: {
display: "flex",
flexDirection: "row",
paddingTop: "8px",
},
name: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
},
nwid: {
color: "#007fff",
fontWeight: "bolder",
},
cidr: {
color: "#b5b5b5",
},
}));
export default useStyles;

View file

@ -0,0 +1 @@
export { default } from "./NetworkButton";

View file

@ -0,0 +1 @@
export { default } from "./HomeLoggedIn";

View file

@ -0,0 +1,31 @@
import { Grid, Typography } from "@material-ui/core";
function HomeLoggedOut() {
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Grid item xs={10}>
<Typography variant="h5">
<span>
ZeroUI - ZeroTier Controller Web UI - is a web user interface for a
self-hosted ZeroTier network controller.
</span>
</Typography>
<Typography>
<span>Please Log In to continue</span>
</Typography>
</Grid>
</Grid>
);
}
export default HomeLoggedOut;

View file

@ -0,0 +1 @@
export { default } from "./HomeLoggedOut";

View file

@ -0,0 +1,20 @@
import { Divider } from "@material-ui/core";
import LogInUser from "./components/LogInUser";
import LogInToken from "./components/LogInToken";
function LogIn() {
return (
<>
{process.env.NODE_ENV === "development" && (
<>
<LogInToken />
<Divider orientation="vertical" />
</>
)}
<LogInUser />
</>
);
}
export default LogIn;

View file

@ -0,0 +1,90 @@
import { useState } from "react";
import { useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import {
TextField,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@material-ui/core";
function LogInToken() {
const [open, setOpen] = useState(false);
const [errorText, setErrorText] = useState("");
const [, setLoggedIn] = useLocalStorage("loggedIn", false);
const [token, setToken] = useLocalStorage("token", null);
const history = useHistory();
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleKeyPress = (event) => {
const key = event.key;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
if (key === "Enter") {
LogIn();
}
};
const LogIn = () => {
if (token.length !== 32) {
setErrorText("Token length error");
return;
}
setLoggedIn(true);
setToken(token);
handleClose();
history.go(0);
};
return (
<div>
<Button onClick={handleClickOpen} color="inherit" variant="outlined">
Token Log In
</Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle>
<DialogContent>
<DialogContentText>ADVANCED FEATURE.</DialogContentText>
<TextField
value={token}
onChange={(e) => {
setToken(e.target.value);
}}
error={!!errorText}
helperText={errorText}
margin="dense"
label="token"
type="text"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={LogIn} color="primary">
Log In
</Button>
</DialogActions>
</Dialog>
</div>
);
}
export default LogInToken;

View file

@ -0,0 +1 @@
export { default } from "./LogInToken";

View file

@ -0,0 +1,123 @@
import { useState } from "react";
import { useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import {
TextField,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Snackbar,
} from "@material-ui/core";
import axios from "axios";
function LogInUser() {
const [open, setOpen] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [, setLoggedIn] = useLocalStorage("loggedIn", false);
const [, setToken] = useLocalStorage("token", null);
const history = useHistory();
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
setSnackbarOpen(false);
};
const handleKeyPress = (event) => {
const key = event.key;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
if (key === "Enter") {
LogIn();
}
};
const LogIn = () => {
if (!username || !password) {
return;
}
axios
.post("/auth/login", {
username: username,
password: password,
})
.then(function (response) {
setLoggedIn(true);
setToken(response.data.token);
handleClose();
history.go(0);
})
.catch(function (error) {
setPassword("");
setSnackbarOpen(true);
console.log(error);
});
};
return (
<>
<Button onClick={handleClickOpen} color="primary" variant="contained">
Log In
</Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle>
<DialogContent>
<TextField
autoFocus
value={username}
onChange={(e) => {
setUsername(e.target.value);
}}
margin="dense"
label="username"
type="username"
fullWidth
/>
<TextField
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
margin="dense"
label="password"
type="password"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={LogIn} color="primary">
Log In
</Button>
</DialogActions>
</Dialog>
<Snackbar
open={snackbarOpen}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
message="Invalid username or password"
/>
</>
);
}
export default LogInUser;

View file

@ -0,0 +1 @@
export { default } from "./LogInUser";

View file

@ -0,0 +1 @@
export { default } from "./LogIn";

View file

@ -0,0 +1,17 @@
import { Grid, Typography } from "@material-ui/core";
function NetworkHeader({ network }) {
return (
<Grid item>
<Typography variant="h5">
<span>{network["config"]["id"]}</span>
</Typography>
<Typography variant="h6" style={{ fontStyle: "italic" }}>
<span>{network["config"] && network["config"]["name"]}</span>
</Typography>
<span>{network["config"] && network["description"]}</span>
</Grid>
);
}
export default NetworkHeader;

View file

@ -0,0 +1 @@
export { default } from "./NetworkHeader";

View file

@ -0,0 +1,80 @@
import { useState } from "react";
import { useParams, useHistory } from "react-router-dom";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Button,
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
DialogActions,
Typography,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import DeleteIcon from "@material-ui/icons/Delete";
import API from "utils/API";
function NetworkManagment() {
const { nwid } = useParams();
const history = useHistory();
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const sendDelReq = async () => {
const req = await API.delete("/network/" + nwid);
console.log("Action:", req);
};
const deleteNetwork = async () => {
await sendDelReq();
history.push("/");
history.go(0);
};
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Managment</Typography>
</AccordionSummary>
<AccordionDetails>
<Button
variant="contained"
color="secondary"
startIcon={<DeleteIcon />}
onClick={handleClickOpen}
>
Delete Network
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>
{"Are you sure you want to delete this network?"}
</DialogTitle>
<DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={deleteNetwork} color="secondary">
Delete
</Button>
</DialogActions>
</Dialog>
</AccordionDetails>
</Accordion>
);
}
export default NetworkManagment;

View file

@ -0,0 +1 @@
export { default } from "./NetworkManagment";

View file

@ -0,0 +1,185 @@
import { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox,
Grid,
Typography,
IconButton,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import RefreshIcon from "@material-ui/icons/Refresh";
import DataTable from "react-data-table-component";
import MemberName from "./components/MemberName";
import ManagedIP from "./components/ManagedIP";
import DeleteMember from "./components/DeleteMember";
import MemberSettings from "./components/MemberSettings";
import AddMember from "./components/AddMember";
import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
function NetworkMembers() {
const { nwid } = useParams();
const [members, setMembers] = useState([]);
const fetchData = useCallback(async () => {
try {
const members = await API.get("network/" + nwid + "/member");
setMembers(members.data);
console.log("Members:", members.data);
} catch (err) {
console.error(err);
}
}, [nwid]);
useEffect(() => {
fetchData();
const timer = setInterval(() => fetchData(), 30000);
return () => clearInterval(timer);
}, [nwid, fetchData]);
const sendReq = async (mid, data) => {
const req = await API.post("/network/" + nwid + "/member/" + mid, data);
console.log("Action:", req);
};
const handleChange = (
member,
key1,
key2 = null,
mode = "text",
id = null
) => (event) => {
const value = parseValue(event, mode, member, key1, key2, id);
const updatedMember = replaceValue({ ...member }, key1, key2, value);
const index = members.findIndex((item) => {
return item["config"]["id"] === member["config"]["id"];
});
let mutableMembers = [...members];
mutableMembers[index] = updatedMember;
setMembers(mutableMembers);
const data = setValue({}, key1, key2, value);
sendReq(member["config"]["id"], data);
};
const columns = [
{
id: "auth",
name: "Authorized",
minWidth: "80px",
cell: (row) => (
<Checkbox
checked={row.config.authorized}
color="primary"
onChange={handleChange(row, "config", "authorized", "checkbox")}
/>
),
},
{
id: "address",
name: "Address",
minWidth: "150px",
cell: (row) => (
<Typography variant="body2">{row.config.address}</Typography>
),
},
{
id: "name",
name: "Name/Description",
minWidth: "250px",
cell: (row) => <MemberName member={row} handleChange={handleChange} />,
},
{
id: "ips",
name: "Managed IPs",
minWidth: "220px",
cell: (row) => <ManagedIP member={row} handleChange={handleChange} />,
},
{
***REMOVED***
id: "status",
name: "Peer status",
minWidth: "100px",
cell: (row) =>
row.online ? (
<Typography style={{ color: "#008000" }}>
{"ONLINE (v" +
row.config.vMajor +
"." +
row.config.vMinor +
"." +
row.config.vRev +
")"}
</Typography>
) : (
<Typography color="error">OFFLINE</Typography>
),
},
{
id: "delete",
name: "",
minWidth: "50px",
right: true,
cell: (row) => (
<>
<MemberSettings member={row} handleChange={handleChange} />
<DeleteMember nwid={nwid} mid={row.config.id} callback={fetchData} />
</>
),
},
];
return (
<Accordion defaultExpanded={true}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Members</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column" spacing={3}>
<IconButton color="primary" onClick={fetchData}>
<RefreshIcon />
</IconButton>
<Grid container>
{members.length ? (
<DataTable
noHeader={true}
columns={columns}
data={[...members]}
/>
) : (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Typography variant="h6" style={{ padding: "10%" }}>
No devices have joined this network. Use the app on your
devices to join <b>{nwid}</b>.
</Typography>
</Grid>
)}
</Grid>
<Grid item>
<AddMember nwid={nwid} callback={fetchData} />
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
);
}
export default NetworkMembers;

View file

@ -0,0 +1,55 @@
import { useState } from "react";
import { List, Typography, IconButton, TextField } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import API from "utils/API";
function AddMember({ nwid, callback }) {
const [member, setMember] = useState("");
const handleInput = (event) => {
setMember(event.target.value);
};
const addMemberReq = async () => {
if (member.length === 10) {
const req = await API.post("/network/" + nwid + "/member/" + member, {
config: { authorized: true },
hidden: false,
});
console.log("Action:", req);
callback();
}
setMember("");
};
return (
<>
<Typography>Manually Add Member</Typography>
<List
disablePadding={true}
style={{
display: "flex",
flexDirection: "row",
}}
>
<TextField
value={member}
onChange={handleInput}
placeholder={"##########"}
/>
<IconButton size="small" color="primary" onClick={addMemberReq}>
<AddIcon
style={{
fontSize: 16,
}}
/>
</IconButton>
</List>
</>
);
}
export default AddMember;

View file

@ -0,0 +1 @@
export { default } from "./AddMember";

View file

@ -0,0 +1,59 @@
import { useState } from "react";
import {
Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
IconButton,
} from "@material-ui/core";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import API from "utils/API";
function DeleteMember({ nwid, mid, callback }) {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const deleteMemberReq = async () => {
const req = await API.delete("/network/" + nwid + "/member/" + mid);
console.log("Action:", req);
setOpen(false);
callback();
};
return (
<>
<IconButton color="primary" onClick={handleClickOpen}>
<DeleteOutlineIcon color="secondary" style={{ fontSize: 20 }} />
</IconButton>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>
{"Are you sure you want to delete this member?"}
</DialogTitle>
<DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={deleteMemberReq} color="secondary">
Delete
</Button>
</DialogActions>
</Dialog>
</>
);
}
export default DeleteMember;

View file

@ -0,0 +1 @@
export { default } from "./DeleteMember";

View file

@ -0,0 +1,76 @@
import { useState } from "react";
import { Grid, List, TextField, IconButton } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import { validateIP, normilizeIP } from "utils/IP";
function ManagedIP({ member, handleChange }) {
const [ipInput, setIpInput] = useState();
const [normolizedInput, setNormolizedInput] = useState();
const handleInput = (event) => {
const ip = event.target.value;
setIpInput(ip);
if (validateIP(ip)) {
setNormolizedInput(normilizeIP(ip));
}
};
return (
<Grid>
{member.config.ipAssignments.map((value, i) => (
<List
key={i + "_ips"}
disablePadding={true}
style={{ display: "flex", flexDirection: "row" }}
>
<IconButton
size="small"
color="secondary"
onClick={handleChange(
member,
"config",
"ipAssignments",
"arrayDel",
i
)}
>
<DeleteOutlineIcon style={{ fontSize: 14 }} />
</IconButton>
{value}
</List>
))}
<List
disablePadding={true}
style={{
display: "flex",
flexDirection: "row",
}}
>
<IconButton
size="small"
color="primary"
onClick={handleChange(
member,
"config",
"ipAssignments",
"arrayAdd",
normolizedInput
)}
>
<AddIcon
style={{
fontSize: 14,
}}
/>
</IconButton>
<TextField value={ipInput} onChange={handleInput} />
</List>
</Grid>
);
}
export default ManagedIP;

View file

@ -0,0 +1 @@
export { default } from "./ManagedIP";

View file

@ -0,0 +1,28 @@
import { Grid, TextField } from "@material-ui/core";
function MemberName({ member, handleChange }) {
return (
<Grid>
<TextField
value={member.name}
onChange={handleChange(member, "name")}
label="Name"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
<TextField
value={member.description}
onChange={handleChange(member, "description")}
label="Description"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
);
}
export default MemberName;

View file

@ -0,0 +1 @@
export { default } from "./MemberName";

View file

@ -0,0 +1,64 @@
import { useState } from "react";
import {
Checkbox,
Dialog,
DialogTitle,
DialogContent,
Grid,
IconButton,
} from "@material-ui/core";
import BuildIcon from "@material-ui/icons/Build";
function MemberSettings({ member, handleChange }) {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<>
<IconButton color="primary" onClick={handleClickOpen}>
<BuildIcon style={{ fontSize: 20 }} />
</IconButton>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{"Member " + member.config.id + " settings"}</DialogTitle>
<DialogContent>
<Grid item>
<Checkbox
checked={member["config"]["activeBridge"]}
color="primary"
onChange={handleChange(
member,
"config",
"activeBridge",
"checkbox"
)}
/>
<span>Allow Ethernet Bridging</span>
</Grid>
<Grid item>
<Checkbox
checked={member["config"]["noAutoAssignIps"]}
color="primary"
onChange={handleChange(
member,
"config",
"noAutoAssignIps",
"checkbox"
)}
/>
<span>Do Not Auto-Assign IPs</span>
</Grid>
</DialogContent>
</Dialog>
</>
);
}
export default MemberSettings;

View file

@ -0,0 +1 @@
export { default } from "./MemberSettings";

View file

@ -0,0 +1 @@
export { default } from "./NetworkMembers";

View file

@ -0,0 +1,132 @@
import { useState } from "react";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Button,
Divider,
Snackbar,
Hidden,
Grid,
Typography,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import CodeMirror from "@uiw/react-codemirror";
import "codemirror/theme/3024-day.css";
import { compile } from "external/RuleCompiler";
import debounce from "lodash/debounce";
import API from "utils/API";
function NetworkRules({ network }) {
const [editor, setEditor] = useState(null);
const [flowData, setFlowData] = useState({
rules: [...network.config.rules],
capabilities: [...network.config.capabilities],
tags: [...network.config.tags],
});
const [errors, setErrors] = useState([]);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const saveChanges = async () => {
if (editor) {
const req = await API.post("/network/" + network["config"]["id"], {
config: { ...flowData },
rulesSource: editor.getValue(),
});
console.log("Action", req);
setSnackbarOpen(true);
const timer = setTimeout(() => {
setSnackbarOpen(false);
}, 1500);
return () => clearTimeout(timer);
}
};
const onChange = debounce((event) => {
const src = event.getValue();
setEditor(event);
let rules = [],
caps = [],
tags = [];
const res = compile(src, rules, caps, tags);
if (!res) {
setFlowData({
rules: [...rules],
capabilities: [...caps],
tags: [...tags],
});
setErrors([]);
} else {
setErrors(res);
}
}, 100);
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Flow Rules</Typography>
</AccordionSummary>
<AccordionDetails>
{/* Important note: value in CodeMirror instance means INITAIL VALUE
or it could be used to replace editor state with the new value.
No need to update on every user character input
*/}
<CodeMirror
value={network["rulesSource"]}
onChange={onChange}
options={{ tabSize: 2, lineWrapping: true }}
/>
<Hidden mdDown>
<div>
<CodeMirror
value={JSON.stringify(flowData, null, 2)}
width="100%"
height="50%"
options={{
theme: "3024-day",
readOnly: true,
lineNumbers: false,
lineWrapping: true,
}}
/>
</div>
</Hidden>
<Divider />
<Grid
item
style={{
margin: "1%",
display: "block",
overflowWrap: "break-word",
width: "250px",
}}
>
{!!errors.length ? (
<Typography color="error">
{"[" + errors[0] + ":" + errors[1] + "] " + errors[2]}
</Typography>
) : (
<Button variant="contained" color="primary" onClick={saveChanges}>
Save Changes
</Button>
)}
</Grid>
<Snackbar
open={snackbarOpen}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
message="Saved"
/>
</AccordionDetails>
</Accordion>
);
}
export default NetworkRules;

View file

@ -0,0 +1 @@
export { default } from "./NetworkRules";

View file

@ -0,0 +1,141 @@
import {
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox,
Divider,
Grid,
Typography,
TextField,
Select,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ManagedRoutes from "./components/ManagedRoutes";
import IPv4AutoAssign from "./components/IPv4AutoAssign";
import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
function NetworkSettings({ network, setNetwork }) {
const sendReq = async (data) => {
try {
const req = await API.post("/network/" + network["config"]["id"], data);
console.log("Action", req);
} catch (err) {
console.error(err);
}
};
const handleChange = (key1, key2, mode = "text", additionalData = null) => (
event
) => {
const value = parseValue(event, mode, additionalData);
let updatedNetwork = replaceValue({ ...network }, key1, key2, value);
setNetwork(updatedNetwork);
let data = setValue({}, key1, key2, value);
sendReq(data);
};
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>General settings</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column" spacing={3}>
<Grid item>
<Typography>Network ID</Typography>
<Typography variant="h5">
<span>{network["config"]["id"]}</span>
</Typography>
</Grid>
<Grid item>
<TextField
value={network["config"]["name"]}
onChange={handleChange("config", "name")}
label="Name"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item>
<TextField
value={network["description"]}
onChange={handleChange("description")}
multiline
rows={2}
rowsMax={Infinity}
label="Description"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Divider />
<Grid item>
<Typography>Access Control</Typography>
<Select
native
value={network["config"]["private"]}
onChange={handleChange("config", "private", "json")}
>
<option value={true}>Private</option>
<option value={false}>Public</option>
</Select>
</Grid>
<Divider />
<Grid item>
<ManagedRoutes
routes={network["config"]["routes"]}
handleChange={handleChange}
/>
</Grid>
<Divider />
<Grid item>
<IPv4AutoAssign
ipAssignmentPools={network["config"]["ipAssignmentPools"]}
handleChange={handleChange}
/>
</Grid>
{/* TODO: */}
{/* <Grid item>
<Typography>IPv6 Auto-Assign</Typography>
</Grid> */}
<Divider />
<Grid item>
<TextField
label="Multicast Recipient Limit"
type="number"
value={network["config"]["multicastLimit"]}
onChange={handleChange("config", "multicastLimit", "json")}
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item>
<Checkbox
checked={network["config"]["enableBroadcast"]}
color="primary"
onChange={handleChange("config", "enableBroadcast", "checkbox")}
/>
<span>Enable Broadcast</span>
</Grid>
{/* TODO: */}
{/* <Grid item>
<Typography>DNS</Typography>
</Grid> */}
</Grid>
</AccordionDetails>
</Accordion>
);
}
export default NetworkSettings;

View file

@ -0,0 +1,177 @@
import { useState } from "react";
import {
Button,
Box,
Divider,
Grid,
List,
Typography,
TextField,
IconButton,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import DataTable from "react-data-table-component";
import { addressPool } from "utils/NetworkConfig";
import { getCIDRAddress, validateIP, normilizeIP } from "utils/IP";
function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
const handleStartInput = (event) => {
setStart(event.target.value);
};
const handleEndInput = (event) => {
setEnd(event.target.value);
};
const setDefaultPool = (index) => {
addPoolReq(addressPool[index]["start"], addressPool[index]["end"], true);
handleChange("config", "routes", "custom", [
{
target: getCIDRAddress(
addressPool[index]["start"],
addressPool[index]["end"]
),
},
])(null);
};
const addPoolReq = (localStart, localEnd, reset = false) => {
let data = {};
console.log(localStart, localEnd);
if (validateIP(localStart) && validateIP(localEnd)) {
data["ipRangeStart"] = normilizeIP(localStart);
data["ipRangeEnd"] = normilizeIP(localEnd);
} else {
return;
}
let newPool = [];
if (ipAssignmentPools && !reset) {
newPool = [...ipAssignmentPools];
}
newPool.push(data);
console.log(newPool);
handleChange("config", "ipAssignmentPools", "custom", newPool)(null);
setStart("");
setEnd("");
};
const removePoolReq = (index) => {
let newPool = [...ipAssignmentPools];
newPool.splice(index, 1);
handleChange("config", "ipAssignmentPools", "custom", newPool)(null);
};
const columns = [
{
id: "remove",
width: "10px",
cell: (_, index) => (
<IconButton
size="small"
color="secondary"
onClick={() => removePoolReq(index)}
>
<DeleteOutlineIcon style={{ fontSize: 14 }} />
</IconButton>
),
},
{
id: "Start",
name: "Start",
cell: (row) => row["ipRangeStart"],
},
{
id: "End",
name: "End",
cell: (row) => row["ipRangeEnd"],
},
];
return (
<>
<Typography>IPv4 Auto-Assign</Typography>
<div
style={{
padding: "30px",
}}
>
<Grid container spacing={1}>
{addressPool.map((item, index) => (
<Grid item xs={3} key={item["name"]}>
<Button
variant="contained"
fullWidth={true}
onClick={() => setDefaultPool(index)}
>
{item["name"]}
</Button>
</Grid>
))}
</Grid>
</div>
<Typography style={{ paddingBottom: "10px" }}>
Auto-Assign Pools
</Typography>
<Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}>
<DataTable
noHeader={true}
columns={columns}
data={ipAssignmentPools}
/>
<Divider />
<Typography>Add IPv4 Pool</Typography>
<List
style={{
display: "flex",
flexDirection: "row",
}}
>
<TextField
value={start}
onChange={handleStartInput}
placeholder={"Start"}
/>
<Divider
orientation="vertical"
style={{
margin: "10px",
}}
flexItem
/>
<TextField
value={end}
onChange={handleEndInput}
placeholder={"End"}
/>
<IconButton
size="small"
color="primary"
onClick={() => addPoolReq(start, end)}
>
<AddIcon
style={{
fontSize: 16,
}}
/>
</IconButton>
</List>
</Grid>
</Box>
</>
);
}
export default IPv4AutoAssign;

View file

@ -0,0 +1 @@
export { default } from "./IPv4AutoAssign";

View file

@ -0,0 +1,131 @@
import { useState } from "react";
import {
Box,
Divider,
Grid,
List,
Typography,
TextField,
IconButton,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import DataTable from "react-data-table-component";
import { validateIP, normilizeIP, validateCIDR } from "utils/IP";
function ManagedRoutes({ routes, handleChange }) {
const [destination, setDestination] = useState("");
const [via, setVia] = useState("");
const handleDestinationInput = (event) => {
setDestination(event.target.value);
};
const handleViaInput = (event) => {
setVia(event.target.value);
};
const addRouteReq = () => {
let data = {};
if (validateCIDR(destination)) {
data["target"] = destination;
} else {
return;
}
if (via && validateIP(via)) {
data["via"] = normilizeIP(via);
}
let newRoutes = [...routes];
newRoutes.push(data);
handleChange("config", "routes", "custom", newRoutes)(null);
setDestination("");
setVia("");
};
const removeRouteReq = (index) => {
let newRoutes = [...routes];
newRoutes.splice(index, 1);
handleChange("config", "routes", "custom", newRoutes)(null);
};
const columns = [
{
id: "remove",
width: "10px",
cell: (_, index) => (
<IconButton
size="small"
color="secondary"
onClick={() => removeRouteReq(index)}
>
<DeleteOutlineIcon style={{ fontSize: 14 }} />
</IconButton>
),
},
{
id: "target",
name: "Target",
cell: (row) => row["target"],
},
{
id: "via",
name: "via",
cell: (row) => (row["via"] ? row["via"] : "(LAN)"),
},
];
return (
<>
<Typography style={{ paddingBottom: "10px" }}>
Managed Routes ({routes.length + "/32"})
</Typography>
<Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}>
<DataTable noHeader={true} columns={columns} data={routes} />
<Divider />
<Typography>Add Routes</Typography>
<List
style={{
display: "flex",
flexDirection: "row",
}}
>
<TextField
value={destination}
onChange={handleDestinationInput}
placeholder={"Destination (CIDR)"}
/>
<Divider
orientation="vertical"
style={{
margin: "10px",
}}
flexItem
/>
<TextField
value={via}
onChange={handleViaInput}
placeholder={"Via (Optional)"}
/>
<IconButton size="small" color="primary" onClick={addRouteReq}>
<AddIcon
style={{
fontSize: 16,
}}
/>
</IconButton>
</List>
</Grid>
</Box>
</>
);
}
export default ManagedRoutes;

View file

@ -0,0 +1 @@
export { default } from "./ManagedRoutes";

View file

@ -0,0 +1 @@
export { default } from "./NetworkSettings";

View file

@ -0,0 +1,21 @@
import { ThemeProvider } from "@material-ui/styles";
import { createMuiTheme } from "@material-ui/core/styles";
import { red, amber } from "@material-ui/core/colors";
const theme = createMuiTheme({
palette: {
primary: {
main: amber[500],
},
secondary: {
main: red[500],
},
type: "light",
},
});
function Theme({ children }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
export default Theme;

View file

@ -0,0 +1 @@
export { default } from "./Theme";

1147
frontend/src/external/RuleCompiler.js vendored Normal file

File diff suppressed because it is too large Load diff

9
frontend/src/index.css Normal file
View file

@ -0,0 +1,9 @@
body {
margin: 0;
overflow-x: hidden;
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

13
frontend/src/index.jsx Normal file
View file

@ -0,0 +1,13 @@
import "./index.css";
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);

View file

@ -0,0 +1,16 @@
import { useLocalStorage } from "react-use";
import HomeLoggedIn from "components/HomeLoggedIn";
import HomeLoggedOut from "components/HomeLoggedOut";
function Home() {
const [loggedIn] = useLocalStorage("loggedIn", false);
if (loggedIn) {
return <HomeLoggedIn />;
} else {
return <HomeLoggedOut />;
}
}
export default Home;

View file

@ -0,0 +1 @@
export { default } from "./Home";

View file

@ -0,0 +1,83 @@
import { useState, useEffect } from "react";
import { Link as RouterLink, useParams, useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import { Link, Grid, Typography } from "@material-ui/core";
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
import useStyles from "./Network.styles";
import NetworkHeader from "components/NetworkHeader";
import NetworkSettings from "components/NetworkSettings";
import NetworkMembers from "components/NetworkMembers";
import NetworkRules from "components/NetworkRules";
import NetworkManagment from "components/NetworkManagment";
import API from "utils/API";
function Network() {
const { nwid } = useParams();
const [loggedIn] = useLocalStorage("loggedIn", false);
const [network, setNetwork] = useState({});
const classes = useStyles();
const history = useHistory();
useEffect(() => {
async function fetchData() {
try {
const network = await API.get("network/" + nwid);
setNetwork(network.data);
console.log("Current network:", network.data);
} catch (err) {
if (err.response.status === 404) {
history.push("/404");
}
console.error(err);
}
}
fetchData();
}, [nwid, history]);
if (loggedIn) {
return (
<>
<Link color="inherit" component={RouterLink} to="/" underline="none">
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
Networks
</Link>
<div className={classes.container}>
{network["config"] && (
<>
<NetworkHeader network={network} />
<NetworkSettings network={network} setNetwork={setNetwork} />
</>
)}
<NetworkMembers />
{network["config"] && <NetworkRules network={network} />}
<NetworkManagment />
</div>
</>
);
} else {
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Grid item xs={10}>
<Typography variant="h5">
You are not authorized. Please Log In
</Typography>
</Grid>
</Grid>
);
}
}
export default Network;

View file

@ -0,0 +1,12 @@
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
backIcon: {
fontSize: 12,
},
container: {
margin: "1%",
},
}));
export default useStyles;

View file

@ -0,0 +1 @@
export { default } from "./Network";

View file

@ -0,0 +1,28 @@
import { Grid, Typography } from "@material-ui/core";
function NotFound() {
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Grid item xs={10}>
<Typography variant="h1">
<span>404</span>
</Typography>
<Typography variant="h4">
<span>Not found</span>
</Typography>
</Grid>
</Grid>
);
}
export default NotFound;

View file

@ -0,0 +1 @@
export { default } from "./NotFound";

11
frontend/src/utils/API.js Normal file
View file

@ -0,0 +1,11 @@
import axios from "axios";
const baseURL = "/api/";
export default axios.create({
baseURL: baseURL,
responseType: "json",
headers: {
Authorization: `Bearer ${JSON.parse(localStorage.getItem("token"))}`,
},
});

View file

@ -0,0 +1,52 @@
export function parseValue(
event,
mode = "text",
data = null,
key1 = null,
key2 = null,
id = null
) {
let value;
if (mode === "json") {
value = JSON.parse(event.target.value);
} else if (mode === "checkbox") {
value = event.target.checked;
} else if (mode === "arrayDel") {
value = data[key1][key2];
if (id) {
value.splice(id, 1);
}
} else if (mode === "arrayAdd") {
value = data[key1][key2];
if (id) {
value.push(id);
}
} else if (mode === "custom") {
value = data;
} else {
value = event.target.value;
}
return value;
}
export function replaceValue(data, key1, key2, value) {
if (key2) {
data[key1][key2] = value;
} else {
data[key1] = value;
}
return data;
}
export function setValue(data, key1, key2, value) {
if (key2) {
data = {
[key1]: { [key2]: value },
};
} else {
data = {
[key1]: value,
};
}
return data;
}

50
frontend/src/utils/IP.js Normal file
View file

@ -0,0 +1,50 @@
import ipaddr from "ipaddr.js";
export function getCIDRAddress(start, end) {
const cidr = getCIDR(start, end);
return start.replace(/.$/, 0) + "/" + cidr;
}
function getCIDR(start, end) {
const startInt = toInt(start);
const endInt = toInt(end);
const binaryXOR = startInt ^ endInt;
if (binaryXOR === 0) {
return 32;
} else {
const binaryStr = binaryXOR.toString(2);
const zeroCount = binaryStr.split("0").length - 1;
const oneCount = binaryStr.split("1").length - 1;
return 32 - (zeroCount + oneCount);
}
}
function toInt(addr) {
const ip = ipaddr.parse(addr);
const arr = ip.octets;
let ipInt = 0;
let counter = 3;
for (const i in arr) {
ipInt += arr[i] * Math.pow(256, counter);
counter--;
}
return ipInt;
}
export function validateIP(string) {
return ipaddr.IPv4.isValid(string) || ipaddr.IPv6.isValid(string);
}
export function normilizeIP(string) {
const addr = ipaddr.parse(string);
return addr.toNormalizedString();
}
export function validateCIDR(string) {
try {
ipaddr.parseCIDR(string);
return true;
} catch (err) {
return false;
}
}

View file

@ -0,0 +1,136 @@
export function generateNetworkConfig() {
const randSubnetPart = getRandomInt(0, 254).toString();
const randNamePart = new Date().getTime();
return {
config: {
name: "new-net-" + randNamePart.toString().substring(8),
private: true,
v6AssignMode: { rfc4193: false, "6plane": false, zt: false },
v4AssignMode: { zt: true },
routes: [
{
target: "172.30." + randSubnetPart + ".0/24",
via: null,
flags: 0,
metric: 0,
},
],
ipAssignmentPools: [
{
ipRangeStart: "172.30." + randSubnetPart + ".1",
ipRangeEnd: "172.30." + randSubnetPart + ".254",
},
],
enableBroadcast: true,
},
};
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
export const addressPool = [
{
name: "10.147.17.*",
start: "10.147.17.1",
end: "10.147.17.254",
},
{
name: "10.147.18.*",
start: "10.147.18.1",
end: "10.147.18.254",
},
{
name: "10.147.19.*",
start: "10.147.19.1",
end: "10.147.19.254",
},
{
name: "10.147.20.*",
start: "10.147.20.1",
end: "10.147.20.254",
},
{
name: "10.241.*.*",
start: "10.241.0.1",
end: "10.241.255.254",
},
{
name: "10.242.*.*",
start: "10.242.0.1",
end: "10.242.255.254",
},
{
name: "10.243.*.*",
start: "10.243.0.1",
end: "10.243.255.254",
},
{
name: "10.244.*.*",
start: "10.244.0.1",
end: "10.244.255.254",
},
{
name: "172.23.*.*",
start: "172.23.0.1",
end: "172.23.255.254",
},
{
name: "172.24.*.*",
start: "172.24.0.1",
end: "172.24.255.254",
},
{
name: "172.25.*.*",
start: "172.25.0.1",
end: "172.25.255.254",
},
{
name: "172.26.*.*",
start: "172.26.0.1",
end: "172.26.255.254",
},
{
name: "172.27.*.*",
start: "172.27.0.1",
end: "172.27.255.254",
},
{
name: "172.28.*.*",
start: "172.28.0.1",
end: "172.28.255.254",
},
{
name: "172.29.*.*",
start: "172.29.0.1",
end: "172.29.255.254",
},
{
name: "172.30.*.*",
start: "172.30.0.1",
end: "172.30.255.254",
},
{
name: "192.168.192.*",
start: "192.168.192.1",
end: "192.168.192.254",
},
{
name: "192.168.193.*",
start: "192.168.193.1",
end: "192.168.193.254",
},
{
name: "192.168.194.*",
start: "192.168.194.1",
end: "192.168.194.254",
},
{
name: "192.168.195.*",
start: "192.168.195.1",
end: "192.168.195.254",
},
];