Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ‘₯ Support for multiple users #98

Merged
merged 29 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
712f062
Create users.js validation
gnmyt Sep 14, 2024
c23b278
Update account.js route
gnmyt Sep 14, 2024
6c6895d
Create users.js routes
gnmyt Sep 14, 2024
31a0d84
Create permission.js middleware
gnmyt Sep 14, 2024
a8880aa
Add role to Account.js model
gnmyt Sep 14, 2024
bedaa8c
Update account.js controller
gnmyt Sep 14, 2024
aff622c
Add session.js#createSession
gnmyt Sep 14, 2024
aaf308f
Create /api/users router
gnmyt Sep 14, 2024
ed888be
Create Users page
gnmyt Sep 14, 2024
1298c83
Create SettingsItem component
gnmyt Sep 14, 2024
e9e2540
Integrate admin settings into the SettingsNavigation component
gnmyt Sep 14, 2024
d908c30
Create adminPages in Settings.jsx page
gnmyt Sep 14, 2024
0648ec0
Create ActionConfirmDialog component
gnmyt Sep 14, 2024
ea2d3ea
Add secondary button style
gnmyt Sep 14, 2024
5a657ea
Update PasswordChange dialog
gnmyt Sep 14, 2024
b371254
Create ContextMenu component
gnmyt Sep 14, 2024
a027d9e
Implement Users page
gnmyt Sep 14, 2024
9d8e073
Add User styles.sass
gnmyt Sep 14, 2024
8c3e132
Add CreateUserDialog component
gnmyt Sep 14, 2024
d46a088
Integrate CreateUserDialog into Users page
gnmyt Sep 14, 2024
1938b5c
Fix bug in Sessions.jsx
gnmyt Sep 14, 2024
acfa779
Update RequestUtil.js
gnmyt Sep 14, 2024
353b676
Integrate overrideToken into UserContext.jsx
gnmyt Sep 14, 2024
b2c4d15
Implement logout button into Sidebar component
gnmyt Sep 14, 2024
68a5153
Integrate "login as user" in ContextMenu component
gnmyt Sep 14, 2024
d6d8f8e
Fix bug in users.js route
gnmyt Sep 14, 2024
e31f2e1
Add QOL change to PasswordChange.jsx
gnmyt Sep 14, 2024
2619736
Update ContextMenu.jsx
gnmyt Sep 14, 2024
b40ae4a
Remove security check from Users.jsx
gnmyt Sep 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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