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 .
|
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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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) { }
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
25
frontend/package-lock.json
generated
25
frontend/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
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 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>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue