idk a lot of changes for the admin stuff
This commit is contained in:
parent
2cbbc00489
commit
5251ca6f99
23 changed files with 576 additions and 15 deletions
|
|
@ -1,15 +1,23 @@
|
|||
import { Navbar as BsNavbar, Button, Container } from "react-bootstrap";
|
||||
// import styles from "./Navbar.module.scss";
|
||||
import Searchbar from "../Searchbar/Searchbar";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Cookies from "js-cookie";
|
||||
import { GetCurrentUser, type Login } from "../../Lib/Auth";
|
||||
|
||||
const Navbar = () => {
|
||||
const [searchText, setSearchText] = useState<string>("");
|
||||
const [currentUser, setCurrentUser] = useState<Login | undefined>(undefined);
|
||||
const navigate = useNavigate();
|
||||
const session = Cookies.get("session");
|
||||
|
||||
useEffect(() => {
|
||||
GetCurrentUser().then(user => {
|
||||
setCurrentUser(user);
|
||||
});
|
||||
}, [session]);
|
||||
|
||||
function onLogout() {
|
||||
Cookies.remove("session");
|
||||
navigate("/login");
|
||||
|
|
@ -20,8 +28,12 @@ const Navbar = () => {
|
|||
setSearchText("");
|
||||
}
|
||||
|
||||
function onManageUser() {
|
||||
navigate("/user");
|
||||
}
|
||||
|
||||
return (
|
||||
<BsNavbar expand="lg" className={"bg-light "}>
|
||||
<BsNavbar expand="lg" className={"bg-light"} style={{ paddingRight: "10px" }}>
|
||||
<Container>
|
||||
<Link to={"/"} style={{ textDecoration: "none" }}>
|
||||
<BsNavbar.Brand>JellyGlass</BsNavbar.Brand>
|
||||
|
|
@ -33,7 +45,9 @@ const Navbar = () => {
|
|||
</Container>
|
||||
</BsNavbar.Collapse>
|
||||
{session && <Button onClick={onLogout}>Logout</Button>}
|
||||
{session && currentUser?.isAdmin && <Link to={"/admin"} style={{ paddingLeft: "10px" }}><Button>Admin</Button></Link>}
|
||||
</Container>
|
||||
<Button onClick={() => onManageUser()}>Manage User</Button>
|
||||
</BsNavbar>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,51 @@ export interface Session {
|
|||
expiresOn: string;
|
||||
}
|
||||
|
||||
export interface Login {
|
||||
username: string;
|
||||
isAdmin: string;
|
||||
}
|
||||
|
||||
export const logIn = async (username: string, password: string) => {
|
||||
const response = await axios.post<Session>(`${apiUrl}/auth/login`, `username=${encodeURI(username)}&password=${encodeURI(password)}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const GetLoginFromSessonCookie = async () => {
|
||||
const response = await axios.get<Login>(`${apiUrl}/auth`, { withCredentials: true });
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const IsCurrentUserAdmin = async () => {
|
||||
return (await GetLoginFromSessonCookie()).isAdmin;
|
||||
}
|
||||
|
||||
export const AddUser = async (username: string, password: string, isAdmin: boolean) => {
|
||||
const response = await axios.post<Login>(`${apiUrl}/auth`, { username: username, password: password, isAdmin: isAdmin }, { withCredentials: true });
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const DeleteUser = async (username: string) => {
|
||||
const response = await axios.delete<Login>(`${apiUrl}/auth`, { data: { username: username }, withCredentials: true });
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const GetUserList = async () => {
|
||||
const response = await axios.get<Array<Login>>(`${apiUrl}/auth/all`, { withCredentials: true });
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const GetCurrentUser = async () => {
|
||||
const resonse = await axios.get<Login>(`${apiUrl}/auth`, { withCredentials: true });
|
||||
|
||||
return resonse.data;
|
||||
}
|
||||
|
||||
export const ChangeCurrentUserPassword = async (oldPassword: string, newPassword: string) => {
|
||||
await axios.put(`${apiUrl}/auth`, { password: oldPassword, newPassword: newPassword }, { withCredentials: true });
|
||||
}
|
||||
|
|
@ -11,10 +11,19 @@ export interface Server {
|
|||
}
|
||||
|
||||
export const getServerList = async (): Promise<Array<Server>> => {
|
||||
console.log("fetching server list");
|
||||
const response = await axios.get<Array<Server>>(`${apiUrl}/servers/all`, { withCredentials: true });
|
||||
|
||||
console.log(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const RemoveServer = async (serverUrl: string) => {
|
||||
const response = await axios.delete<Server>(`${apiUrl}/servers`, { data: { Url: serverUrl }, withCredentials: true });
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const AddServer = async (owner: string, url: string, apiToken: string) => {
|
||||
const response = await axios.post<Server>(`${apiUrl}/servers`, { url: url, owner: owner, apiToken: apiToken }, { withCredentials: true });
|
||||
|
||||
return response.data;
|
||||
}
|
||||
31
frontend/src/Pages/Admin/Admin.tsx
Normal file
31
frontend/src/Pages/Admin/Admin.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { useEffect } from "react";
|
||||
import ServerManagement from "./ServerManagement/ServerManagement";
|
||||
import UserManagement from "./UserManagement/UserManagement";
|
||||
import { IsCurrentUserAdmin } from "../../Lib/Auth";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
||||
const Admin = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
IsCurrentUserAdmin().then(isAdmin => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}).catch(err => {
|
||||
navigate("/");
|
||||
alert(err);
|
||||
})
|
||||
}, [navigate])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ServerManagement />
|
||||
<UserManagement />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Admin;
|
||||
15
frontend/src/Pages/Admin/Management.module.scss
Normal file
15
frontend/src/Pages/Admin/Management.module.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.form {
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.formButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
max-width: 1800px;
|
||||
}
|
||||
114
frontend/src/Pages/Admin/ServerManagement/ServerManagement.tsx
Normal file
114
frontend/src/Pages/Admin/ServerManagement/ServerManagement.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { useEffect } from "react";
|
||||
import { Button, Form, Spinner, Table } from "react-bootstrap";
|
||||
import { AddServer, getServerList, RemoveServer, type Server } from "../../../Lib/Servers";
|
||||
import { useImmer } from "use-immer";
|
||||
import styles from "../Management.module.scss";
|
||||
|
||||
|
||||
const ServerManagement = () => {
|
||||
const [servers, setServers] = useImmer<Array<Server> | undefined>(undefined);
|
||||
|
||||
const [addServerInfo, setAddServerInfo] = useImmer({
|
||||
url: "",
|
||||
owner: "",
|
||||
apiToken: ""
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getServerList().then(serverList => {
|
||||
setServers(serverList);
|
||||
}).catch(err => {
|
||||
setServers([]);
|
||||
alert(err);
|
||||
})
|
||||
}, [setServers]);
|
||||
|
||||
const onServerRemove = (url: string) => {
|
||||
RemoveServer(url).then(server => {
|
||||
setServers(draft => {
|
||||
const index = draft?.findIndex(s => s.url == server.url);
|
||||
|
||||
if (index! > -1) {
|
||||
draft?.splice(index!, 1);
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
alert(err);
|
||||
})
|
||||
}
|
||||
|
||||
const onServerAdd = () => {
|
||||
AddServer(addServerInfo.owner, addServerInfo.url, addServerInfo.apiToken).then(result => {
|
||||
if (result.errored) {
|
||||
alert("Server was added, but is not working. Check the logs for details");
|
||||
}
|
||||
|
||||
setServers(draft => {
|
||||
if (!draft) {
|
||||
draft = [result];
|
||||
}
|
||||
else {
|
||||
draft.push(result);
|
||||
}
|
||||
|
||||
setAddServerInfo(draft => {
|
||||
draft.apiToken = "";
|
||||
draft.owner = "";
|
||||
draft.url = "";
|
||||
});
|
||||
})
|
||||
}).catch(err => {
|
||||
alert(err);
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form className={styles.form}>
|
||||
<h4>Add Server</h4>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Owner</Form.Label>
|
||||
<Form.Control type="text" placeholder="Owner" onChange={e => setAddServerInfo(draft => { draft.owner = e.target.value })} value={addServerInfo.owner} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Url</Form.Label>
|
||||
<Form.Control type="text" placeholder="Url" onChange={e => setAddServerInfo(draft => { draft.url = e.target.value })} value={addServerInfo.url} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>ApiToken</Form.Label>
|
||||
<Form.Control type="text" placeholder="ApiToken" onChange={e => setAddServerInfo(draft => { draft.apiToken = e.target.value })} value={addServerInfo.apiToken} />
|
||||
</Form.Group>
|
||||
<Button className={styles.formButton} onClick={() => onServerAdd()}>Add Server</Button>
|
||||
</Form>
|
||||
<Table className={styles.table} bordered striped>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server Owner</th>
|
||||
<th>Server URL</th>
|
||||
<th>Server Status</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
servers ?
|
||||
servers?.map(server => {
|
||||
return (
|
||||
<tr key={server.url}>
|
||||
<td>{server.owner}</td>
|
||||
<td>{server.url}</td>
|
||||
<td>{!server.errored ? "Online" : "Errored"}</td>
|
||||
<td><Button variant="danger" onClick={() => onServerRemove(server.url)}>Remove</Button></td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerManagement;
|
||||
136
frontend/src/Pages/Admin/UserManagement/UserManagement.tsx
Normal file
136
frontend/src/Pages/Admin/UserManagement/UserManagement.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { useImmer } from "use-immer";
|
||||
import { AddUser, DeleteUser, GetCurrentUser, GetUserList, type Login } from "../../../Lib/Auth";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Form, Spinner, Table } from "react-bootstrap";
|
||||
import styles from "../Management.module.scss";
|
||||
|
||||
|
||||
const UserManagement = () => {
|
||||
const [currentUser, setCurrentUser] = useState<Login | undefined>(undefined);
|
||||
const [users, setUsers] = useImmer<Array<Login> | undefined>(undefined);
|
||||
|
||||
const [addUserData, setAddUserData] = useImmer({
|
||||
username: "",
|
||||
password: "",
|
||||
isAdmin: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
GetUserList().then(data => {
|
||||
setUsers(data);
|
||||
}).catch(err => {
|
||||
alert(err);
|
||||
setUsers([]);
|
||||
})
|
||||
}, [setUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
GetCurrentUser().then(data => {
|
||||
setCurrentUser(data);
|
||||
}).catch(err => {
|
||||
alert(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onCreateUser = () => {
|
||||
AddUser(addUserData.username, addUserData.password, addUserData.isAdmin).then(user => {
|
||||
if (!users) {
|
||||
setUsers([user]);
|
||||
}
|
||||
else {
|
||||
setUsers(draft => { draft?.push(user) });
|
||||
}
|
||||
|
||||
setAddUserData(draft => {
|
||||
draft.username = "";
|
||||
draft.password = "";
|
||||
});
|
||||
}).catch(err => {
|
||||
alert(err);
|
||||
})
|
||||
}
|
||||
|
||||
const isDeleteDisabled = (user: Login) => {
|
||||
if (users?.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (user.username == currentUser?.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let adminCount = 0;
|
||||
|
||||
users?.forEach(u => {
|
||||
if (u.isAdmin) {
|
||||
adminCount += 1;
|
||||
}
|
||||
})
|
||||
|
||||
if (adminCount === 1 && user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const onDeleteUser = (user: Login) => {
|
||||
DeleteUser(user.username).then(user => {
|
||||
setUsers(draft => {
|
||||
const index = draft?.findIndex(u => u.username == user.username);
|
||||
|
||||
if (index! > -1) {
|
||||
draft?.splice(index!, 1);
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
alert(err);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form className={styles.form}>
|
||||
<h4>Add User</h4>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Username</Form.Label>
|
||||
<Form.Control type="text" placeholder="Username" onChange={e => setAddUserData(draft => { draft.username = e.target.value })} value={addUserData.username} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Password</Form.Label>
|
||||
<Form.Control type="password" placeholder="Password" onChange={e => setAddUserData(draft => { draft.password = e.target.value })} value={addUserData.password} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Check onChange={e => setAddUserData(draft => { draft.isAdmin = e.target.checked })} checked={addUserData.isAdmin} label="Is Admin" />
|
||||
</Form.Group>
|
||||
<Button className={styles.formButton} onClick={() => onCreateUser()}>Create user</Button>
|
||||
</Form>
|
||||
<Table className={styles.table} striped bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Is Admin</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users ?
|
||||
users.map(user => {
|
||||
return (
|
||||
<tr key={user.username}>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.isAdmin ? "Yes" : "No"}</td>
|
||||
<td><Button variant="danger" disabled={isDeleteDisabled(user)} onClick={() => onDeleteUser(user)}>Delete</Button></td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
</tbody>
|
||||
</Table >
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserManagement;
|
||||
5
frontend/src/Pages/ManageUser/ManageUser.module.scss
Normal file
5
frontend/src/Pages/ManageUser/ManageUser.module.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.changePasswordForm {
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
max-width: 400px;
|
||||
}
|
||||
68
frontend/src/Pages/ManageUser/ManageUser.tsx
Normal file
68
frontend/src/Pages/ManageUser/ManageUser.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { Button, Form } from "react-bootstrap";
|
||||
import styles from "./ManageUser.module.scss";
|
||||
import { useImmer } from "use-immer";
|
||||
import { ChangeCurrentUserPassword } from "../../Lib/Auth";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
||||
const ManageUser = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [passwordInputs, setPasswordInputs] = useImmer({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
repeatedNewPassword: ""
|
||||
});
|
||||
|
||||
const onChangePassword = () => {
|
||||
if (passwordInputs.newPassword === "" || passwordInputs.oldPassword === "" || passwordInputs.repeatedNewPassword === "") {
|
||||
alert("Please enter your old and new passwords");
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordInputs.newPassword !== passwordInputs.repeatedNewPassword) {
|
||||
alert("Passwords do not match!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
ChangeCurrentUserPassword(passwordInputs.oldPassword, passwordInputs.newPassword).then(() => {
|
||||
navigate("/");
|
||||
}).catch(err => {
|
||||
if (err.response) {
|
||||
if (err.response.status === 401) {
|
||||
alert("Password was incorrect");
|
||||
return;
|
||||
}
|
||||
}
|
||||
alert(err);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.changePasswordForm}>
|
||||
<Form>
|
||||
<h3>Change Password</h3>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Old Password</Form.Label>
|
||||
<Form.Control type="password" placeholder="Old Password"
|
||||
onChange={e => setPasswordInputs(draft => { draft.oldPassword = e.target.value })} value={passwordInputs.oldPassword} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>New Password</Form.Label>
|
||||
<Form.Control type="password" placeholder="New Password"
|
||||
onChange={e => setPasswordInputs(draft => { draft.newPassword = e.target.value })} value={passwordInputs.newPassword} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Repeat New Password</Form.Label>
|
||||
<Form.Control type="password" placeholder="Repeat New Password"
|
||||
onChange={e => setPasswordInputs(draft => { draft.repeatedNewPassword = e.target.value })} value={passwordInputs.repeatedNewPassword} />
|
||||
</Form.Group>
|
||||
<Button className="mt-4" onClick={() => onChangePassword()}>Change Password</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManageUser;
|
||||
17
frontend/src/Pages/NotFound/NotFound.tsx
Normal file
17
frontend/src/Pages/NotFound/NotFound.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
|
||||
const NotFound = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
navigate("/");
|
||||
})
|
||||
|
||||
return (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound;
|
||||
|
|
@ -6,15 +6,21 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
|||
import Navbar from './Components/Navbar/Navbar.tsx'
|
||||
import Search from './Pages/Search/Search.tsx';
|
||||
import Login from './Pages/Login/Login.tsx';
|
||||
import Admin from './Pages/Admin/Admin.tsx';
|
||||
import NotFound from './Pages/NotFound/NotFound.tsx';
|
||||
import ManageUser from './Pages/ManageUser/ManageUser.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route index={true} path="/" element={<Index />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/user" element={<ManageUser />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue