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
2
Makefile
2
Makefile
|
|
@ -2,4 +2,4 @@ build:
|
|||
docker build -t foxgirlriley/jellyglass:latest .
|
||||
|
||||
run: build
|
||||
docker run -p 5000:5000 foxgirlriley/jellyglass:latest
|
||||
docker run --rm -p 5000:5000 foxgirlriley/jellyglass:latest
|
||||
|
|
@ -32,6 +32,28 @@ public class AuthController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetLoginFromSession()
|
||||
{
|
||||
if (!await IsAuthenticated())
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var sessionToken = GetSessionToken();
|
||||
|
||||
try
|
||||
{
|
||||
var login = await _service.GetLoginFromSession(sessionToken!);
|
||||
|
||||
return Ok(login);
|
||||
}
|
||||
catch (LoginFailedException)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("all")]
|
||||
public async Task<IActionResult> GetLogins()
|
||||
{
|
||||
if (!await IsAdminAuthenticated())
|
||||
|
|
@ -72,7 +94,7 @@ public class AuthController : ControllerBase
|
|||
return Forbid();
|
||||
}
|
||||
|
||||
var newLogin = await _service.CreateLogin(login.Username, login.Password);
|
||||
var newLogin = await _service.CreateLogin(login.Username, login.Password, login.IsAdmin);
|
||||
|
||||
return Ok(newLogin);
|
||||
}
|
||||
|
|
@ -80,9 +102,9 @@ public class AuthController : ControllerBase
|
|||
[HttpPut]
|
||||
public async Task<IActionResult> UpdateLogin([FromBody] UpdateLoginDTO login)
|
||||
{
|
||||
if (!await IsAdminAuthenticated())
|
||||
if (!await IsAuthenticated())
|
||||
{
|
||||
return Forbid();
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ public class ServersController : ControllerBase
|
|||
[HttpPost]
|
||||
public async Task<IActionResult> AddServer([FromBody] AddServerDTO server)
|
||||
{
|
||||
_logger.LogInformation("Started adding server");
|
||||
if (!await IsAdminAuthenticated())
|
||||
{
|
||||
return Forbid();
|
||||
|
|
|
|||
|
|
@ -19,4 +19,11 @@ public class AuthAlreadyExistsException : AuthRepositoryException
|
|||
public AuthAlreadyExistsException() { }
|
||||
public AuthAlreadyExistsException(string message) : base(message) { }
|
||||
public AuthAlreadyExistsException(string message, System.Exception inner) : base(message, inner) { }
|
||||
}
|
||||
|
||||
public class UserNotDeletableException : AuthRepositoryException
|
||||
{
|
||||
public UserNotDeletableException() { }
|
||||
public UserNotDeletableException(string message) : base(message) { }
|
||||
public UserNotDeletableException(string message, System.Exception inner) : base(message, inner) { }
|
||||
}
|
||||
|
|
@ -4,4 +4,5 @@ public class CreateLoginDTO
|
|||
{
|
||||
public required string Username { get; set; }
|
||||
public required string Password { get; set; }
|
||||
public bool IsAdmin { get; set; } = false;
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@ public interface ISessionRepository
|
|||
public Task<UserSession?> GetUserSession(string sessionToken);
|
||||
public Task RefreshSessionExpiry(string sessionToken);
|
||||
public Task DeleteSession(string sessionToken);
|
||||
public Task DeleteUserSessions(string username);
|
||||
}
|
||||
|
|
@ -78,8 +78,23 @@ public class LoginRepository : ILoginRepository
|
|||
|
||||
public async Task<UserLogin> DeleteLogin(string username)
|
||||
{
|
||||
if ((await _context.Logins.CountAsync()) == 1) //if there is only one user registered, make sure you can't delete it
|
||||
{
|
||||
throw new UserNotDeletableException("There is only one user registered");
|
||||
}
|
||||
|
||||
var login = await GetUserLogin(username);
|
||||
|
||||
if (login.IsAdmin) //if they're trying to delete the only admin
|
||||
{
|
||||
var admins = _context.Logins.Where(u => u.IsAdmin);
|
||||
|
||||
if ((await admins.CountAsync()) == 1)
|
||||
{
|
||||
throw new UserNotDeletableException("You can't delete the only admin user");
|
||||
}
|
||||
}
|
||||
|
||||
_context.Logins.Remove(login);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@ public class SessionRepository : ISessionRepository
|
|||
}
|
||||
}
|
||||
|
||||
public async Task DeleteUserSessions(string username)
|
||||
{
|
||||
var sessions = await _context.Sessions.Include(s => s.Login).Where(s => s.Login.Username == username).ToArrayAsync();
|
||||
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
_context.Sessions.Remove(session);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static DateTime GenerateExpiryDate()
|
||||
{
|
||||
return DateTime.Now + TimeSpan.FromDays(30); //TODO: Make this configurable
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ public class AuthService : IAuthService
|
|||
{
|
||||
private ILoginRepository _loginRepo;
|
||||
private ISessionRepository _sessionRepo;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
|
||||
public AuthService(ILoginRepository loginRepo, ISessionRepository sessionRepo)
|
||||
public AuthService(ILoginRepository loginRepo, ISessionRepository sessionRepo, ILogger<AuthService> logger)
|
||||
{
|
||||
_loginRepo = loginRepo;
|
||||
_sessionRepo = sessionRepo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<UserSessionDTO> AuthenticateUser(string username, string password)
|
||||
|
|
@ -69,11 +71,13 @@ public class AuthService : IAuthService
|
|||
{
|
||||
var session = await _sessionRepo.GetUserSession(sessionToken);
|
||||
|
||||
|
||||
if (session == null)
|
||||
{
|
||||
throw new SessionNotFoundException();
|
||||
}
|
||||
|
||||
|
||||
return session.Login.IsAdmin;
|
||||
}
|
||||
|
||||
|
|
@ -98,11 +102,23 @@ public class AuthService : IAuthService
|
|||
return new UserLoginDTO(login);
|
||||
}
|
||||
|
||||
public async Task<UserLoginDTO> CreateLogin(string username, string password)
|
||||
public async Task<UserLoginDTO> GetLoginFromSession(string sessionToken)
|
||||
{
|
||||
var session = await _sessionRepo.GetUserSession(sessionToken);
|
||||
|
||||
if (session == null)
|
||||
{
|
||||
throw new LoginFailedException();
|
||||
}
|
||||
|
||||
return new UserLoginDTO(session.Login);
|
||||
}
|
||||
|
||||
public async Task<UserLoginDTO> CreateLogin(string username, string password, bool isAdmin)
|
||||
{
|
||||
var hashedPassword = BCrypt.Net.BCrypt.HashPassword(password);
|
||||
|
||||
var newLogin = await _loginRepo.CreateLogin(username, hashedPassword, false);
|
||||
var newLogin = await _loginRepo.CreateLogin(username, hashedPassword, isAdmin);
|
||||
|
||||
return new UserLoginDTO(newLogin);
|
||||
}
|
||||
|
|
@ -137,6 +153,8 @@ public class AuthService : IAuthService
|
|||
{
|
||||
var deletedLogin = await _loginRepo.DeleteLogin(username);
|
||||
|
||||
await _sessionRepo.DeleteUserSessions(username);
|
||||
|
||||
return new UserLoginDTO(deletedLogin);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@ public interface IAuthService
|
|||
public Task<bool> IsAdmin(string sessionToken);
|
||||
public Task<UserLoginDTO[]> GetLogins();
|
||||
public Task<UserLoginDTO> GetLogin(string username);
|
||||
public Task<UserLoginDTO> CreateLogin(string username, string password);
|
||||
public Task<UserLoginDTO> GetLoginFromSession(string sessionToken);
|
||||
public Task<UserLoginDTO> CreateLogin(string username, string password, bool isAdmin);
|
||||
public Task UpdateLoginOwnPassword(string sessionToken, string newPassword, string oldPassword);
|
||||
public Task UpdateLoginPassword(string username, string newPassword);
|
||||
public Task<UserLoginDTO> DeleteLogin(string username);
|
||||
|
|
|
|||
25
frontend/package-lock.json
generated
25
frontend/package-lock.json
generated
|
|
@ -10,13 +10,15 @@
|
|||
"dependencies": {
|
||||
"axios": "^1.13.5",
|
||||
"bootstrap": "^5.3.8",
|
||||
"immer": "^11.1.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"react": "^19.2.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"sass": "^1.97.3",
|
||||
"scss": "^0.2.4"
|
||||
"scss": "^0.2.4",
|
||||
"use-immer": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
|
@ -3222,6 +3224,17 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "11.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
|
||||
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
|
|
@ -4188,6 +4201,16 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-immer": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.11.0.tgz",
|
||||
"integrity": "sha512-RNAqi3GqsWJ4bcCd4LMBgdzvPmTABam24DUaFiKfX9s3MSorNRz9RDZYJkllJoMHUxVLMDetwAuCDeyWNrp1yA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"immer": ">=8.0.0",
|
||||
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,13 +12,15 @@
|
|||
"dependencies": {
|
||||
"axios": "^1.13.5",
|
||||
"bootstrap": "^5.3.8",
|
||||
"immer": "^11.1.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"react": "^19.2.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"sass": "^1.97.3",
|
||||
"scss": "^0.2.4"
|
||||
"scss": "^0.2.4",
|
||||
"use-immer": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
|
|
|||
|
|
@ -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