feat(auth): Added authentication

This commit is contained in:
Fishandchips321 2026-03-05 12:34:55 +00:00
parent d85d4334f8
commit 5e100c75ed
39 changed files with 704 additions and 86 deletions

View file

@ -1,12 +1,19 @@
import { Navbar as BsNavbar, Container } from "react-bootstrap";
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 { Link, useNavigate } from "react-router-dom";
import Cookies from "js-cookie";
const Navbar = () => {
const [searchText, setSearchText] = useState<string>("");
const navigate = useNavigate();
const session = Cookies.get("session");
function onLogout() {
Cookies.remove("session");
navigate("/login");
}
function onSearch() {
navigate(`/search?search=${searchText}`);
@ -22,9 +29,10 @@ const Navbar = () => {
<BsNavbar.Toggle />
<BsNavbar.Collapse>
<Container className="justify-content-center d-flex">
<Searchbar text={searchText} setText={setSearchText} onSearch={onSearch} />
<Searchbar text={searchText} setText={setSearchText} onSearch={onSearch} enabled={session !== undefined} />
</Container>
</BsNavbar.Collapse>
{session && <Button onClick={onLogout}>Logout</Button>}
</Container>
</BsNavbar>
)

View file

@ -2,4 +2,10 @@
border-radius: 10px;
padding: 5px;
width: 100%;
}
.searchbarDisabled {
@extend .searchbar;
background-color: lightgray;
cursor: not-allowed;
}

View file

@ -5,9 +5,10 @@ interface SearchbarProps {
text: string,
setText: (text: string) => void;
onSearch?: () => void;
enabled?: boolean;
}
const Searchbar = ({ text, setText, onSearch }: SearchbarProps) => {
const Searchbar = ({ text, setText, onSearch, enabled = true }: SearchbarProps) => {
function onKeyPressed(event: React.KeyboardEvent<HTMLInputElement>) {
if (onSearch === undefined) {
@ -20,7 +21,7 @@ const Searchbar = ({ text, setText, onSearch }: SearchbarProps) => {
}
return (
<input className={styles.searchbar} type="text" placeholder="Search" value={text} onChange={e => setText(e.target.value)} onKeyUp={onKeyPressed} />
<input className={enabled ? styles.searchbar : styles.searchbarDisabled} type="text" placeholder="Search" value={text} onChange={e => setText(e.target.value)} onKeyUp={onKeyPressed} disabled={!enabled} />
)
}

View file

@ -11,6 +11,9 @@ const ServerList = () => {
setServers(serverList);
}).catch(err => {
setServers([]);
if (err.response && err.response.status === 401) {
return;
}
alert(err);
})
}, []);

13
frontend/src/Lib/Auth.ts Normal file
View file

@ -0,0 +1,13 @@
import axios from "axios"
import { apiUrl } from "./api"
export interface Session {
sessionToken: string;
expiresOn: string;
}
export const logIn = async (username: string, password: string) => {
const response = await axios.post<Session>(`${apiUrl}/auth`, `username=${encodeURI(username)}&password=${encodeURI(password)}`);
return response.data;
}

View file

@ -11,7 +11,7 @@ export interface SearchResult {
}
export const search = async (searchTerm: string, serverId: string): Promise<Array<SearchResult>> => {
const response = await axios.get<Array<SearchResult>>(encodeURI(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`));
const response = await axios.get<Array<SearchResult>>(encodeURI(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`), { withCredentials: true });
return response.data;
}

View file

@ -11,7 +11,7 @@ export interface Server {
export const getServerList = async (): Promise<Array<Server>> => {
console.log("fetching server list");
const response = await axios.get<Array<Server>>(`${apiUrl}/servers`);
const response = await axios.get<Array<Server>>(`${apiUrl}/servers`, { withCredentials: true });
console.log(response);

View file

@ -0,0 +1,9 @@
.form {
margin: auto;
margin-top: 50px;
max-width: 400px;
}
.formButton {
margin-top: 10px;
}

View file

@ -0,0 +1,54 @@
import React, { useEffect, useState } from "react";
import { Button, Form, FormGroup } from "react-bootstrap";
import { logIn } from "../../Lib/Auth";
import Cookies from "js-cookie";
import { useNavigate } from "react-router-dom";
import styles from "./Login.module.scss";
const Login = () => {
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const navigate = useNavigate();
useEffect(() => {
if (Cookies.get("session") !== undefined) {
navigate("/");
}
}, [navigate]);
function onSubmit() {
logIn(username, password).then(sessionToken => {
Cookies.set("session", sessionToken.sessionToken, { expires: Date.parse(sessionToken.expiresOn) });
navigate("/");
}).catch(err => {
alert(err);
})
}
function onFormKeypress(e: React.KeyboardEvent<HTMLFormElement>) {
if (e.key === "Enter") {
onSubmit();
}
}
return (
<div className={styles.form}>
<Form onKeyUp={onFormKeypress}>
<h3>Login</h3>
<FormGroup>
<Form.Label>Username</Form.Label>
<Form.Control type="text" value={username} onChange={e => setUsername(e.target.value)} />
</FormGroup>
<FormGroup>
<Form.Label>Password</Form.Label>
<Form.Control type="password" value={password} onChange={e => setPassword(e.target.value)} />
</FormGroup>
<Button className={styles.formButton} variant="primary" onClick={onSubmit}>Login</Button>
</Form>
</div>
);
}
export default Login;

View file

@ -3,35 +3,51 @@ import { getServerList, type Server } from "../../Lib/Servers";
import ServerSearch from "../../Components/ServerSearch/ServerSearch";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Spinner } from "react-bootstrap";
import Cookies from "js-cookie";
const Search = () => {
const [searchParams] = useSearchParams();
const [servers, setServers] = useState<Array<Server>>([]);
const navigate = useNavigate();
const sessionCookie = Cookies.get("session");
const searchTerm = searchParams.get("search") || "";
useEffect(() => {
if (!sessionCookie) {
navigate("/login");
return;
}
if (searchTerm === "") {
alert(`Error search term missing: ${searchTerm}`);
navigate("/");
}
if (servers.length > 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setServers([]);
}
getServerList().then(servers => {
if (servers.length === 0) {
alert("No servers found");
}
setServers(servers);
const workingServers: Array<Server> = [];
servers.forEach(s => {
if (!s.errored) {
workingServers.push(s);
}
})
if (workingServers.length === 0) {
alert("No working servers");
navigate("/");
}
setServers(workingServers);
}).catch(e => {
alert(e);
});
}, [searchTerm]);
}, [searchTerm, navigate]);
return (
<>

View file

@ -1,6 +1,18 @@
import ServerList from "./Components/ServerList/ServerList"
import { useEffect } from "react";
import ServerList from "./Components/ServerList/ServerList";
import Cookies from "js-cookie";
import { useNavigate } from "react-router-dom";
const Index = () => {
const navigate = useNavigate();
const sessionCookie = Cookies.get("session");
useEffect(() => {
if (!sessionCookie) {
navigate("/login");
}
}, [navigate, sessionCookie]);
return (
<div style={{ width: "100%", padding: "20px", display: "flex", flexDirection: "column", alignItems: "center" }}>
<h1>Available Servers</h1>

View file

@ -5,6 +5,7 @@ import Index from './index.tsx'
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';
createRoot(document.getElementById('root')!).render(
<StrictMode>
@ -13,6 +14,7 @@ createRoot(document.getElementById('root')!).render(
<Routes>
<Route path="/" element={<Index />} />
<Route path="/search" element={<Search />} />
<Route path="/login" element={<Login />} />
</Routes>
</BrowserRouter>
</StrictMode>,