idk a lot of changes for the admin stuff

This commit is contained in:
Fishandchips321 2026-03-08 19:30:00 +00:00
parent 2cbbc00489
commit 5251ca6f99
23 changed files with 576 additions and 15 deletions

View file

@ -2,4 +2,4 @@ build:
docker build -t foxgirlriley/jellyglass:latest . docker build -t foxgirlriley/jellyglass:latest .
run: build run: build
docker run -p 5000:5000 foxgirlriley/jellyglass:latest docker run --rm -p 5000:5000 foxgirlriley/jellyglass:latest

View file

@ -32,6 +32,28 @@ public class AuthController : ControllerBase
} }
[HttpGet] [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() public async Task<IActionResult> GetLogins()
{ {
if (!await IsAdminAuthenticated()) if (!await IsAdminAuthenticated())
@ -72,7 +94,7 @@ public class AuthController : ControllerBase
return Forbid(); 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); return Ok(newLogin);
} }
@ -80,9 +102,9 @@ public class AuthController : ControllerBase
[HttpPut] [HttpPut]
public async Task<IActionResult> UpdateLogin([FromBody] UpdateLoginDTO login) public async Task<IActionResult> UpdateLogin([FromBody] UpdateLoginDTO login)
{ {
if (!await IsAdminAuthenticated()) if (!await IsAuthenticated())
{ {
return Forbid(); return Unauthorized();
} }
try try

View file

@ -76,6 +76,7 @@ public class ServersController : ControllerBase
[HttpPost] [HttpPost]
public async Task<IActionResult> AddServer([FromBody] AddServerDTO server) public async Task<IActionResult> AddServer([FromBody] AddServerDTO server)
{ {
_logger.LogInformation("Started adding server");
if (!await IsAdminAuthenticated()) if (!await IsAdminAuthenticated())
{ {
return Forbid(); return Forbid();

View file

@ -19,4 +19,11 @@ public class AuthAlreadyExistsException : AuthRepositoryException
public AuthAlreadyExistsException() { } public AuthAlreadyExistsException() { }
public AuthAlreadyExistsException(string message) : base(message) { } public AuthAlreadyExistsException(string message) : base(message) { }
public AuthAlreadyExistsException(string message, System.Exception inner) : base(message, inner) { } 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) { }
} }

View file

@ -4,4 +4,5 @@ public class CreateLoginDTO
{ {
public required string Username { get; set; } public required string Username { get; set; }
public required string Password { get; set; } public required string Password { get; set; }
public bool IsAdmin { get; set; } = false;
} }

View file

@ -8,4 +8,5 @@ public interface ISessionRepository
public Task<UserSession?> GetUserSession(string sessionToken); public Task<UserSession?> GetUserSession(string sessionToken);
public Task RefreshSessionExpiry(string sessionToken); public Task RefreshSessionExpiry(string sessionToken);
public Task DeleteSession(string sessionToken); public Task DeleteSession(string sessionToken);
public Task DeleteUserSessions(string username);
} }

View file

@ -78,8 +78,23 @@ public class LoginRepository : ILoginRepository
public async Task<UserLogin> DeleteLogin(string username) 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); 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); _context.Logins.Remove(login);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();

View file

@ -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() private static DateTime GenerateExpiryDate()
{ {
return DateTime.Now + TimeSpan.FromDays(30); //TODO: Make this configurable return DateTime.Now + TimeSpan.FromDays(30); //TODO: Make this configurable

View file

@ -8,11 +8,13 @@ public class AuthService : IAuthService
{ {
private ILoginRepository _loginRepo; private ILoginRepository _loginRepo;
private ISessionRepository _sessionRepo; 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; _loginRepo = loginRepo;
_sessionRepo = sessionRepo; _sessionRepo = sessionRepo;
_logger = logger;
} }
public async Task<UserSessionDTO> AuthenticateUser(string username, string password) public async Task<UserSessionDTO> AuthenticateUser(string username, string password)
@ -69,11 +71,13 @@ public class AuthService : IAuthService
{ {
var session = await _sessionRepo.GetUserSession(sessionToken); var session = await _sessionRepo.GetUserSession(sessionToken);
if (session == null) if (session == null)
{ {
throw new SessionNotFoundException(); throw new SessionNotFoundException();
} }
return session.Login.IsAdmin; return session.Login.IsAdmin;
} }
@ -98,11 +102,23 @@ public class AuthService : IAuthService
return new UserLoginDTO(login); 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 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); return new UserLoginDTO(newLogin);
} }
@ -137,6 +153,8 @@ public class AuthService : IAuthService
{ {
var deletedLogin = await _loginRepo.DeleteLogin(username); var deletedLogin = await _loginRepo.DeleteLogin(username);
await _sessionRepo.DeleteUserSessions(username);
return new UserLoginDTO(deletedLogin); return new UserLoginDTO(deletedLogin);
} }
} }

View file

@ -9,7 +9,8 @@ public interface IAuthService
public Task<bool> IsAdmin(string sessionToken); public Task<bool> IsAdmin(string sessionToken);
public Task<UserLoginDTO[]> GetLogins(); public Task<UserLoginDTO[]> GetLogins();
public Task<UserLoginDTO> GetLogin(string username); 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 UpdateLoginOwnPassword(string sessionToken, string newPassword, string oldPassword);
public Task UpdateLoginPassword(string username, string newPassword); public Task UpdateLoginPassword(string username, string newPassword);
public Task<UserLoginDTO> DeleteLogin(string username); public Task<UserLoginDTO> DeleteLogin(string username);

View file

@ -10,13 +10,15 @@
"dependencies": { "dependencies": {
"axios": "^1.13.5", "axios": "^1.13.5",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"immer": "^11.1.4",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"react": "^19.2.0", "react": "^19.2.0",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0", "react-router-dom": "^7.13.0",
"sass": "^1.97.3", "sass": "^1.97.3",
"scss": "^0.2.4" "scss": "^0.2.4",
"use-immer": "^0.11.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@ -3222,6 +3224,17 @@
"node": ">= 4" "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": { "node_modules/immutable": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
@ -4188,6 +4201,16 @@
"punycode": "^2.1.0" "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": { "node_modules/vite": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",

View file

@ -12,13 +12,15 @@
"dependencies": { "dependencies": {
"axios": "^1.13.5", "axios": "^1.13.5",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"immer": "^11.1.4",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"react": "^19.2.0", "react": "^19.2.0",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0", "react-router-dom": "^7.13.0",
"sass": "^1.97.3", "sass": "^1.97.3",
"scss": "^0.2.4" "scss": "^0.2.4",
"use-immer": "^0.11.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",

View file

@ -1,15 +1,23 @@
import { Navbar as BsNavbar, Button, Container } from "react-bootstrap"; import { Navbar as BsNavbar, Button, Container } from "react-bootstrap";
// import styles from "./Navbar.module.scss"; // import styles from "./Navbar.module.scss";
import Searchbar from "../Searchbar/Searchbar"; import Searchbar from "../Searchbar/Searchbar";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { GetCurrentUser, type Login } from "../../Lib/Auth";
const Navbar = () => { const Navbar = () => {
const [searchText, setSearchText] = useState<string>(""); const [searchText, setSearchText] = useState<string>("");
const [currentUser, setCurrentUser] = useState<Login | undefined>(undefined);
const navigate = useNavigate(); const navigate = useNavigate();
const session = Cookies.get("session"); const session = Cookies.get("session");
useEffect(() => {
GetCurrentUser().then(user => {
setCurrentUser(user);
});
}, [session]);
function onLogout() { function onLogout() {
Cookies.remove("session"); Cookies.remove("session");
navigate("/login"); navigate("/login");
@ -20,8 +28,12 @@ const Navbar = () => {
setSearchText(""); setSearchText("");
} }
function onManageUser() {
navigate("/user");
}
return ( return (
<BsNavbar expand="lg" className={"bg-light "}> <BsNavbar expand="lg" className={"bg-light"} style={{ paddingRight: "10px" }}>
<Container> <Container>
<Link to={"/"} style={{ textDecoration: "none" }}> <Link to={"/"} style={{ textDecoration: "none" }}>
<BsNavbar.Brand>JellyGlass</BsNavbar.Brand> <BsNavbar.Brand>JellyGlass</BsNavbar.Brand>
@ -33,7 +45,9 @@ const Navbar = () => {
</Container> </Container>
</BsNavbar.Collapse> </BsNavbar.Collapse>
{session && <Button onClick={onLogout}>Logout</Button>} {session && <Button onClick={onLogout}>Logout</Button>}
{session && currentUser?.isAdmin && <Link to={"/admin"} style={{ paddingLeft: "10px" }}><Button>Admin</Button></Link>}
</Container> </Container>
<Button onClick={() => onManageUser()}>Manage User</Button>
</BsNavbar> </BsNavbar>
) )
} }

View file

@ -6,8 +6,51 @@ export interface Session {
expiresOn: string; expiresOn: string;
} }
export interface Login {
username: string;
isAdmin: string;
}
export const logIn = async (username: string, password: string) => { export const logIn = async (username: string, password: string) => {
const response = await axios.post<Session>(`${apiUrl}/auth/login`, `username=${encodeURI(username)}&password=${encodeURI(password)}`); const response = await axios.post<Session>(`${apiUrl}/auth/login`, `username=${encodeURI(username)}&password=${encodeURI(password)}`);
return response.data; 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 });
} }

View file

@ -11,10 +11,19 @@ export interface Server {
} }
export const getServerList = async (): Promise<Array<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 }); 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; return response.data;
} }

View 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;

View 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;
}

View 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;

View 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;

View file

@ -0,0 +1,5 @@
.changePasswordForm {
margin: auto;
margin-top: 50px;
max-width: 400px;
}

View 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;

View 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;

View file

@ -6,15 +6,21 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Navbar from './Components/Navbar/Navbar.tsx' import Navbar from './Components/Navbar/Navbar.tsx'
import Search from './Pages/Search/Search.tsx'; import Search from './Pages/Search/Search.tsx';
import Login from './Pages/Login/Login.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( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<Navbar /> <Navbar />
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route index={true} path="/" element={<Index />} />
<Route path="/search" element={<Search />} /> <Route path="/search" element={<Search />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/admin" element={<Admin />} />
<Route path="/user" element={<ManageUser />} />
<Route path="*" element={<NotFound />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</StrictMode>, </StrictMode>,