Skip to content

Commit

Permalink
Merge pull request #98 from gnmyt/features/multiple-users
Browse files Browse the repository at this point in the history
👥 Support for multiple users
  • Loading branch information
gnmyt authored Sep 14, 2024
2 parents 793a2a9 + b40ae4a commit 26f848b
Show file tree
Hide file tree
Showing 33 changed files with 811 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import "./styles.sass";
import { DialogProvider } from "@/common/components/Dialog";
import Button from "@/common/components/Button/index.js";

export const ActionConfirmDialog = ({open, setOpen, onConfirm, onCancel, text}) => {

const cancel = () => {
setOpen(false);

if (onCancel) {
onCancel();
}
}

const confirm = () => {
setOpen(false);

if (onConfirm) {
onConfirm();
}
}

return (
<DialogProvider onClose={() => setOpen(false)} open={open}>
<div className="confirm-dialog">
<h2>Are you sure?</h2>
<p>{text ? text : "This action cannot be undone."}</p>
<div className="btn-area">
<Button onClick={cancel} type="secondary" text="Cancel" />
<Button onClick={confirm} type="primary" text="Confirm" />
</div>
</div>
</DialogProvider>
)
}
Empty file.
19 changes: 19 additions & 0 deletions client/src/common/components/ActionConfirmDialog/styles.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.confirm-dialog
display: flex
flex-direction: column
gap: 1rem
width: 20rem

h2
margin: 0

p
margin: 0
font-size: 1.1rem
font-weight: 600


.btn-area
display: flex
gap: 1rem
justify-content: flex-end
4 changes: 2 additions & 2 deletions client/src/common/components/Button/Button.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import "./styles.sass";
import Icon from "@mdi/react";

export const Button = ({onClick, text, icon, disabled}) => {
export const Button = ({onClick, text, icon, disabled, type}) => {
return (
<button className="btn" onClick={onClick} disabled={disabled}>
<button className={"btn" + (type ? " type-" + type : "")} onClick={onClick} disabled={disabled}>
{icon ? <Icon path={icon} /> : null}
<h3>{text}</h3>
</button>
Expand Down
5 changes: 4 additions & 1 deletion client/src/common/components/Button/styles.sass
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@

&:disabled
background-color: $gray
cursor: not-allowed
cursor: not-allowed

.type-secondary
background-color: $gray
41 changes: 28 additions & 13 deletions client/src/common/components/Sidebar/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import "./styles.sass";
import NextermLogo from "@/common/img/logo.png";
import { mdiCog, mdiPackageVariant, mdiServerOutline } from "@mdi/js";
import { mdiCog, mdiLogout, mdiPackageVariant, mdiServerOutline } from "@mdi/js";
import Icon from "@mdi/react";
import { Link, useLocation } from "react-router-dom";
import { useContext, useState } from "react";
import { UserContext } from "@/common/contexts/UserContext.jsx";
import { ActionConfirmDialog } from "@/common/components/ActionConfirmDialog/ActionConfirmDialog.jsx";

export const Sidebar = () => {

const location = useLocation();

const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);

const {logout, user} = useContext(UserContext);

const navigation = [
{ title: "Settings", path: "/settings", icon: mdiCog },
{ title: "Servers", path: "/servers", icon: mdiServerOutline },
Expand All @@ -20,17 +26,26 @@ export const Sidebar = () => {

return (
<div className="sidebar">
<img src={NextermLogo} alt="Nexterm Logo" />
<hr />

<nav>
{navigation.map((item, index) => (
<Link key={index} className={"nav-item" + (isActive(item.path) ? " nav-item-active " : "")}
to={item.path}>
<Icon path={item.icon} />
</Link>
))}
</nav>
<ActionConfirmDialog open={logoutDialogOpen} setOpen={setLogoutDialogOpen}
text={`This will log you out of the ${user?.username} account. Are you sure?`}
onConfirm={logout} />
<div className="sidebar-top">
<img src={NextermLogo} alt="Nexterm Logo" />
<hr />

<nav>
{navigation.map((item, index) => (
<Link key={index} className={"nav-item" + (isActive(item.path) ? " nav-item-active " : "")}
to={item.path}>
<Icon path={item.icon} />
</Link>
))}
</nav>
</div>

<div className="log-out-btn" onClick={() => setLogoutDialogOpen(true)}>
<Icon path={mdiLogout} />
</div>
</div>
);
};
31 changes: 29 additions & 2 deletions client/src/common/components/Sidebar/styles.sass
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@
height: 100%
display: flex
flex-direction: column
justify-content: space-between
align-items: center
border-right: 2px solid $dark-gray
overflow: hidden

.sidebar-top
display: flex
flex-direction: column
align-items: center

img
margin-top: 0.5rem
width: 3.5rem
height: 3.5rem

hr
background-color: $dark-gray
width: 40%
width: 50%
margin: 1rem 0
border: none
height: 2px
Expand All @@ -44,7 +50,28 @@
width: 2.5rem
height: 2.5rem

&:hover
color: $primary

.nav-item-active
background-color: $dark-gray
border: 1px solid $gray
color: $primary
color: $primary


.log-out-btn
border-top: 2px solid $dark-gray
width: 100%
height: 3rem
display: flex
justify-content: center
align-items: center
padding: 0.5rem
cursor: pointer

svg
width: 2rem
height: 2rem

&:hover
color: $error
32 changes: 25 additions & 7 deletions client/src/common/contexts/UserContext.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { createContext, useEffect, useState } from "react";
import LoginDialog from "@/common/components/LoginDialog";
import { getRequest } from "@/common/utils/RequestUtil.js";
import { getRequest, postRequest } from "@/common/utils/RequestUtil.js";

export const UserContext = createContext({});

export const UserProvider = ({ children }) => {

const [sessionToken, setSessionToken] = useState(localStorage.getItem("sessionToken"));
const [sessionToken, setSessionToken] = useState(localStorage.getItem("overrideToken")
|| localStorage.getItem("sessionToken"));
const [firstTimeSetup, setFirstTimeSetup] = useState(false);
const [user, setUser] = useState(null);

Expand All @@ -15,7 +16,7 @@ export const UserProvider = ({ children }) => {
localStorage.setItem("sessionToken", sessionToken);

login();
}
};

const checkFirstTimeSetup = async () => {
try {
Expand All @@ -24,7 +25,7 @@ export const UserProvider = ({ children }) => {
} catch (error) {
console.error(error);
}
}
};

const login = async () => {
try {
Expand All @@ -36,16 +37,33 @@ export const UserProvider = ({ children }) => {
localStorage.removeItem("sessionToken");
}
}
}
};

const logout = async () => {
await postRequest("auth/logout", { token: sessionToken });

if (localStorage.getItem("overrideToken")) {
localStorage.removeItem("overrideToken");
setSessionToken(localStorage.getItem("sessionToken"));
}

login();
};

const overrideToken = (token) => {
localStorage.setItem("overrideToken", token);
setSessionToken(token);
login();
};

useEffect(() => {
sessionToken ? login() : checkFirstTimeSetup();
}, []);

return (
<UserContext.Provider value={{updateSessionToken, user, sessionToken, firstTimeSetup, login}}>
<UserContext.Provider value={{ updateSessionToken, user, sessionToken, firstTimeSetup, login, logout, overrideToken }}>
<LoginDialog open={!sessionToken} />
{children}
</UserContext.Provider>
);
}
};
14 changes: 9 additions & 5 deletions client/src/common/utils/RequestUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,30 @@ export const request = async (url, method, body, headers) => {
return data;
}

const getToken = () => {
return localStorage.getItem("overrideToken") || localStorage.getItem("sessionToken");
}

export const sessionRequest = (url, method, token, body) => {
return request(url, method, body, {"Authorization": `Bearer ${token}`});
}

export const getRequest = (url) => {
return sessionRequest(url, "GET", localStorage.getItem("sessionToken"));
return sessionRequest(url, "GET", getToken());
}

export const postRequest = (url, body) => {
return sessionRequest(url, "POST", localStorage.getItem("sessionToken"), body);
return sessionRequest(url, "POST", getToken(), body);
}

export const putRequest = (url, body) => {
return sessionRequest(url, "PUT", localStorage.getItem("sessionToken"), body);
return sessionRequest(url, "PUT", getToken(), body);
}

export const deleteRequest = (url) => {
return sessionRequest(url, "DELETE", localStorage.getItem("sessionToken"));
return sessionRequest(url, "DELETE", getToken());
}

export const patchRequest = (url, body) => {
return sessionRequest(url, "PATCH", localStorage.getItem("sessionToken"), body);
return sessionRequest(url, "PATCH", getToken(), body);
}
13 changes: 9 additions & 4 deletions client/src/pages/Settings/Settings.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import "./styles.sass";
import Icon from "@mdi/react";
import { mdiAccountCircleOutline, mdiClockStarFourPointsOutline } from "@mdi/js";
import { mdiAccountCircleOutline, mdiAccountGroup, mdiClockStarFourPointsOutline } from "@mdi/js";
import SettingsNavigation from "./components/SettingsNavigation";
import { Navigate, useLocation } from "react-router-dom";
import Account from "@/pages/Settings/pages/Account";
import Sessions from "@/pages/Settings/pages/Sessions";
import Users from "@/pages/Settings/pages/Users";

export const Settings = () => {
const location = useLocation();

const pages = [
const userPages = [
{ title: "Account", icon: mdiAccountCircleOutline, content: <Account /> },
{ title: "Sessions", icon: mdiClockStarFourPointsOutline, content: <Sessions /> }
];

const currentPage = pages.find(page => location.pathname.endsWith(page.title.toLowerCase()));
const adminPages = [
{ title: "Users", icon: mdiAccountGroup, content: <Users /> }
];

const currentPage = [...userPages, ...adminPages].find(page => location.pathname.endsWith(page.title.toLowerCase()));

if (!currentPage) return <Navigate to="/settings/account" />;

return (
<div className="settings-page">
<SettingsNavigation pages={pages} />
<SettingsNavigation userPages={userPages} adminPages={adminPages} />
<div className="settings-content">
<div className="settings-header">
<Icon path={currentPage.icon} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import Icon from "@mdi/react";
import "./styles.sass";
import { useLocation, useNavigate } from "react-router-dom";
import SettingsItem from "./components/SettingsItem";
import { useContext } from "react";
import { UserContext } from "@/common/contexts/UserContext.jsx";

export const SettingsNavigation = ({pages}) => {
export const SettingsNavigation = ({ userPages, adminPages }) => {

const location = useLocation();
const navigate = useNavigate();

const endsWith = (path) => {
return location.pathname.endsWith(path);
}
const { user } = useContext(UserContext);

return (
<div className="settings-navigation">
{pages.map((page, index) => (
<div key={index} className={"settings-item" + (endsWith(page.title.toLowerCase()) ? " settings-item-active" : "")}
onClick={() => navigate("/settings/" + page.title.toLowerCase())}>
<Icon path={page.icon} />
<h2>{page.title}</h2>
</div>
))}
<p>USER SETTINGS</p>

<div className="settings-group">
{userPages.map((page, index) => (
<SettingsItem key={index} icon={page.icon} title={page.title} />
))}
</div>

{user?.role === "admin" && <p>ADMIN SETTINGS</p>}
{user?.role === "admin" && <div className="settings-group">
{adminPages.map((page, index) => (
<SettingsItem key={index} icon={page.icon} title={page.title} />
))}
</div>}
</div>
);
};
Loading

0 comments on commit 26f848b

Please sign in to comment.