From 66287a84c68b23ed6173d4acd9286429e29256fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bu=C3=BAs=20Levente?= Date: Tue, 25 Nov 2025 19:45:34 +0100 Subject: [PATCH] bugos userhandling deck handling keszen van --- SerpentRace_Frontend/src/App.jsx | 4 + SerpentRace_Frontend/src/api/adminApi.js | 173 ++++ SerpentRace_Frontend/src/api/userApi.js | 56 ++ .../src/components/Navbar/Navbar.jsx | 41 +- .../src/pages/Admin/Admin.jsx | 750 ++++++++++++++++++ SerpentRace_Frontend/src/utils/routes.js | 6 + 6 files changed, 1026 insertions(+), 4 deletions(-) create mode 100644 SerpentRace_Frontend/src/api/adminApi.js create mode 100644 SerpentRace_Frontend/src/pages/Admin/Admin.jsx diff --git a/SerpentRace_Frontend/src/App.jsx b/SerpentRace_Frontend/src/App.jsx index 9a0c72b9..7fb97588 100644 --- a/SerpentRace_Frontend/src/App.jsx +++ b/SerpentRace_Frontend/src/App.jsx @@ -25,6 +25,7 @@ import ChooseDeck from "./pages/Game/ChooseDeck" import PlayerSetup from "./pages/Game/PlayerSetup" import GameModalsDemo from "./pages/Game/GameModalsDemo" import { GameWebSocketProvider } from "./contexts/GameWebSocketContext" +import Admin from "./pages/Admin/Admin" function App() { const [isMobile, setIsMobile] = useState(false) @@ -78,6 +79,9 @@ function App() { } /> } /> } /> + } /> + + diff --git a/SerpentRace_Frontend/src/api/adminApi.js b/SerpentRace_Frontend/src/api/adminApi.js new file mode 100644 index 00000000..c2f105bf --- /dev/null +++ b/SerpentRace_Frontend/src/api/adminApi.js @@ -0,0 +1,173 @@ +// ...new file... +import { apiClient } from "./userApi" + +/** + * Ellenőrzi localStorage.authLevel === "1". Ha nem, redirect a megadott útvonalra. + * Visszatér: true (admin) | false (nem admin, redirect történt) + */ +export function ensureAdminOrRedirect(redirectPath = "/") { + try { + const level = localStorage.getItem("authLevel") + const isAdmin = String(level) === "1" + if (!isAdmin) { + window.location.replace(redirectPath) + return false + } + return true + } catch (err) { + window.location.replace(redirectPath) + return false + } +} + +/** + * Admin: paginált paklik lekérése (admin endpoint) + * Visszatér: tömb of decks (raw vagy {decks: []} formátum kezelve) + */ +export async function adminListDecks(from = 0, to = 99) { + if (!ensureAdminOrRedirect("/")) return [] + try { + const resp = await apiClient.get(`/admin/decks/page/${from}/${to}`) + const data = resp?.data + return data?.decks || data || [] + } catch (err) { + console.error("adminApi.adminListDecks error:", err) + throw err + } +} + +/** + * Admin: keresés admin végponton. + * Ha nincs külön kereső végpont, a backend/resp lehet tömb vagy {decks: []} + * Visszatér: tömb of decks + */ +export async function adminSearchDecks(query = "", limit = 100, offset = 0) { + if (!ensureAdminOrRedirect("/")) return [] + try { + if (!query) { + // fallback: list page + return await adminListDecks(offset, offset + (limit - 1)) + } + // Preferált: admin search endpoint /admin/decks/search/{term} + const resp = await apiClient.get(`/admin/decks/search/${encodeURIComponent(query)}`, { + params: { limit, offset } + }) + const data = resp?.data + return data?.decks || data || [] + } catch (err) { + console.error("adminApi.adminSearchDecks error:", err) + throw err + } +} + +/** + * Admin: paginált felhasználók lekérése + * Visszatér: { users: [], pagination: {...} } vagy tömb of users + */ +export async function adminListUsers(from = 0, to = 99, includeDeleted = false) { + if (!ensureAdminOrRedirect("/")) return { users: [], pagination: { from, to, returned: 0, totalCount: 0, includeDeleted } } + try { + const resp = await apiClient.get(`/admin/users/page/${from}/${to}`, { + params: { includeDeleted } + }) + return resp?.data || { users: [] } + } catch (err) { + console.error("adminApi.adminListUsers error:", err) + throw err + } +} + +/** + * Admin: Get user by id + */ +export async function adminGetUserById(userId, includeDeleted = false) { + if (!ensureAdminOrRedirect("/")) return null + try { + const resp = await apiClient.get(`/admin/users/${userId}`, { params: { includeDeleted } }) + return resp?.data || null + } catch (err) { + console.error("adminApi.adminGetUserById error:", err) + throw err + } +} + +/** + * Admin: Search users + */ +export async function adminSearchUsers(query = "", includeDeleted = false, limit = 100, offset = 0) { + if (!ensureAdminOrRedirect("/")) return [] + try { + if (!query) { + // fallback: list page + const to = offset + (limit - 1) + const res = await adminListUsers(offset, to, includeDeleted) + // adminListUsers may return object or array + if (Array.isArray(res)) return res + return res.users || [] + } + const resp = await apiClient.get(`/admin/users/search/${encodeURIComponent(query)}`, { + params: { includeDeleted, limit, offset } + }) + const data = resp?.data + // prefer array but handle wrapped + return data?.users || data || [] + } catch (err) { + console.error("adminApi.adminSearchUsers error:", err) + throw err + } +} + +/** + * Admin: Update user + */ +export async function adminUpdateUser(userId, payload = {}) { + if (!ensureAdminOrRedirect("/")) return null + try { + const resp = await apiClient.patch(`/admin/users/${userId}`, payload) + return resp?.data || null + } catch (err) { + console.error("adminApi.adminUpdateUser error:", err) + throw err + } +} + +/** + * Admin: Deactivate user + */ +export async function adminDeactivateUser(userId) { + if (!ensureAdminOrRedirect("/")) return null + try { + const resp = await apiClient.post(`/admin/users/${userId}/deactivate`) + return resp?.data || null + } catch (err) { + console.error("adminApi.adminDeactivateUser error:", err) + throw err + } +} + +/** + * Admin: Delete user + */ +export async function adminDeleteUser(userId) { + if (!ensureAdminOrRedirect("/")) return null + try { + const resp = await apiClient.delete(`/admin/users/${userId}`) + return resp?.data || null + } catch (err) { + console.error("adminApi.adminDeleteUser error:", err) + throw err + } +} + +export default { + ensureAdminOrRedirect, + adminListDecks, + adminSearchDecks, + // new user API exports + adminListUsers, + adminGetUserById, + adminSearchUsers, + adminUpdateUser, + adminDeactivateUser, + adminDeleteUser, +} diff --git a/SerpentRace_Frontend/src/api/userApi.js b/SerpentRace_Frontend/src/api/userApi.js index af231c48..08f6640b 100644 --- a/SerpentRace_Frontend/src/api/userApi.js +++ b/SerpentRace_Frontend/src/api/userApi.js @@ -107,3 +107,59 @@ export const resetPassword = async (token, newPassword) => { } }; +// Add: response interceptor + error parser +apiClient.interceptors.response.use( + (resp) => resp, + (error) => { + // Log useful debug info + try { + // eslint-disable-next-line no-console + console.error("API Error:", { + message: error.message, + status: error?.response?.status, + url: error?.config?.url, + data: error?.response?.data, + }) + } catch (e) {} + // forward + return Promise.reject(error) + } +) + +/** + * Normalize axios error into readable string + * Prefer backend-provided message (resp.data.message), fallback to resp.data or error.message. + */ +export function getApiErrorMessage(error) { + if (!error) return "Ismeretlen hiba" + // axios error with response + const resp = error.response + if (resp) { + const data = resp.data + if (data) { + if (typeof data === "string") return data + if (data.message) return String(data.message) + // sometimes backend sends { errors: [...] } + if (data.errors && Array.isArray(data.errors)) return data.errors.map(e => e.message || JSON.stringify(e)).join("; ") + // fallback to whole payload + try { return JSON.stringify(data) } catch (e) {} + } + return `HTTP ${resp.status} ${resp.statusText || ""}`.trim() + } + // non-response axios error + return error.message || String(error) +} + +export default { + login, + register, + getUserStats, + verifyEmail, + getUserProfile, + updateUserProfile, + deleteUserProfile, + forgotPassword, + resetPassword, + apiClient, +} + diff --git a/SerpentRace_Frontend/src/components/Navbar/Navbar.jsx b/SerpentRace_Frontend/src/components/Navbar/Navbar.jsx index 1d58cbf5..b7c4cd17 100644 --- a/SerpentRace_Frontend/src/components/Navbar/Navbar.jsx +++ b/SerpentRace_Frontend/src/components/Navbar/Navbar.jsx @@ -2,7 +2,7 @@ import React, { useState } from "react" import Logo from "../../assets/pictures/Logo" import { Link } from "react-router-dom" import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" // ✅ importáld a navigációs hookot -import { FaSignOutAlt, FaChartBar, FaUser, FaBars } from "react-icons/fa" +import { FaSignOutAlt, FaChartBar, FaUser, FaBars, FaUserShield } from "react-icons/fa" const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10" const navLinkClassPlay = @@ -13,6 +13,9 @@ const Navbar = () => { const [userMenuOpen, setUserMenuOpen] = useState(false) const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username")) + // Új: ellenőrizzük, hogy admin-e (authLevel === "1") + const isAdmin = localStorage.getItem("authLevel") === "1" + // ✅ Használjuk a HandleNavigate hookot const { goLanding, goAbout, goHome, goLogin, goContacts } = HandleNavigate() @@ -111,6 +114,21 @@ const Navbar = () => { Statisztikák + + {/* Admin gomb a felhasználói lenyílóban, csak adminoknak */} + {isAdmin && ( + + )} + {isLoggedIn && ( - setMenuOpen(false)} className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center"> + setMenuOpen(false)} + className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center" + > Paklik )} + {/* Admin gomb csak authLevel === "1" esetén */} + {isAdmin && ( + setMenuOpen(false)} + className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center" + > + Admin + + )} + + ) + + // --- Teljes Deck handling (DeckManager-ről áthozva) --- + const DeckHandling = () => ( + +
+

Deck management

+ +
+ +
+
+ setQuery(e.target.value)} + width={240} + placeholder="Keresés..." + className="mr-4" + /> + + Típus: + + {deckTypes.map((type) => ( + + ))} + Eredet: + + + + Rendezés: + + + +
+ + {/* Items per page selector and count */} +
+
+ + Elemek oldalanként: + + +
+ +
+ {totalDecks > 0 ? ( + <> + {startIndex + 1}-{Math.min(endIndex, totalDecks)} / {totalDecks} pakli + + ) : ( + <>0 pakli + )} +
+
+ + {/* Decks Grid */} +
+ {/* Create New Deck card */} +
goDeckCreator()} + className="flex flex-col items-center justify-center h-40 sm:h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-xl sm:rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg" + > + + Új pakli létrehozása +
+ + {loading &&
Betöltés...
} + {!loading && paginatedDecks.length === 0 &&
Nincsenek találatok.
} + + {!loading && paginatedDecks.map((deck) => { + const deckType = deckTypes.find((t) => t.label === deck.type) + const borderColor = deckType ? deckType.color : "var(--color-success)" + return ( +
setSelectedDeck(deck)} + > +
+ + {deck.type === "Luck" + ? "Szerencse" + : deck.type === "Question" + ? "Kérdés" + : "Joker"} + +

+ {deck.name} +

+
+
+ Létrehozva: {deck.created} +
+
+ ) + })} +
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + +
+ {[...Array(totalPages)].map((_, index) => { + const pageNum = index + 1 + if ( + pageNum === 1 || + pageNum === totalPages || + (pageNum >= currentPage - 1 && pageNum <= currentPage + 1) + ) { + return ( + + ) + } else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) { + return ... + } + return null + })} +
+ + +
+ )} + + {/* Sort help popup */} + {showSortHelp && ( + setShowSortHelp(false)}> +

Rendezési lehetőségek magyarázata

+
    +
  • 📅↑ – Dátum szerint növekvő sorrendben (legrégebbi elöl)
  • +
  • 📅↓ – Dátum szerint csökkenő sorrendban (legújabb elöl)
  • +
  • A→Z – Név szerint növekvő sorrendben (A-tól Z-ig)
  • +
  • Z→A – Név szerint csökkenő sorrendben (Z-től A-ig)
  • +
+
+ +
+
+ )} + + {/* Deck Info Popup */} + {selectedDeck && setSelectedDeck(null)} />} +
+
+ ) + + const UserHandlingMock = () => ( + +
+

User management

+
+ alert("Invite / invite-flow to implement")} width="w-36" /> +
+
+ +
+
+ setUserQuery(e.target.value)} + width={240} + placeholder="Keresés felhasználók között..." + className="mr-4" + /> + +
+ +
+
+ Elemek oldalanként: + +
+ +
+ {userTotalCount > 0 ? `${Math.min((userCurrentPage-1)*userItemsPerPage + 1, userTotalCount)}-${Math.min(userCurrentPage*userItemsPerPage, userTotalCount)} / ${userTotalCount} felhasználó` : '0 felhasználó'} +
+
+ + {/* User list */} +
+ {userLoading &&
Betöltés...
} + {!userLoading && users.length === 0 &&
Nincsenek találatok.
} + + {!userLoading && users.slice((userCurrentPage-1)*userItemsPerPage, userCurrentPage*userItemsPerPage).map(u => ( +
+
+
{u.username}
+
{u.email}
+
Létrehozva: {u.created}
+
+
+ {u.deleted ? 'Törölt' : 'Aktív'} + +
+
+ ))} +
+ + {/* Pagination controls for users */} + { Math.ceil(Math.max(userTotalCount, users.length) / userItemsPerPage) > 1 && ( +
+ {(() => { + const totalPages = Math.max(1, Math.ceil(Math.max(userTotalCount, users.length) / userItemsPerPage)) + return ( + <> + +
+ {[...Array(totalPages)].map((_, i) => { + const p = i+1 + if (p === 1 || p === totalPages || (p >= userCurrentPage-1 && p <= userCurrentPage+1)) { + return + } + if (p === userCurrentPage-2 || p === userCurrentPage+2) return ... + return null + })} +
+ + + ) + })()} +
+ )} + + {/* User details popup */} + {selectedUser && ( + setSelectedUser(null)}> +

{selectedUser.username}

+
+ Email: {selectedUser.email}
+ Létrehozva: {selectedUser.created}
+ Status: {selectedUser.deleted ? 'Törölt' : 'Aktív'} +
+
+ {!selectedUser.deleted && ( + + )} + + +
+
+ )} +
+
+ ) + + // ---------- NEW: UUID helper + resolver ---------- + const isUuid = (val) => { + if (!val || typeof val !== "string") return false + // simple RFC4122 v4-ish UUID check (hex + dashes) + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val.trim()) + } + + // Try various places on the user object for a UUID, fallback to search by username/email + const resolveUserUuid = async (user) => { + if (!user) return null + const candidates = [ + user.id, + user.raw?.id, + user.raw?.userid, + user.raw?.userId, + user.raw?.uuid, + user.raw?.user_id + ] + for (const c of candidates) if (isUuid(String(c || ""))) return String(c).trim() + + // fallback: try to find canonical user by username or email via adminSearchUsers + const q = user.username || user.email || user.raw?.username || user.raw?.email + if (!q) return null + try { + const res = await adminSearchUsers(q, includeDeletedUsers ? true : false, 10, 0) + const arr = Array.isArray(res) ? res : (res.users || res || []) + if (Array.isArray(arr) && arr.length) { + // try to match by username/email exactly, or take first with valid id + const exact = arr.find(u => (u.username && u.username === user.username) || (u.email && u.email === user.email)) + const pick = exact || arr[0] + if (pick && isUuid(pick.id)) return pick.id + } + } catch (err) { + console.error("resolveUserUuid: search failed", err) + } + return null + } + // ---------- end new helpers ---------- + + return ( +
+
+ +
+ +
+ +
+ +
+
+ {/* Left navigation column */} + + + {/* Right content column */} +
+
+ {active === "decks" ? : } +
+
+
+
+ +
+ ) +} diff --git a/SerpentRace_Frontend/src/utils/routes.js b/SerpentRace_Frontend/src/utils/routes.js index 24913a46..712a57ed 100644 --- a/SerpentRace_Frontend/src/utils/routes.js +++ b/SerpentRace_Frontend/src/utils/routes.js @@ -36,8 +36,14 @@ export const ROUTES = { REPORTS: '/report', CONTACTS: '/contacts', TEST: '/test', + + // admin + ADMIN: '/admin', + } + + // Helper functions to generate dynamic routes export const routeHelpers = { deckDetails: (deckId) => `/deck/${deckId}`,