Compare commits

...
10 Commits
10 changed files with 456 additions and 71 deletions
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/GameUI-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<title>GameUI</title>
</head>
<body>
<div id="root"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+21 -13
View File
@@ -1,25 +1,33 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Dashboard from "./pages/Dashboard";
import Layout from "./components/Layout";
import ProtectedRoute from "./components/ProtectedRoute";
const App = () => {
const token = localStorage.getItem("token");
const [token, setToken] = useState(null);
useEffect(() => {
const storedToken = localStorage.getItem("token");
setToken(storedToken);
}, []);
return (
<Layout>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/dashboard"
element={token ? <Dashboard /> : <Navigate to="/login" />}
/>
<Route path="*" element={<Navigate to="/login" />} />
</Routes>
</Layout>
<Routes>
<Route path="/login" element={<Login setToken={setToken} />} />
<Route path="/register" element={<Register />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/login" />} />
</Routes>
);
};
+2 -8
View File
@@ -1,15 +1,9 @@
import React, { useEffect, useState } from "react";
import React from "react";
import { useNavigate } from "react-router-dom";
const Layout = ({ children }) => {
const [token, setToken] = useState(null);
const Layout = ({ children, token, setToken }) => {
const navigate = useNavigate();
useEffect(() => {
const t = localStorage.getItem("token");
setToken(t);
}, []);
const handleLogout = () => {
localStorage.removeItem("token");
setToken(null);
+21
View File
@@ -0,0 +1,21 @@
import React from "react";
const PageWrapper = ({ children, center = false }) => (
<div
style={{
height: "100vh",
margin: 0,
padding: 0,
display: "flex",
flexDirection: "column",
alignItems: center ? "center" : "stretch",
justifyContent: center ? "center" : "flex-start",
backgroundColor: "#242424",
position: "relative",
}}
>
{children}
</div>
);
export default PageWrapper;
+64
View File
@@ -0,0 +1,64 @@
import React from "react";
import { Navigate, useNavigate } from "react-router-dom";
import PageWrapper from "./PageWrapper";
import useIdleTimer from "../hooks/useIdleTimer";
const ProtectedRoute = ({ children }) => {
const token = localStorage.getItem("token");
const navigate = useNavigate();
const handleLogout = async () => {
const token = localStorage.getItem("token");
if (!token) {
console.warn("No token found — skipping API logout.");
} else {
try {
await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Auth/logout`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch (err) {
console.error("Logout API error:", err);
}
}
localStorage.removeItem("token");
localStorage.removeItem("role");
navigate("/login");
};
useIdleTimer(() => {
if (token) {
handleLogout();
}
}, 60000); // 1 minute
if (!token) return <Navigate to="/login" />;
return (
<PageWrapper>
<button
onClick={handleLogout}
style={{
position: "absolute",
top: "1rem",
right: "1rem",
backgroundColor: "#1a1a1a",
color: "#fff",
padding: "0.5rem 1rem",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
Logout
</button>
{children}
</PageWrapper>
);
};
export default ProtectedRoute;
+24
View File
@@ -0,0 +1,24 @@
import { useEffect, useRef } from "react";
const useIdleTimer = (onIdle, timeout = 60000) => {
const timerRef = useRef(null);
const resetTimer = () => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(onIdle, timeout);
};
useEffect(() => {
const events = ["mousemove", "keydown", "mousedown", "touchstart"];
events.forEach(e => window.addEventListener(e, resetTimer));
resetTimer(); // initialize timer
return () => {
clearTimeout(timerRef.current);
events.forEach(e => window.removeEventListener(e, resetTimer));
};
}, []);
};
export default useIdleTimer;
+237
View File
@@ -0,0 +1,237 @@
import React, { useEffect, useState } from "react";
import config from "../config";
const thStyle = {
padding: "0.75rem",
backgroundColor: "#333",
borderBottom: "1px solid #555",
color: "#fff",
position: "sticky",
top: 0,
zIndex: 1,
textAlign: "center",
};
const tdStyle = {
padding: "0.5rem 1rem",
borderBottom: "1px solid #444",
textAlign: "center",
color: "#fff",
};
const Dashboard = () => {
const [role, setRole] = useState("");
const [currentUser, setCurrentUser] = useState("");
const [users, setUsers] = useState([]);
const [newRoles, setNewRoles] = useState({});
const [selectedUsers, setSelectedUsers] = useState([]);
useEffect(() => {
const storedRole = localStorage.getItem("role");
const storedUsername = localStorage.getItem("username");
setRole(storedRole);
setCurrentUser(storedUsername);
if (storedRole === "SUPER") {
fetchUsers();
}
}, []);
const fetchUsers = async () => {
const token = localStorage.getItem("token");
try {
const res = await fetch(`${config.apiBaseUrl}/api/Auth/users`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) throw new Error("Failed to fetch users");
const data = await res.json();
setUsers(data);
} catch (err) {
console.error("Error fetching users:", err);
}
};
const handleRoleChange = (username, value) => {
setNewRoles({ ...newRoles, [username]: value });
};
const updateUserRole = async (username) => {
const token = localStorage.getItem("token");
const newRole = newRoles[username];
try {
const res = await fetch(`${config.apiBaseUrl}/api/Auth/role`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ username, newRole }),
});
if (!res.ok) throw new Error("Failed to update role");
fetchUsers();
} catch (err) {
console.error("Error updating role:", err);
}
};
const deleteUser = async (username) => {
if (!window.confirm(`Are you sure you want to delete ${username}?`)) return;
const token = localStorage.getItem("token");
try {
const res = await fetch(`${config.apiBaseUrl}/api/User?username=${encodeURIComponent(username)}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) throw new Error("Failed to delete user");
fetchUsers();
} catch (err) {
console.error("Error deleting user:", err);
}
};
const toggleUserSelection = (username) => {
setSelectedUsers((prev) =>
prev.includes(username)
? prev.filter((u) => u !== username)
: [...prev, username]
);
};
return (
<div style={{ color: "#fff", textAlign: "center", marginTop: "2rem" }}>
<h1>Dashboard</h1>
<p>Welcome! You are logged in.</p>
{role === "SUPER" && (
<>
<p style={{ fontWeight: "bold" }}>You are SUPER</p>
<div
style={{
maxHeight: "320px",
overflowY: "auto",
margin: "2rem auto 0 auto",
width: "fit-content",
border: "1px solid #444",
borderRadius: "4px",
}}
>
<table style={{ borderCollapse: "collapse", minWidth: "600px" }}>
<thead>
<tr>
<th style={thStyle}></th>
<th style={thStyle}>Username</th>
<th style={thStyle}>Role</th>
{selectedUsers.length > 0 && <th style={thStyle}>Action</th>}
</tr>
</thead>
<tbody>
{users
.filter((user) => user.username !== currentUser)
.map((user) => {
const isSelected = selectedUsers.includes(user.username);
const currentNewRole = newRoles[user.username] ?? user.role;
return (
<tr key={user.username}>
<td style={tdStyle}>
<input
type="checkbox"
checked={isSelected}
onChange={() => toggleUserSelection(user.username)}
/>
</td>
<td style={tdStyle}>{user.username}</td>
<td style={tdStyle}>
{isSelected ? (
<select
value={currentNewRole}
onChange={(e) =>
handleRoleChange(
user.username,
e.target.value
)
}
style={{
padding: "0.3rem",
backgroundColor: "#2a2a2a",
color: "#fff",
border: "1px solid #444",
}}
>
<option value="USER">USER</option>
<option value="SUPER">SUPER</option>
</select>
) : (
user.role
)}
</td>
{selectedUsers.length > 0 && (
<td style={tdStyle}>
{isSelected && (
<div
style={{
display: "flex",
gap: "0.5rem",
justifyContent: "center",
}}
>
<button
onClick={() =>
updateUserRole(user.username)
}
style={{
padding: "0.4rem 0.8rem",
backgroundColor: "#444",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
Update
</button>
<button
onClick={() => deleteUser(user.username)}
style={{
padding: "0.4rem 0.8rem",
backgroundColor: "#aa3333",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
Delete
</button>
</div>
)}
</td>
)}
</tr>
);
})}
</tbody>
</table>
</div>
</>
)}
</div>
);
};
export default Dashboard;
+42 -35
View File
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import config from "../config";
import { useAuth } from "../AuthContext";
import PageWrapper from "../components/PageWrapper";
const styles = {
form: {
@@ -81,48 +82,54 @@ const Login = () => {
}
const data = await res.json();
login(data.token);
login(data.accessToken);
localStorage.setItem("token", data.accessToken);
localStorage.setItem("role", data.role);
localStorage.setItem("username", data.username);
navigate("/dashboard");
} catch (err) {
console.error("Login error:", err);
alert("Login failed: " + err.message);
}
};
return (
<form onSubmit={handleSubmit} style={styles.form}>
<h2 style={styles.title}>Login</h2>
<input
type="text"
name="username"
placeholder="Username"
value={form.username}
onChange={handleChange}
style={styles.input}
required
/>
<input
type="password"
name="password"
placeholder="Password"
value={form.password}
onChange={handleChange}
style={styles.input}
required
/>
<div style={styles.buttonRow}>
<button type="submit" style={styles.button}>
Login
</button>
<button
type="button"
onClick={() => navigate("/register")}
style={styles.button}
>
Register
</button>
</div>
</form>
<PageWrapper center>
<form onSubmit={handleSubmit} style={styles.form}>
<h2 style={styles.title}>Login</h2>
<input
type="text"
name="username"
placeholder="Username"
value={form.username}
onChange={handleChange}
style={styles.input}
required
/>
<input
type="password"
name="password"
placeholder="Password"
value={form.password}
onChange={handleChange}
style={styles.input}
required
/>
<div style={styles.buttonRow}>
<button type="submit" style={styles.button}>
Login
</button>
<button
type="button"
onClick={() => navigate("/register")}
style={styles.button}
>
Register
</button>
</div>
</form>
</PageWrapper>
);
};
+43 -13
View File
@@ -1,6 +1,8 @@
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import config from "../config";
import { useAuth } from "../AuthContext";
const styles = {
form: {
@@ -45,6 +47,8 @@ const styles = {
};
const Register = () => {
const { login } = useAuth();
const [form, setForm] = useState({
username: "",
email: "",
@@ -60,29 +64,55 @@ const Register = () => {
const handleSubmit = async (e) => {
e.preventDefault();
const payload = {
username: form.username,
password: form.password,
};
try {
const res = await fetch(`${config.apiBaseUrl}/api/Auth/register`, {
// Step 1: Register the user
const registerRes = await fetch(
`${config.apiBaseUrl}/api/Auth/register`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);
if (!registerRes.ok) {
const errorData = await registerRes.json();
throw new Error(errorData.message || "Registration failed");
}
// Consume body even if empty to ensure flush
await registerRes.text(); // <-- this is the key to making chaining clean
// Step 2: Immediately login
const loginRes = await fetch(`${config.apiBaseUrl}/api/Auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: form.username,
email: form.email,
password: form.password,
}),
body: JSON.stringify(payload),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.message || "Registration failed");
if (!loginRes.ok) {
throw new Error("Registration succeeded but login failed");
}
alert("Registration successful!");
navigate("/login");
const loginData = await loginRes.json();
login(loginData.accessToken);
localStorage.setItem("token", loginData.accessToken);
localStorage.setItem("role", loginData.role);
localStorage.setItem("username", data.username);
navigate("/dashboard");
} catch (err) {
console.error("Register error:", err);
alert("Network or server error occurred.");
console.error("Register/login error:", err);
}
};