Skip to content
Merged
2 changes: 2 additions & 0 deletions backend/src/auth/jwt/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface AuthenticatedRequestUser {
* pending token is `isTwoFactorEnabled: true, tfa: 'pending'`.
*/
isTwoFactorEnabled: boolean;
avatar: string | null;
}

const cookieExtractor = (req: Request): string | null => {
Expand Down Expand Up @@ -68,6 +69,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
username: user.username,
tfa: payload.tfa,
isTwoFactorEnabled: user.isTwoFactorEnabled,
avatar: user.avatar,
};
}
}
36 changes: 36 additions & 0 deletions frontend/src/app/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from "react";

const isImageUrl = (avatar?: string | null): boolean =>
!!avatar && /^(https?:\/\/|\/)/.test(avatar);

interface AvatarProps {
src?: string | null;
alt?: string;
className?: string;
}

export default function Avatar({
src,
alt = "avatar",
className = "w-10 h-10 text-xl",
}: AvatarProps) {
const [failed, setFailed] = useState(false);
const showImage = isImageUrl(src) && !failed;
return (
<div
className={`rounded-full bg-white flex items-center justify-center ring-2 ring-white/50 shadow flex-shrink-0 overflow-hidden ${className}`}
>
{showImage ? (
<img
src={src!}
alt={alt}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
onError={() => setFailed(true)}
/>
) : (
<span>{(!isImageUrl(src) && src) || "😊"}</span>
)}
</div>
);
}
25 changes: 8 additions & 17 deletions frontend/src/app/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { useState, useEffect, useRef } from "react";
import { socket } from "../../socket/socket";
import { useGame } from "../context/GameContext";
import Avatar from "./Avatar";

interface LayoutProps {
username: string;
Expand Down Expand Up @@ -128,7 +129,7 @@ return (
className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 hover:bg-white text-gray-700 hover:text-amber-600 border border-gray-200 transition-all shadow-sm hover:shadow-md"
>
<Trophy className="w-4 h-4" />
<span className="hidden sm:inline font-medium">Classement</span>
<span className="hidden sm:inline font-medium">Ranking</span>
</button>

{/* Bouton Profil (avec avatar dynamique) */}
Expand All @@ -137,13 +138,7 @@ return (
className="group flex items-center gap-3 px-4 py-2 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 transition-all shadow-md hover:shadow-lg"
>
<span className="text-white font-medium">{username}</span>
<div className="w-10 h-10 rounded-full bg-white flex items-center justify-center text-2xl ring-2 ring-white/50 overflow-hidden">
{userAvatar ? (
<img src={userAvatar} alt={username} className="w-full h-full object-cover" />
) : (
<span>😊</span>
)}
</div>
<Avatar src={userAvatar} alt={username} className="w-10 h-10 text-2xl" />
</button>

{/* Bouton Déconnexion */}
Expand Down Expand Up @@ -201,7 +196,7 @@ return (
<div className="bg-gradient-to-r from-purple-500 to-pink-500 p-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-white" />
<span className="text-white font-bold">Chat Global</span>
<span className="text-white font-bold">Global Chat</span>
<span className="bg-white/30 text-white px-2 py-0.5 rounded-full text-sm">
{onlineCount} online
</span>
Expand All @@ -225,22 +220,18 @@ return (
{/* Messages (Zone de texte) */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50">
{Array.isArray(messages) && messages.length === 0 ? (
<p className="text-gray-500 text-sm text-center mt-4">Aucun message pour l'instant...</p>
<p className="text-gray-500 text-sm text-center mt-4">No message for now...</p>
) : (
Array.isArray(messages) && messages.map((msg, index) => {
const authorName = msg.author?.username || msg.user || "Inconnu";

return (
<div
key={msg.id || index}
className="flex gap-3 animate-in fade-in slide-in-from-bottom-2"
>
{/* Avatar */}
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-purple-400 flex items-center justify-center flex-shrink-0 text-lg">
{msg.avatar || "😊"}
</div>

<div className="flex-1">
<Avatar src={msg.author?.avatar ?? msg.avatar} alt={authorName} />
<div className="flex-1">
<div className="flex items-baseline gap-2">
{/* Pseudo Cliquable */}
<span
Expand Down Expand Up @@ -282,7 +273,7 @@ return (
<div className="flex gap-2 items-center">
<input
type="text"
placeholder="Écrire un message..."
placeholder="Write a message..."
value={newMessage}
maxLength={MAX_MESSAGE_LENGTH}
onChange={(e) => setNewMessage(e.target.value)}
Expand Down
36 changes: 18 additions & 18 deletions frontend/src/app/components/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
if (response.ok) {
if (isSignUp) {
setIsSignUp(false);
setErrorMsg("Compte créé avec succès ! Connectez-vous.");
setErrorMsg("Account created! Please log in.");
return;
}

Expand All @@ -55,11 +55,11 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)

if (onLogin) onLogin();
} else {
setErrorMsg(data.message || "Erreur lors de la connexion");
setErrorMsg(data.message || "Error while connecting");
}
} catch (error) {
console.error("Network Error:", error);
setErrorMsg("Impossible de contacter le serveur NestJS.");
setErrorMsg("Impossible to reach NestJS server.");
}
};

Expand All @@ -83,8 +83,8 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
</h1>
<p className="text-gray-600">
{is2FARequired
? "Vérification en deux étapes"
: (isSignUp ? "Créez votre compte pour commencer" : "Bienvenue ! Connectez-vous pour continuer")}
? "Two steps verification"
: (isSignUp ? "Create your account to start" : "Welcome ! Log in to continue")}
</p>
</div>

Expand All @@ -93,7 +93,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)

{/* Banner d'erreur dynamique */}
{errorMsg && (
<div className={`mb-6 p-4 rounded-xl border flex items-center gap-3 animate-in fade-in ${errorMsg.includes("succès") ? "bg-green-50 border-green-200 text-green-600" : "bg-red-50 border-red-200 text-red-600"}`}>
<div className={`mb-6 p-4 rounded-xl border flex items-center gap-3 animate-in fade-in ${errorMsg.includes("created") ? "bg-green-50 border-green-200 text-green-600" : "bg-red-50 border-red-200 text-red-600"}`}>
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<span className="text-sm">{errorMsg}</span>
</div>
Expand All @@ -103,7 +103,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)

{is2FARequired ? (
<div>
<label className="block mb-2 text-gray-700 font-medium text-sm">Code d'authentification (OTP)</label>
<label className="block mb-2 text-gray-700 font-medium text-sm">Auth Code(OTP)</label>
<div className="relative">
<Key className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
Expand All @@ -121,14 +121,14 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
<>
{isSignUp && (
<div>
<label className="block mb-2 text-gray-700 font-medium text-sm">Nom d'utilisateur</label>
<label className="block mb-2 text-gray-700 font-medium text-sm">Username</label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Votre pseudo"
placeholder="Your username"
className="w-full pl-12 pr-4 py-3 rounded-xl bg-gray-50 border-2 border-gray-100 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200 transition-all"
required
/>
Expand All @@ -144,15 +144,15 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="votre@email.com"
placeholder="your@email.com"
className="w-full pl-12 pr-4 py-3 rounded-xl bg-gray-50 border-2 border-gray-100 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200 transition-all"
required
/>
</div>
</div>

<div>
<label className="block mb-2 text-gray-700 font-medium text-sm">Mot de passe</label>
<label className="block mb-2 text-gray-700 font-medium text-sm">Password</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
Expand All @@ -171,7 +171,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
{!isSignUp && !is2FARequired && (
<div className="flex justify-end">
<button type="button" className="text-sm text-purple-600 hover:text-purple-700 font-medium transition-colors">
Mot de passe oublié ?
Forgotten password ?
</button>
</div>
)}
Expand All @@ -180,7 +180,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
type="submit"
className="w-full py-4 rounded-xl bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-bold shadow-lg hover:shadow-xl hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition-all transform hover:-translate-y-0.5 mt-2"
>
{is2FARequired ? "Vérifier le code" : (isSignUp ? "Créer mon compte" : "Se connecter")}
{is2FARequired ? "Verify the code" : (isSignUp ? "Create account" : "Log in")}
</button>
</form>

Expand All @@ -191,7 +191,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
<div className="w-full border-t border-gray-200"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-white text-gray-400 font-medium">ou continuer avec</span>
<span className="px-4 bg-white text-gray-400 font-medium">or continue with</span>
</div>
</div>

Expand All @@ -201,14 +201,14 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center font-bold shadow-sm">
<span className="bg-gradient-to-r from-cyan-500 to-blue-600 bg-clip-text text-transparent">42</span>
</div>
<span>Se connecter avec l'intra 42</span>
<span>Log in with 42 intra</span>
</a>

<a href="/api/auth/github"
className="w-full flex items-center justify-center gap-3 py-4 px-6 rounded-xl bg-gradient-to-r from-gray-800 to-gray-900 hover:from-gray-900 hover:to-black text-decoration-none text-white font-semibold shadow-lg hover:shadow-xl transition-all transform hover:-translate-y-0.5 mt-4"
>
<Github className="w-5 h-5" />
<span>Se connecter avec GitHub</span>
<span>Log in with GitHub</span>
</a>

<div className="mt-6 text-center">
Expand All @@ -221,9 +221,9 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
className="text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors"
>
{isSignUp ? (
<>Vous avez déjà un compte ? <span className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent font-bold underline">Se connecter</span></>
<>Already have an account ? <span className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent font-bold underline">Log in</span></>
) : (
<>Pas encore de compte ? <span className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent font-bold underline">S'inscrire</span></>
<>No account yet ? <span className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent font-bold underline">Sign in</span></>
)}
</button>
</div>
Expand Down
Loading