Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
461a537de1 | ||
|
|
d0c512fd15 | ||
|
|
0bf24c69ff | ||
|
|
649155c0c6 | ||
|
|
a3ca64dc8d | ||
|
|
6a1697048c | ||
|
|
643b525d1b | ||
|
|
39d3ff9918 | ||
|
|
8f1cba5f82 | ||
|
|
fbd48297e2 |
+2
-2
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user