diff --git a/PintudosFront/package-lock.json b/PintudosFront/package-lock.json index bb1bbe5..55431db 100644 --- a/PintudosFront/package-lock.json +++ b/PintudosFront/package-lock.json @@ -8,9 +8,11 @@ "name": "pintudos", "version": "0.0.0", "dependencies": { + "@react-oauth/google": "^0.12.2", "@stomp/stompjs": "^7.1.0", "axios": "^1.8.4", "crypto-js": "^4.2.0", + "jwt-decode": "^4.0.0", "pintudos": "file:", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -1086,6 +1088,16 @@ "node": ">= 8" } }, + "node_modules/@react-oauth/google": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz", + "integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.38.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.38.0.tgz", @@ -2909,6 +2921,15 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/PintudosFront/package.json b/PintudosFront/package.json index d691b7e..ba9fb14 100644 --- a/PintudosFront/package.json +++ b/PintudosFront/package.json @@ -10,9 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@react-oauth/google": "^0.12.2", "@stomp/stompjs": "^7.1.0", "axios": "^1.8.4", "crypto-js": "^4.2.0", + "jwt-decode": "^4.0.0", "pintudos": "file:", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/PintudosFront/src/App.tsx b/PintudosFront/src/App.tsx index df11462..25c9f08 100644 --- a/PintudosFront/src/App.tsx +++ b/PintudosFront/src/App.tsx @@ -1,16 +1,29 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; -import Home from './components/Home/Home'; +import { GoogleOAuthProvider } from '@react-oauth/google'; +import { AuthProvider } from './components/AuthContext/AuthContext'; +import Login from './components/Login/Login'; import Juego from './components/Juego/Juego'; +import ProtectedRoute from './components/ProtectedRoute/ProtectedRoute'; +import Home from './components/Home/Home'; // Asegúrate de que la ruta sea correcta function App() { return ( - - - } /> - } /> - - + + + + + } /> + } /> + + + + } /> + + + + ); } diff --git a/PintudosFront/src/components/AuthContext/AuthContext.tsx b/PintudosFront/src/components/AuthContext/AuthContext.tsx new file mode 100644 index 0000000..d837a98 --- /dev/null +++ b/PintudosFront/src/components/AuthContext/AuthContext.tsx @@ -0,0 +1,54 @@ +import React, { + createContext, + useState, + useContext, + ReactNode, + FC +} from 'react'; + +// Define el tipo del contexto +interface AuthContextType { + token: string | null; + login: (newToken: string) => void; + logout: () => void; +} + +// Crear contexto con valor inicial `undefined` +const AuthContext = createContext(undefined); + +// Props del proveedor +interface AuthProviderProps { + children: ReactNode; +} + +// Componente proveedor +export const AuthProvider: FC = ({ children }) => { + const [token, setToken] = useState( + localStorage.getItem("token") + ); + + const login = (newToken: string) => { + setToken(newToken); + localStorage.setItem("token", newToken); + }; + + const logout = () => { + setToken(null); + localStorage.removeItem("token"); + }; + + return ( + + {children} + + ); +}; + +// Hook para usar el contexto +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth debe usarse dentro de un AuthProvider"); + } + return context; +}; diff --git a/PintudosFront/src/components/Login/Login.css b/PintudosFront/src/components/Login/Login.css new file mode 100644 index 0000000..62cf420 --- /dev/null +++ b/PintudosFront/src/components/Login/Login.css @@ -0,0 +1,45 @@ +/* Home.css */ +.login-container { + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, #6a0dad, #f7a83a, #6a0dad); + padding: 20px; +} + +.login-card { + background-color: #ffffff; + border-radius: 16px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + padding: 40px 30px; + max-width: 400px; + width: 100%; + text-align: center; +} + +.login-title { + font-size: 32px; + font-weight: bold; + color: #333; + margin-bottom: 10px; +font-family: 'Finger Paint', cursive; +} + +.login-title span { + color: #7e5bef; +font-family: 'Finger Paint', cursive; +} + +.login-subtitle { + color: #666; + margin-bottom: 30px; + font-size: 16px; + font-family: 'Finger Paint', cursive; +} + +.login-button { + display: flex; + justify-content: center; + font-family: 'Finger Paint', cursive; +} diff --git a/PintudosFront/src/components/Login/Login.tsx b/PintudosFront/src/components/Login/Login.tsx new file mode 100644 index 0000000..6f2ddbd --- /dev/null +++ b/PintudosFront/src/components/Login/Login.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { GoogleLogin, CredentialResponse } from '@react-oauth/google'; +import { jwtDecode } from 'jwt-decode'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../AuthContext/AuthContext'; +import './Login.css'; // Asegúrate de importar el CSS + +interface DecodedToken { + name: string; + email: string; + picture: string; + sub: string; + [key: string]: unknown; +} + +const Login: React.FC = () => { + const navigate = useNavigate(); + const { login } = useAuth(); + + const onSuccess = (credentialResponse: CredentialResponse) => { + const token = credentialResponse.credential; + if (token) { + const user = jwtDecode(token); + console.log("Usuario:", user); + login(token); + navigate("/home"); + } else { + console.error("No se recibió token de Google"); + } + }; + + return ( + + + Bienvenido a Pintu2 + Inicia sesión para comenzar a jugar + + console.log('Falló el login')} + /> + + + + ); +}; + +export default Login; diff --git a/PintudosFront/src/components/ProtectedRoute/ProtectedRoute.tsx b/PintudosFront/src/components/ProtectedRoute/ProtectedRoute.tsx new file mode 100644 index 0000000..7f4757a --- /dev/null +++ b/PintudosFront/src/components/ProtectedRoute/ProtectedRoute.tsx @@ -0,0 +1,15 @@ +import React, { ReactNode } from "react"; +import { Navigate } from "react-router-dom"; +import { useAuth } from "../AuthContext/AuthContext"; + +interface ProtectedRouteProps { + children: ReactNode; +} + +const ProtectedRoute: React.FC = ({ children }) => { + const { token } = useAuth(); + return token ? children : ; +}; + +export default ProtectedRoute; + diff --git a/PintudosFront/src/main.tsx b/PintudosFront/src/main.tsx index 29da340..6e823b4 100644 --- a/PintudosFront/src/main.tsx +++ b/PintudosFront/src/main.tsx @@ -1,12 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; -import { WebSocketProvider } from './useWebSocket'; +import { WebSocketProvider } from './useWebSocket'; +import { AuthProvider } from './components/AuthContext/AuthContext'; // Asegúrate que la ruta esté correcta ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + ); diff --git a/PintudosFront/src/useWebSocket.tsx b/PintudosFront/src/useWebSocket.tsx index 09e5aaf..cd233ba 100644 --- a/PintudosFront/src/useWebSocket.tsx +++ b/PintudosFront/src/useWebSocket.tsx @@ -1,52 +1,78 @@ -import React, { createContext, useContext, useEffect, useRef, useState } from "react"; +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; import { Client, IMessage } from "@stomp/stompjs"; import SockJS from "sockjs-client"; - type WebSocketContextType = { createRoom: (roomId: string, player: string) => void; joinRoom: (roomId: string, player: string) => void; - sendMessage: (roomId: string, message: string, type?: "chat" | "trace") => void; + sendMessage: ( + roomId: string, + message: string, + type?: "chat" | "trace" + ) => void; subscribeToChat: (roomId: string, callback: (msg: string) => void) => void; subscribeToTraces: (roomId: string, callback: (trace: any) => void) => void; connected: boolean; }; - const WebSocketContext = createContext(null); -export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { const clientRef = useRef(null); const [connected, setConnected] = useState(false); - useEffect(() => { - const client = new Client({ - webSocketFactory: () => new SockJS("http://localhost:8080/game"), - reconnectDelay: 5000, - heartbeatIncoming: 4000, - heartbeatOutgoing: 4000, - onConnect: () => { - console.log("✅ Conectado a STOMP"); - setConnected(true); - }, - onDisconnect: () => { - console.log("❌ Desconectado de STOMP"); - setConnected(false); - }, - onStompError: (frame) => { - console.error('STOMP error:', frame); - }, - onWebSocketError: (event) => { - console.error('WebSocket error:', event); - }, - }); + // Paso 1: Realizar la solicitud previa para establecer sesión + fetch("http://localhost:8080/game?continue", { + method: "GET", + credentials: "include", // Importante: incluir cookies + }) + .then((response) => { + console.log("✅ Pre-autenticación completada:", response.status); + + // Paso 2: Una vez que tenemos respuesta, inicializar WebSocket + initializeWebSocket(); + }) + .catch((error) => { + console.error("❌ Error en pre-autenticación:", error); + }); + function initializeWebSocket() { + const client = new Client({ + webSocketFactory: () => new SockJS("http://localhost:8080/game"), + reconnectDelay: 5000, + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000, + onConnect: () => { + console.log("✅ Conectado a STOMP"); + setConnected(true); + }, + onDisconnect: () => { + console.log("❌ Desconectado de STOMP"); + setConnected(false); + }, + onStompError: (frame) => { + console.error("STOMP error:", frame); + }, + onWebSocketError: (event) => { + console.error("WebSocket error:", event); + }, + }); - client.activate(); - clientRef.current = client; + client.activate(); + clientRef.current = client; + } + // Limpieza al desmontar el componente return () => { - if (client.active) { - client.deactivate(); + if (clientRef.current?.active) { + clientRef.current.deactivate(); } }; }, []); @@ -54,7 +80,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi const createRoom = (roomId: string, player: string) => { clientRef.current?.publish({ destination: "/app/createRoom", - body: JSON.stringify({ roomId, player}), + body: JSON.stringify({ roomId, player }), }); }; @@ -65,18 +91,17 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi }); }; - const sendMessage = ( roomId: string, message: string, type: "chat" | "trace" = "trace", - sender?: string + sender?: string ) => { const destination = type === "chat" ? `/app/chat/${roomId}` : `/app/trace/${roomId}`; - + let body; - + if (type === "chat") { body = JSON.stringify({ sender: sender ?? "Anónimo", @@ -85,51 +110,67 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi } else { body = JSON.stringify(message); } - - clientRef.current?.publish({ - destination, - body, - }); - }; - - const subscribeToPlayerCount = (roomId: string, callback: (count: number) => void) => { - clientRef.current?.subscribe(`/topic/room/${roomId}/players`, (msg: IMessage) => { - const data = JSON.parse(msg.body); - callback(data.players); - console.log("🧪 Recibido conteo de jugadores:", data); + + clientRef.current?.publish({ + destination, + body, }); }; - - const subscribeToChat = (roomId: string, callback: (msg: ChatMessage) => void) => { - return clientRef.current?.subscribe(`/topic/chat/${roomId}`, (msg: IMessage) => { - const data = JSON.parse(msg.body); - callback(data); - }); + + const subscribeToPlayerCount = ( + roomId: string, + callback: (count: number) => void + ) => { + clientRef.current?.subscribe( + `/topic/room/${roomId}/players`, + (msg: IMessage) => { + const data = JSON.parse(msg.body); + callback(data.players); + console.log("🧪 Recibido conteo de jugadores:", data); + } + ); }; - const subscribeToTraces = (roomId: string, callback: (trace: any) => void) => { - clientRef.current?.subscribe(`/topic/${roomId}/traces`, (message: IMessage) => { - const trace = JSON.parse(message.body); - callback(trace); - }); + const subscribeToChat = ( + roomId: string, + callback: (msg: ChatMessage) => void + ) => { + return clientRef.current?.subscribe( + `/topic/chat/${roomId}`, + (msg: IMessage) => { + const data = JSON.parse(msg.body); + callback(data); + } + ); + }; + + const subscribeToTraces = ( + roomId: string, + callback: (trace: any) => void + ) => { + clientRef.current?.subscribe( + `/topic/${roomId}/traces`, + (message: IMessage) => { + const trace = JSON.parse(message.body); + callback(trace); + } + ); }; - return ( - {children} - - + value={{ + createRoom, + joinRoom, + sendMessage, + subscribeToChat, + subscribeToTraces, + connected, + subscribeToPlayerCount, + }} + > + {children} + ); };
Inicia sesión para comenzar a jugar