Autosave: 20260405-222200

This commit is contained in:
Flatlogic Bot 2026-04-05 22:22:00 +00:00
parent db8313c88e
commit 3b03cdb4a1
34 changed files with 3449 additions and 509 deletions

View File

@ -1,41 +1,31 @@
RJLResaka - Dynamic Web Project (Tomcat 9)
RJLResaka - version finale Java JEE
===================================
Objectif:
- Application web de discussion privée style Facebook Messenger
- Java JEE (Servlet/JSP) + MySQL Workbench
- Projet Dynamic Web Project sans Maven
1) Ouvrir MySQL Workbench puis exécuter le script: database/rjlresaka.sql
2) Dans Eclipse EE, importer le dossier RJLResaka comme Dynamic Web Project
3) Ajouter dans WEB-INF/lib les JAR suivants:
- mysql-connector-j-8.x.x.jar
- jstl-1.2.jar
- standard-1.1.2.jar (si votre distribution JSTL l'exige)
- jbcrypt-0.4.jar
4) Vérifier WebContent/WEB-INF/web.xml:
- db.url
- db.user
- db.password
5) Déployer sur Tomcat 9
6) Ouvrir /RJLResaka/home
Structure actuellement livrée:
- src/com/rjlresaka/model -> modèles Java
- src/com/rjlresaka/dao -> accès MySQL (users + reset password)
- src/com/rjlresaka/servlet -> Home, Register, Login, ForgotPassword, ResetPassword, Dashboard, Logout
- src/com/rjlresaka/filter -> filtre UTF-8 + protection des pages privées
- src/com/rjlresaka/util -> connexion MySQL, BCrypt, génération de token
- WebContent/WEB-INF/views -> JSP protégées
- WebContent/assets -> design moderne clair responsive
- database/rjlresaka.sql -> schéma MySQL complet prêt pour Workbench
Fonctionnalités livrées:
- Inscription / connexion
- Mot de passe oublié / réinitialisation par token
- Liste des utilisateurs
- Chat privé
- Modifier / supprimer un message
- Upload image / fichier
- Téléchargement des pièces jointes
- Réactions emoji simples
- Interface claire type Facebook / Messenger
JARs à placer dans WebContent/WEB-INF/lib:
- jbcrypt-0.4.jar
- mysql-connector-java-8.0.18.jar
- javax.mail-api-1.6.2.jar
- jstl-1.2.jar
Fonctionnalités déjà codées dans cette étape:
1. Page d'accueil moderne
2. Inscription
3. Connexion
4. Mot de passe oublié (génération de token)
5. Réinitialisation du mot de passe
6. Tableau de bord avec liste des autres utilisateurs
Configuration MySQL à adapter dans WebContent/WEB-INF/web.xml:
- db.url
- db.user
- db.password
Étapes suivantes recommandées:
1. Vraies conversations privées 1-1
2. Envoi / modification / suppression de messages
3. Upload image/fichier + téléchargement
4. Emojis, réactions, badge non lus
Remarque logo:
- Le fichier assets/img/logo_resaka.png est un logo provisoire généré dans le projet.
- Si vous avez votre vrai logo, remplacez simplement ce fichier par votre version.

View File

@ -12,39 +12,24 @@
<link rel="stylesheet" href="${pageContext.request.contextPath}/assets/css/app.css">
</head>
<body class="auth-body">
<main class="auth-shell">
<main class="auth-shell branded">
<section class="auth-panel glass">
<div class="auth-brand">
<img src="${pageContext.request.contextPath}/assets/img/logo_resaka.png" alt="Logo RJLResaka" width="56" height="56">
<div><strong>RJLResaka</strong><span>Réinitialisation</span></div>
</div>
<a class="ghost-link" href="${pageContext.request.contextPath}/login">← Retour</a>
<span class="hero-badge">Réinitialisation</span>
<h1>Mot de passe oublié</h1>
<p class="auth-copy">Entrez votre email. Pour l'instant, le lien est affiché à l'écran; ensuite vous pourrez brancher JavaMail.</p>
<% if (request.getAttribute("error") != null) { %>
<div class="alert error"><%= request.getAttribute("error") %></div>
<% } %>
<% if (request.getAttribute("success") != null) { %>
<div class="alert success"><%= request.getAttribute("success") %></div>
<% } %>
<% if (request.getAttribute("error") != null) { %><div class="alert error"><%= request.getAttribute("error") %></div><% } %>
<% if (request.getAttribute("success") != null) { %><div class="alert success"><%= request.getAttribute("success") %></div><% } %>
<form action="${pageContext.request.contextPath}/forgot-password" method="post" class="stack-form">
<label>
<span>Email</span>
<input type="email" name="email" required>
</label>
<label><span>Email</span><input type="email" name="email" required></label>
<button class="button primary" type="submit">Générer le lien</button>
</form>
<% if (request.getAttribute("resetLink") != null) { %>
<div class="token-box">
<strong>Lien généré :</strong><br>
<span><%= request.getAttribute("resetLink") %></span><br><br>
<strong>Token :</strong> <%= request.getAttribute("generatedToken") %>
</div>
<% } %>
<% if (request.getAttribute("debugMessage") != null) { %>
<p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p>
<% } %>
<% if (request.getAttribute("resetLink") != null) { %><div class="token-box"><strong>Lien généré :</strong><br><span><%= request.getAttribute("resetLink") %></span><br><br><strong>Token :</strong> <%= request.getAttribute("generatedToken") %></div><% } %>
<% if (request.getAttribute("debugMessage") != null) { %><p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p><% } %>
</section>
</main>
<script src="${pageContext.request.contextPath}/assets/js/app.js"></script>

View File

@ -12,40 +12,28 @@
<link rel="stylesheet" href="${pageContext.request.contextPath}/assets/css/app.css">
</head>
<body class="auth-body">
<main class="auth-shell">
<main class="auth-shell branded">
<section class="auth-panel glass">
<div class="auth-brand">
<img src="${pageContext.request.contextPath}/assets/img/logo_resaka.png" alt="Logo RJLResaka" width="56" height="56">
<div>
<strong>RJLResaka</strong>
<span>Connexion sécurisée</span>
</div>
</div>
<a class="ghost-link" href="${pageContext.request.contextPath}/home">← Retour</a>
<span class="hero-badge">Connexion</span>
<h1>Bon retour sur RJLResaka</h1>
<p class="auth-copy">Utilisez votre email ou votre nom d'utilisateur pour continuer.</p>
<% if (request.getAttribute("error") != null) { %>
<div class="alert error"><%= request.getAttribute("error") %></div>
<% } %>
<% if (request.getAttribute("success") != null) { %>
<div class="alert success"><%= request.getAttribute("success") %></div>
<% } %>
<% if (request.getAttribute("error") != null) { %><div class="alert error"><%= request.getAttribute("error") %></div><% } %>
<% if (request.getAttribute("success") != null) { %><div class="alert success"><%= request.getAttribute("success") %></div><% } %>
<form action="${pageContext.request.contextPath}/login" method="post" class="stack-form">
<label>
<span>Email ou username</span>
<input type="text" name="identity" placeholder="ex: demo@rjlresaka.app" required>
</label>
<label>
<span>Mot de passe</span>
<input type="password" name="password" placeholder="••••••••" required>
</label>
<label><span>Email ou username</span><input type="text" name="identity" placeholder="ex: demo@rjlresaka.app" required></label>
<label><span>Mot de passe</span><input type="password" name="password" placeholder="••••••••" required></label>
<button class="button primary" type="submit">Se connecter</button>
</form>
<div class="auth-links">
<a href="${pageContext.request.contextPath}/forgot-password">Mot de passe oublié ?</a>
<a href="${pageContext.request.contextPath}/register">Créer un compte</a>
</div>
<% if (request.getAttribute("debugMessage") != null) { %>
<p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p>
<% } %>
<div class="auth-links"><a href="${pageContext.request.contextPath}/forgot-password">Mot de passe oublié ?</a><a href="${pageContext.request.contextPath}/register">Créer un compte</a></div>
<% if (request.getAttribute("debugMessage") != null) { %><p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p><% } %>
</section>
</main>
<script src="${pageContext.request.contextPath}/assets/js/app.js"></script>

View File

@ -12,50 +12,27 @@
<link rel="stylesheet" href="${pageContext.request.contextPath}/assets/css/app.css">
</head>
<body class="auth-body">
<main class="auth-shell">
<main class="auth-shell branded">
<section class="auth-panel wide glass">
<div class="auth-brand">
<img src="${pageContext.request.contextPath}/assets/img/logo_resaka.png" alt="Logo RJLResaka" width="56" height="56">
<div><strong>RJLResaka</strong><span>Créer un compte</span></div>
</div>
<a class="ghost-link" href="${pageContext.request.contextPath}/home">← Retour</a>
<span class="hero-badge">Inscription</span>
<h1>Créez votre compte</h1>
<p class="auth-copy">Une seule inscription suffit pour commencer vos discussions privées.</p>
<% if (request.getAttribute("error") != null) { %>
<div class="alert error"><%= request.getAttribute("error") %></div>
<% } %>
<% if (request.getAttribute("error") != null) { %><div class="alert error"><%= request.getAttribute("error") %></div><% } %>
<form action="${pageContext.request.contextPath}/register" method="post" class="stack-form two-col">
<label>
<span>Nom complet</span>
<input type="text" name="fullName" required>
</label>
<label>
<span>Nom d'utilisateur</span>
<input type="text" name="username" required>
</label>
<label>
<span>Email</span>
<input type="email" name="email" required>
</label>
<label>
<span>Mot de passe</span>
<input type="password" name="password" required>
</label>
<label>
<span>Confirmer le mot de passe</span>
<input type="password" name="confirmPassword" required>
</label>
<div class="button-row">
<button class="button primary" type="submit">Créer le compte</button>
</div>
<label><span>Nom complet</span><input type="text" name="fullName" required></label>
<label><span>Nom d'utilisateur</span><input type="text" name="username" required></label>
<label><span>Email</span><input type="email" name="email" required></label>
<label><span>Mot de passe</span><input type="password" name="password" required></label>
<label><span>Confirmer le mot de passe</span><input type="password" name="confirmPassword" required></label>
<div class="button-row"><button class="button primary" type="submit">Créer le compte</button></div>
</form>
<div class="auth-links left">
<a href="${pageContext.request.contextPath}/login">J'ai déjà un compte</a>
</div>
<% if (request.getAttribute("debugMessage") != null) { %>
<p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p>
<% } %>
<div class="auth-links left"><a href="${pageContext.request.contextPath}/login">J'ai déjà un compte</a></div>
<% if (request.getAttribute("debugMessage") != null) { %><p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p><% } %>
</section>
</main>
<script src="${pageContext.request.contextPath}/assets/js/app.js"></script>

View File

@ -1,10 +1,5 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%
String token = request.getParameter("token");
if (request.getAttribute("token") != null) {
token = (String) request.getAttribute("token");
}
%>
<% String token = request.getParameter("token"); if (request.getAttribute("token") != null) { token = (String) request.getAttribute("token"); } %>
<!DOCTYPE html>
<html lang="fr">
<head>
@ -18,36 +13,24 @@
<link rel="stylesheet" href="${pageContext.request.contextPath}/assets/css/app.css">
</head>
<body class="auth-body">
<main class="auth-shell">
<main class="auth-shell branded">
<section class="auth-panel glass">
<div class="auth-brand">
<img src="${pageContext.request.contextPath}/assets/img/logo_resaka.png" alt="Logo RJLResaka" width="56" height="56">
<div><strong>RJLResaka</strong><span>Nouveau mot de passe</span></div>
</div>
<a class="ghost-link" href="${pageContext.request.contextPath}/login">← Retour</a>
<span class="hero-badge">Nouveau mot de passe</span>
<h1>Réinitialisez votre mot de passe</h1>
<p class="auth-copy">Collez le token reçu ou ouvrez directement le lien généré.</p>
<% if (request.getAttribute("error") != null) { %>
<div class="alert error"><%= request.getAttribute("error") %></div>
<% } %>
<% if (request.getAttribute("error") != null) { %><div class="alert error"><%= request.getAttribute("error") %></div><% } %>
<form action="${pageContext.request.contextPath}/reset-password" method="post" class="stack-form">
<label>
<span>Token</span>
<input type="text" name="token" value="<%= token == null ? "" : token %>" required>
</label>
<label>
<span>Nouveau mot de passe</span>
<input type="password" name="password" required>
</label>
<label>
<span>Confirmer le mot de passe</span>
<input type="password" name="confirmPassword" required>
</label>
<label><span>Token</span><input type="text" name="token" value="<%= token == null ? "" : token %>" required></label>
<label><span>Nouveau mot de passe</span><input type="password" name="password" required></label>
<label><span>Confirmer le mot de passe</span><input type="password" name="confirmPassword" required></label>
<button class="button primary" type="submit">Mettre à jour</button>
</form>
<% if (request.getAttribute("debugMessage") != null) { %>
<p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p>
<% } %>
<% if (request.getAttribute("debugMessage") != null) { %><p class="debug-note">Détail technique: <%= request.getAttribute("debugMessage") %></p><% } %>
</section>
</main>
<script src="${pageContext.request.contextPath}/assets/js/app.js"></script>

View File

@ -1,76 +1,374 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tableau de bord | RJLResaka</title>
<meta name="description" content="Tableau de bord RJLResaka avec liste des utilisateurs disponibles pour chatter.">
<title>Messenger Facebook Style | RJLResaka</title>
<meta name="description" content="RJLResaka : discussions privées, groupes, demandes d'amis, réactions et fichiers avec un design clair inspiré de Facebook Messenger.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="${pageContext.request.contextPath}/assets/css/app.css">
</head>
<body class="dashboard-body">
<main class="dashboard-shell">
<section class="sidebar glass">
<div class="sidebar-top">
<span class="hero-badge">Connecté</span>
<a class="ghost-link" href="${pageContext.request.contextPath}/logout">Déconnexion</a>
</div>
<div class="profile-card">
<div class="avatar" style="background:${sessionScope.authUser.avatarColor}">${sessionScope.authUser.initials}</div>
<div>
<h1>${sessionScope.authUser.fullName}</h1>
<p>@${sessionScope.authUser.username}</p>
<small>${sessionScope.authUser.email}</small>
<body class="dashboard-page fb-body">
<main class="fb-dashboard-layout">
<aside class="fb-left-column">
<section class="fb-card profile-card facebook-card">
<div class="fb-profile-row">
<div class="avatar xl" style="background:${sessionScope.authUser.avatarColor}">${sessionScope.authUser.initials}</div>
<div>
<p class="eyebrow">Compte connecté</p>
<h1>${sessionScope.authUser.fullName}</h1>
<p class="muted">@${sessionScope.authUser.username}</p>
<small>${sessionScope.authUser.email}</small>
</div>
</div>
</div>
<div class="panel-note">
<strong>Étape livrée :</strong>
Authentification complète + liste des utilisateurs.<br>
La prochaine étape branchera les conversations privées, l'upload d'images/fichiers et les réactions.
</div>
</section>
<section class="content-panel glass">
<div class="content-head">
<div>
<span class="section-kicker">Utilisateurs</span>
<h2>Choisissez une personne à qui parler</h2>
<div class="stats-row">
<div>
<strong>${friendCount}</strong>
<span>Amis</span>
</div>
<div>
<strong>${requestCount}</strong>
<span>Demandes</span>
</div>
<div>
<strong>${fn:length(conversations)}</strong>
<span>Discussions</span>
</div>
</div>
<span class="pill">${fn:length(users)} compte(s)</span>
</div>
<a class="ghost-link strong" href="${pageContext.request.contextPath}/logout">Déconnexion</a>
</section>
<section class="fb-card facebook-card">
<div class="section-heading">
<div>
<p class="eyebrow">Créer un groupe</p>
<h2>Style Messenger</h2>
</div>
<span class="pill soft">${groupCandidateCount} ami(s)</span>
</div>
<form action="${pageContext.request.contextPath}/app/groups/create" method="post" class="stack-form compact-form">
<label>
<span>Nom du groupe</span>
<input type="text" name="groupName" placeholder="Ex: Projet final GL">
</label>
<div class="checkbox-grid">
<c:forEach items="${friends}" var="friend">
<label class="check-chip">
<input type="checkbox" name="memberIds" value="${friend.id}">
<span>${friend.fullName}</span>
</label>
</c:forEach>
<c:if test="${empty friends}">
<p class="empty-mini">Ajoutez d'abord des amis pour créer un groupe.</p>
</c:if>
</div>
<button class="button primary full" type="submit">Créer le groupe</button>
</form>
</section>
<section class="fb-card facebook-card">
<div class="section-heading">
<div>
<p class="eyebrow">Demandes reçues</p>
<h2>Amis</h2>
</div>
</div>
<div class="request-list">
<c:forEach items="${pendingRequests}" var="invite">
<article class="friend-row">
<div class="avatar large" style="background:${invite.senderAvatarColor}">${invite.senderInitials}</div>
<div class="friend-copy">
<strong>${invite.senderName}</strong>
<small>@${invite.senderUsername}</small>
</div>
<div class="friend-actions split">
<form action="${pageContext.request.contextPath}/app/friends/respond" method="post">
<input type="hidden" name="requestId" value="${invite.id}">
<input type="hidden" name="action" value="accept">
<button class="button tiny primary" type="submit">Accepter</button>
</form>
<form action="${pageContext.request.contextPath}/app/friends/respond" method="post">
<input type="hidden" name="requestId" value="${invite.id}">
<input type="hidden" name="action" value="decline">
<button class="button tiny secondary" type="submit">Refuser</button>
</form>
</div>
</article>
</c:forEach>
<c:if test="${empty pendingRequests}">
<p class="empty-mini">Aucune demande pour le moment.</p>
</c:if>
</div>
</section>
</aside>
<section class="fb-center-column">
<c:if test="${not empty success}">
<div class="alert success floating">${success}</div>
</c:if>
<c:if test="${not empty error}">
<div class="alert error">${error}</div>
<div class="alert error floating">${error}</div>
</c:if>
<div class="user-grid">
<c:forEach items="${users}" var="item">
<article class="user-card">
<div class="avatar large" style="background:${item.avatarColor}">${item.initials}</div>
<h3>${item.fullName}</h3>
<p>@${item.username}</p>
<small>${item.email}</small>
<button class="button secondary" type="button" disabled>Chat disponible à l'étape suivante</button>
</article>
</c:forEach>
<c:choose>
<c:when test="${not empty activeConversation}">
<section class="fb-card messenger-shell-card">
<header class="chat-topbar fb-chat-topbar">
<div class="chat-partner">
<div class="avatar xl" style="background:${activeConversation.avatarColor}">${activeConversation.initials}</div>
<div>
<div class="chat-title-row">
<h2>${activeConversation.title}</h2>
<c:if test="${activeConversation.group}">
<span class="pill">Groupe</span>
</c:if>
<c:if test="${not activeConversation.group}">
<span class="pill soft">Privé</span>
</c:if>
</div>
<p>${activeConversation.subtitle}</p>
</div>
</div>
<div class="chat-top-actions">
<span class="pill soft">Thème clair Facebook</span>
<span class="pill soft">Fichiers + réactions</span>
</div>
</header>
<c:if test="${empty users}">
<article class="empty-card">
<h3>Aucun autre utilisateur pour le moment</h3>
<p>Créez un deuxième compte pour tester le chat privé ensuite.</p>
</article>
</c:if>
</div>
<section class="messages-stream fb-stream" id="messageStream">
<c:if test="${empty messages}">
<article class="empty-chat large-empty">
<div class="empty-3d-shape"></div>
<h3>Conversation prête</h3>
<p>Envoyez votre premier message dans ${activeConversation.title}.</p>
</article>
</c:if>
<c:forEach items="${messages}" var="message">
<article class="message-row ${message.mine ? 'mine' : 'other'}">
<c:if test="${not message.mine}">
<div class="avatar small" style="background:${message.senderAvatarColor}">${message.senderInitials}</div>
</c:if>
<div class="message-stack">
<div class="message-meta-top">
<c:if test="${activeConversation.group && not message.mine}">
<strong>${message.senderName}</strong>
</c:if>
<small><fmt:formatDate value="${message.createdAt}" pattern="dd/MM HH:mm"/></small>
</div>
<div class="message-bubble ${message.mine ? 'mine' : 'other'} ${message.deleted ? 'deleted' : ''}">
<c:choose>
<c:when test="${message.deleted}">
<p class="deleted-copy">Ce message a été supprimé.</p>
</c:when>
<c:otherwise>
<c:if test="${not empty message.body}">
<p class="message-text">${message.body}</p>
</c:if>
<c:if test="${not empty message.attachmentName}">
<a class="attachment-card" href="${pageContext.request.contextPath}/app/files/download?messageId=${message.id}">
<span>📎 ${message.attachmentName}</span>
<small>Télécharger</small>
</a>
</c:if>
<c:if test="${message.edited}">
<small class="edited-note">Modifié</small>
</c:if>
</c:otherwise>
</c:choose>
</div>
<div class="message-toolbar">
<div class="reactions-row">
<c:forEach items="${message.reactions}" var="reaction">
<form action="${pageContext.request.contextPath}/app/messages/react" method="post" class="inline-form">
<input type="hidden" name="conversationId" value="${activeConversation.id}">
<input type="hidden" name="messageId" value="${message.id}">
<input type="hidden" name="emoji" value="${reaction.emoji}">
<button class="reaction-pill ${reaction.reactedByCurrentUser ? 'active' : ''}" type="submit">${reaction.emoji} ${reaction.count}</button>
</form>
</c:forEach>
<form action="${pageContext.request.contextPath}/app/messages/react" method="post" class="inline-form">
<input type="hidden" name="conversationId" value="${activeConversation.id}">
<input type="hidden" name="messageId" value="${message.id}">
<input type="hidden" name="emoji" value="👍">
<button class="emoji-trigger" type="submit">👍</button>
</form>
<form action="${pageContext.request.contextPath}/app/messages/react" method="post" class="inline-form">
<input type="hidden" name="conversationId" value="${activeConversation.id}">
<input type="hidden" name="messageId" value="${message.id}">
<input type="hidden" name="emoji" value="❤️">
<button class="emoji-trigger" type="submit">❤️</button>
</form>
<form action="${pageContext.request.contextPath}/app/messages/react" method="post" class="inline-form">
<input type="hidden" name="conversationId" value="${activeConversation.id}">
<input type="hidden" name="messageId" value="${message.id}">
<input type="hidden" name="emoji" value="😂">
<button class="emoji-trigger" type="submit">😂</button>
</form>
</div>
<c:if test="${message.mine && not message.deleted}">
<div class="message-actions">
<button class="ghost-link mini edit-toggle" type="button" data-target="edit-${message.id}">Modifier</button>
<form action="${pageContext.request.contextPath}/app/messages/delete" method="post" class="inline-form">
<input type="hidden" name="conversationId" value="${activeConversation.id}">
<input type="hidden" name="messageId" value="${message.id}">
<button class="ghost-link danger mini" type="submit">Supprimer</button>
</form>
</div>
</c:if>
</div>
<c:if test="${message.mine && not message.deleted}">
<form id="edit-${message.id}" action="${pageContext.request.contextPath}/app/messages/update" method="post" class="edit-form hidden">
<input type="hidden" name="conversationId" value="${activeConversation.id}">
<input type="hidden" name="messageId" value="${message.id}">
<textarea name="body" rows="3" required>${message.body}</textarea>
<div class="edit-actions">
<button class="button tiny primary" type="submit">Enregistrer</button>
<button class="button tiny secondary edit-cancel" type="button" data-target="edit-${message.id}">Annuler</button>
</div>
</form>
</c:if>
</div>
</article>
</c:forEach>
</section>
<form action="${pageContext.request.contextPath}/app/messages/send" method="post" enctype="multipart/form-data" class="composer fb-composer">
<input type="hidden" name="conversationId" value="${activeConversation.id}">
<textarea name="body" rows="3" placeholder="Écrivez un message comme sur Messenger..."></textarea>
<div class="composer-actions">
<label class="file-label">
<input type="file" name="attachment" accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip">
<span>Ajouter image ou fichier</span>
</label>
<button class="button primary" type="submit">Envoyer</button>
</div>
</form>
</section>
</c:when>
<c:otherwise>
<section class="fb-card empty-chat large-empty messenger-shell-card">
<div class="empty-3d-shape"></div>
<h2>Bienvenue sur votre Messenger</h2>
<p>Ajoutez des amis, acceptez des demandes ou créez un groupe pour commencer.</p>
</section>
</c:otherwise>
</c:choose>
<c:if test="${not empty debugMessage}">
<p class="debug-note">Détail technique: ${debugMessage}</p>
</c:if>
</section>
<aside class="fb-right-column">
<section class="fb-card facebook-card">
<div class="section-heading">
<div>
<p class="eyebrow">Discussions</p>
<h2>Conversations récentes</h2>
</div>
</div>
<div class="conversation-list">
<c:forEach items="${conversations}" var="conversation">
<a class="conversation-item ${conversation.active ? 'active' : ''}" href="${pageContext.request.contextPath}/app/dashboard?conversation=${conversation.id}">
<div class="avatar large" style="background:${conversation.avatarColor}">${conversation.initials}</div>
<div class="conversation-copy">
<div class="conversation-topline">
<strong>${conversation.title}</strong>
<c:if test="${conversation.lastMessageAt != null}">
<small><fmt:formatDate value="${conversation.lastMessageAt}" pattern="HH:mm"/></small>
</c:if>
</div>
<p>${conversation.lastMessagePreview}</p>
</div>
<c:if test="${conversation.unreadCount > 0}">
<span class="unread-badge">${conversation.unreadCount}</span>
</c:if>
</a>
</c:forEach>
<c:if test="${empty conversations}">
<p class="empty-mini">Aucune conversation pour le moment.</p>
</c:if>
</div>
</section>
<section class="fb-card facebook-card">
<div class="section-heading">
<div>
<p class="eyebrow">Découvrir</p>
<h2>Envoyer des demandes</h2>
</div>
</div>
<div class="request-list">
<c:forEach items="${people}" var="person">
<article class="friend-row stacked-mobile">
<div class="avatar large" style="background:${person.avatarColor}">${person.initials}</div>
<div class="friend-copy grow">
<strong>${person.fullName}</strong>
<small>@${person.username}</small>
<c:if test="${not empty person.bio}">
<p>${person.bio}</p>
</c:if>
</div>
<div class="friend-actions vertical">
<c:choose>
<c:when test="${person.requestReceived}">
<span class="status-chip waiting">Vous a envoyé une demande</span>
</c:when>
<c:when test="${person.requestSent}">
<span class="status-chip sent">Demande envoyée</span>
</c:when>
<c:otherwise>
<form action="${pageContext.request.contextPath}/app/friends/request" method="post">
<input type="hidden" name="receiverId" value="${person.id}">
<button class="button tiny secondary" type="submit">Ajouter</button>
</form>
</c:otherwise>
</c:choose>
</div>
</article>
</c:forEach>
<c:if test="${empty people}">
<p class="empty-mini">Tous les autres comptes sont déjà vos amis ou en attente.</p>
</c:if>
</div>
</section>
<section class="fb-card facebook-card">
<div class="section-heading">
<div>
<p class="eyebrow">Amis</p>
<h2>Lancer une discussion</h2>
</div>
</div>
<div class="request-list">
<c:forEach items="${friends}" var="friend">
<article class="friend-row">
<div class="avatar large" style="background:${friend.avatarColor}">${friend.initials}</div>
<div class="friend-copy">
<strong>${friend.fullName}</strong>
<small>@${friend.username}</small>
</div>
<a class="button tiny primary quick-open-form" href="${pageContext.request.contextPath}/app/dashboard?user=${friend.id}">Ouvrir</a>
</article>
</c:forEach>
<c:if test="${empty friends}">
<p class="empty-mini">Acceptez une demande ou ajoutez un ami pour ouvrir un chat privé.</p>
</c:if>
</div>
</section>
</aside>
</main>
<script src="${pageContext.request.contextPath}/assets/js/app.js"></script>
</body>

View File

@ -4,8 +4,8 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RJLResaka | Discussion privée moderne en Java JEE</title>
<meta name="description" content="RJLResaka est une application de discussion privée en Java JEE avec authentification, liste d'utilisateurs, chat et style moderne inspiré de Messenger.">
<title>RJLResaka | Chat privé et groupes style Facebook</title>
<meta name="description" content="RJLResaka est une application de discussion Java JEE inspirée de Facebook Messenger avec demandes d'amis, groupes, pièces jointes et réactions.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
@ -13,38 +13,55 @@
</head>
<body>
<main class="landing-shell">
<section class="landing-topbar">
<a class="brand" href="${pageContext.request.contextPath}/home">
<img src="${pageContext.request.contextPath}/assets/img/logo_resaka.png" alt="Logo RJLResaka" width="56" height="56">
<div>
<strong>RJLResaka</strong>
<span>Java JEE • Tomcat 9 • MySQL Workbench</span>
</div>
</a>
<div class="hero-actions compact">
<a class="button secondary" href="${pageContext.request.contextPath}/login">Connexion</a>
<a class="button primary" href="${pageContext.request.contextPath}/register">Créer un compte</a>
</div>
</section>
<section class="hero-card hero-grid">
<div>
<span class="hero-badge">RJLResaka • Java JEE</span>
<h1 class="hero-title">Discutez comme sur Messenger, dans votre propre application web.</h1>
<span class="hero-badge">Version finale Facebook-like</span>
<h1 class="hero-title">Une messagerie claire comme Facebook, avec amis, groupes et chat en temps réel côté UI.</h1>
<p class="hero-copy">
Inscription, connexion, mot de passe oublié, liste d'utilisateurs, discussion privée,
design clair et responsive. La prochaine étape ajoute les vraies conversations,
les pièces jointes et les réactions.
Votre projet final RJLResaka comprend maintenant l'inscription, la connexion, le mot de passe oublié,
les conversations privées, les groupes de discussion, les demandes d'amis, les réactions,
l'envoi de pièces jointes et une interface blanche inspirée de Facebook Messenger.
</p>
<div class="hero-actions">
<a class="button primary" href="${pageContext.request.contextPath}/register">Créer un compte</a>
<a class="button secondary" href="${pageContext.request.contextPath}/login">Se connecter</a>
<a class="button primary" href="${pageContext.request.contextPath}/register">Commencer</a>
<a class="button secondary" href="${pageContext.request.contextPath}/login">J'ai déjà un compte</a>
</div>
<ul class="hero-list">
<li>Tomcat 9 + Servlet/JSP</li>
<li>MySQL Workbench</li>
<li>Base prête pour chat privé + fichiers</li>
<li>Demandes d'amis et acceptation/refus</li>
<li>Création de groupes avec vos amis</li>
<li>Design clair moderne type Facebook/Messenger</li>
</ul>
</div>
<aside class="preview-card">
<div class="preview-window">
<aside class="preview-card messenger-preview">
<div class="preview-window fb-window">
<div class="preview-topbar">
<span></span><span></span><span></span>
</div>
<div class="preview-chat">
<div class="preview-bubble incoming">Salut 👋 prêt pour l'examen ?</div>
<div class="preview-bubble outgoing">Oui, RJLResaka avance bien !</div>
<div class="preview-users">
<div class="mini-user active">AM</div>
<div class="mini-user">JR</div>
<div class="mini-user">LK</div>
<div class="mini-user">TS</div>
<div class="preview-chat advanced">
<div class="preview-sidebar">
<div class="preview-contact active">AM</div>
<div class="preview-contact">JR</div>
<div class="preview-contact">GR</div>
</div>
<div class="preview-conversation">
<div class="preview-message incoming">Salut, tu as vu la demande d'ami ?</div>
<div class="preview-message outgoing">Oui 👍 et j'ai créé le groupe du projet.</div>
<div class="preview-attachment">📎 cahier-des-charges.pdf</div>
<div class="preview-reactions">❤️ 3&nbsp;&nbsp;😂 1</div>
</div>
</div>
</div>

View File

@ -22,6 +22,10 @@
<param-name>db.password</param-name>
<param-value></param-value>
</context-param>
<context-param>
<param-name>upload.dir</param-name>
<param-value>uploads</param-value>
</context-param>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,11 +1,36 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.alert').forEach(function (alert) {
setTimeout(function () {
var messageStream = document.getElementById('messageStream');
if (messageStream) {
messageStream.scrollTop = messageStream.scrollHeight;
}
var toggles = document.querySelectorAll('.edit-toggle, .edit-cancel');
toggles.forEach(function (button) {
button.addEventListener('click', function () {
var targetId = button.getAttribute('data-target');
var target = document.getElementById(targetId);
if (target) {
target.classList.toggle('hidden');
}
});
});
var fileInputs = document.querySelectorAll('.file-label input[type="file"]');
fileInputs.forEach(function (input) {
input.addEventListener('change', function () {
var label = input.parentElement.querySelector('span');
if (label && input.files && input.files.length > 0) {
label.textContent = input.files[0].name;
}
});
});
var floatingAlerts = document.querySelectorAll('.alert.floating');
floatingAlerts.forEach(function (alert) {
window.setTimeout(function () {
alert.style.opacity = '0';
alert.style.transform = 'translateY(-6px)';
setTimeout(function () {
alert.style.display = 'none';
}, 220);
}, 4200);
alert.style.transition = 'opacity 250ms ease, transform 250ms ease';
}, 3500);
});
});

View File

@ -1,6 +1,5 @@
-- RJLResaka MySQL schema
-- Compatible with MySQL Workbench / MariaDB
-- Create the database then import this file.
CREATE DATABASE IF NOT EXISTS rjlresaka
CHARACTER SET utf8mb4
@ -14,7 +13,7 @@ CREATE TABLE IF NOT EXISTS users (
username VARCHAR(60) NOT NULL UNIQUE,
email VARCHAR(120) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
avatar_color VARCHAR(20) NOT NULL DEFAULT '#0ea5e9',
avatar_color VARCHAR(20) NOT NULL DEFAULT '#1877f2',
bio VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
@ -34,14 +33,27 @@ CREATE TABLE IF NOT EXISTS password_reset_tokens (
CREATE TABLE IF NOT EXISTS conversations (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(120) NULL,
avatar_color VARCHAR(20) NOT NULL DEFAULT '#E7F3FF',
is_group TINYINT(1) NOT NULL DEFAULT 0,
created_by INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_conversation_creator
FOREIGN KEY (created_by) REFERENCES users(id)
ON DELETE SET NULL
);
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS name VARCHAR(120) NULL;
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS avatar_color VARCHAR(20) NOT NULL DEFAULT '#E7F3FF';
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS is_group TINYINT(1) NOT NULL DEFAULT 0;
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS created_by INT NULL;
CREATE TABLE IF NOT EXISTS conversation_participants (
id INT PRIMARY KEY AUTO_INCREMENT,
conversation_id INT NOT NULL,
user_id INT NOT NULL,
role VARCHAR(30) NOT NULL DEFAULT 'member',
joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_conv_part_conversation
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
@ -52,6 +64,8 @@ CREATE TABLE IF NOT EXISTS conversation_participants (
CONSTRAINT uq_conv_part UNIQUE (conversation_id, user_id)
);
ALTER TABLE conversation_participants ADD COLUMN IF NOT EXISTS role VARCHAR(30) NOT NULL DEFAULT 'member';
CREATE TABLE IF NOT EXISTS messages (
id INT PRIMARY KEY AUTO_INCREMENT,
conversation_id INT NOT NULL,
@ -89,6 +103,95 @@ CREATE TABLE IF NOT EXISTS message_reactions (
CONSTRAINT uq_message_reaction UNIQUE (message_id, user_id, emoji)
);
CREATE TABLE IF NOT EXISTS friend_requests (
id INT PRIMARY KEY AUTO_INCREMENT,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
responded_at DATETIME NULL,
CONSTRAINT fk_friend_request_sender
FOREIGN KEY (sender_id) REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT fk_friend_request_receiver
FOREIGN KEY (receiver_id) REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT uq_friend_request UNIQUE (sender_id, receiver_id)
);
CREATE TABLE IF NOT EXISTS friends (
id INT PRIMARY KEY AUTO_INCREMENT,
user_one_id INT NOT NULL,
user_two_id INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_friends_user_one
FOREIGN KEY (user_one_id) REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT fk_friends_user_two
FOREIGN KEY (user_two_id) REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT uq_friends_pair UNIQUE (user_one_id, user_two_id)
);
INSERT INTO users (full_name, username, email, password_hash, avatar_color, bio)
SELECT 'Demo User', 'demo', 'demo@rjlresaka.app', '$2a$10$u6N8G6s8wWC4b7A9iI5L8e2ZfQFlA95zT4zWS3TzFmpXQxwCLWv0W', '#2563eb', 'Compte de démonstration'
SELECT 'Demo User', 'demo', 'demo@rjlresaka.app', '$2a$10$u6N8G6s8wWC4b7A9iI5L8e2ZfQFlA95zT4zWS3TzFmpXQxwCLWv0W', '#1877f2', 'Compte de démonstration'
WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'demo@rjlresaka.app');
INSERT INTO users (full_name, username, email, password_hash, avatar_color, bio)
SELECT 'Alice Miora', 'alice', 'alice@rjlresaka.app', '$2a$10$u6N8G6s8wWC4b7A9iI5L8e2ZfQFlA95zT4zWS3TzFmpXQxwCLWv0W', '#42b72a', 'Étudiante L3 GL'
WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'alice@rjlresaka.app');
INSERT INTO users (full_name, username, email, password_hash, avatar_color, bio)
SELECT 'Junior Ranaivo', 'junior', 'junior@rjlresaka.app', '$2a$10$u6N8G6s8wWC4b7A9iI5L8e2ZfQFlA95zT4zWS3TzFmpXQxwCLWv0W', '#ff8a00', 'Compte de test pour la messagerie'
WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'junior@rjlresaka.app');
INSERT INTO friends (user_one_id, user_two_id)
SELECT LEAST(u1.id, u2.id), GREATEST(u1.id, u2.id)
FROM users u1, users u2
WHERE u1.email = 'demo@rjlresaka.app'
AND u2.email = 'alice@rjlresaka.app'
AND NOT EXISTS (
SELECT 1 FROM friends f
WHERE f.user_one_id = LEAST(u1.id, u2.id)
AND f.user_two_id = GREATEST(u1.id, u2.id)
);
INSERT INTO friend_requests (sender_id, receiver_id, status)
SELECT sender.id, receiver.id, 'pending'
FROM users sender, users receiver
WHERE sender.email = 'junior@rjlresaka.app'
AND receiver.email = 'demo@rjlresaka.app'
AND NOT EXISTS (
SELECT 1 FROM friend_requests fr
WHERE fr.sender_id = sender.id AND fr.receiver_id = receiver.id
);
INSERT INTO conversations (name, avatar_color, is_group, created_by)
SELECT 'Groupe Projet Final', '#E7F3FF', 1, owner.id
FROM users owner
WHERE owner.email = 'demo@rjlresaka.app'
AND NOT EXISTS (
SELECT 1 FROM conversations c WHERE c.name = 'Groupe Projet Final' AND c.is_group = 1
);
INSERT INTO conversation_participants (conversation_id, user_id, role)
SELECT c.id, u.id,
CASE WHEN u.email = 'demo@rjlresaka.app' THEN 'admin' ELSE 'member' END
FROM conversations c
JOIN users u ON u.email IN ('demo@rjlresaka.app', 'alice@rjlresaka.app', 'junior@rjlresaka.app')
WHERE c.name = 'Groupe Projet Final' AND c.is_group = 1
AND NOT EXISTS (
SELECT 1 FROM conversation_participants cp
WHERE cp.conversation_id = c.id AND cp.user_id = u.id
);
INSERT INTO messages (conversation_id, sender_id, body)
SELECT c.id, owner.id, 'Bienvenue dans le groupe du projet final RJLResaka !'
FROM conversations c
JOIN users owner ON owner.email = 'demo@rjlresaka.app'
WHERE c.name = 'Groupe Projet Final' AND c.is_group = 1
AND NOT EXISTS (
SELECT 1 FROM messages m WHERE m.conversation_id = c.id AND m.sender_id = owner.id
);
-- Mot de passe démo attendu: demo123

View File

@ -1,15 +1,17 @@
Placez ces fichiers dans: RJLResaka/WebContent/WEB-INF/lib
Bibliothèques à placer dans WEB-INF/lib
======================================
Obligatoires pour l'étape actuelle:
- jbcrypt-0.4.jar
- mysql-connector-java-8.0.18.jar
Obligatoires:
- mysql-connector-j-8.x.x.jar
- jstl-1.2.jar
- jbcrypt-0.4.jar
Pour la future étape email:
- javax.mail-api-1.6.2.jar
Selon votre pack Eclipse/Tomcat:
- standard-1.1.2.jar
Conseils Eclipse / Tomcat 9:
1. Right click project -> Properties -> Java Build Path -> Libraries
2. Vérifiez que WEB-INF/lib est bien dans Deployment Assembly
3. Importez database/rjlresaka.sql dans MySQL Workbench
4. Modifiez web.xml si votre mot de passe MySQL n'est pas vide
Déjà pris en charge par Tomcat 9:
- servlet-api (ne pas ajouter si déjà fourni par Tomcat)
Notes:
- L'upload utilise l'API native Part de Servlet 3+, donc commons-fileupload n'est pas obligatoire.
- Les fichiers chargés sont stockés côté serveur; seul leur chemin est enregistré en base.

View File

@ -0,0 +1,299 @@
package com.rjlresaka.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletContext;
import com.rjlresaka.model.ConversationItem;
import com.rjlresaka.model.User;
import com.rjlresaka.util.DatabaseConnection;
public class ConversationDAO {
public Integer findConversationIdBetweenUsers(int firstUserId, int secondUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "SELECT c.id "
+ "FROM conversations c "
+ "INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = ? "
+ "INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = ? "
+ "WHERE c.is_group = 0 LIMIT 1";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, firstUserId);
statement.setInt(2, secondUserId);
try (ResultSet rs = statement.executeQuery()) {
return rs.next() ? Integer.valueOf(rs.getInt("id")) : null;
}
}
}
public int findOrCreateConversationId(int firstUserId, int secondUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
Integer existingConversationId = findConversationIdBetweenUsers(firstUserId, secondUserId, context);
if (existingConversationId != null) {
return existingConversationId.intValue();
}
String insertConversation = "INSERT INTO conversations (name, avatar_color, is_group, created_by) VALUES (NULL, '#E7F3FF', 0, ?)";
String insertParticipant = "INSERT INTO conversation_participants (conversation_id, user_id, role) VALUES (?, ?, ?)";
try (Connection connection = DatabaseConnection.getConnection(context)) {
connection.setAutoCommit(false);
try (PreparedStatement conversationStatement = connection.prepareStatement(insertConversation,
Statement.RETURN_GENERATED_KEYS);
PreparedStatement participantStatement = connection.prepareStatement(insertParticipant)) {
conversationStatement.setInt(1, firstUserId);
conversationStatement.executeUpdate();
int conversationId = 0;
try (ResultSet keys = conversationStatement.getGeneratedKeys()) {
if (keys.next()) {
conversationId = keys.getInt(1);
}
}
participantStatement.setInt(1, conversationId);
participantStatement.setInt(2, firstUserId);
participantStatement.setString(3, "admin");
participantStatement.executeUpdate();
participantStatement.setInt(1, conversationId);
participantStatement.setInt(2, secondUserId);
participantStatement.setString(3, "member");
participantStatement.executeUpdate();
connection.commit();
return conversationId;
} catch (SQLException exception) {
connection.rollback();
throw exception;
} finally {
connection.setAutoCommit(true);
}
}
}
public int createGroupConversation(int currentUserId, String name, List<Integer> memberIds, ServletContext context)
throws SQLException, ClassNotFoundException {
String groupName = cleanText(name);
if (groupName == null) {
groupName = "Nouveau groupe";
}
Set<Integer> uniqueMembers = new LinkedHashSet<Integer>();
uniqueMembers.add(Integer.valueOf(currentUserId));
if (memberIds != null) {
for (Integer memberId : memberIds) {
if (memberId != null && memberId.intValue() > 0) {
uniqueMembers.add(memberId);
}
}
}
String insertConversation = "INSERT INTO conversations (name, avatar_color, is_group, created_by) VALUES (?, '#E7F3FF', 1, ?)";
String insertParticipant = "INSERT INTO conversation_participants (conversation_id, user_id, role) VALUES (?, ?, ?)";
try (Connection connection = DatabaseConnection.getConnection(context)) {
connection.setAutoCommit(false);
try (PreparedStatement conversationStatement = connection.prepareStatement(insertConversation,
Statement.RETURN_GENERATED_KEYS);
PreparedStatement participantStatement = connection.prepareStatement(insertParticipant)) {
conversationStatement.setString(1, groupName);
conversationStatement.setInt(2, currentUserId);
conversationStatement.executeUpdate();
int conversationId = 0;
try (ResultSet keys = conversationStatement.getGeneratedKeys()) {
if (keys.next()) {
conversationId = keys.getInt(1);
}
}
for (Integer memberId : uniqueMembers) {
participantStatement.setInt(1, conversationId);
participantStatement.setInt(2, memberId.intValue());
participantStatement.setString(3, memberId.intValue() == currentUserId ? "admin" : "member");
participantStatement.executeUpdate();
}
connection.commit();
return conversationId;
} catch (SQLException exception) {
connection.rollback();
throw exception;
} finally {
connection.setAutoCommit(true);
}
}
}
public List<ConversationItem> findConversationsForUser(int currentUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "SELECT c.id, c.name, c.avatar_color, c.is_group, c.updated_at, "
+ "other.id AS direct_user_id, other.full_name AS direct_name, other.username AS direct_username, "
+ "other.avatar_color AS direct_avatar_color, "
+ "(SELECT COUNT(*) FROM conversation_participants count_cp WHERE count_cp.conversation_id = c.id) AS member_count "
+ "FROM conversations c "
+ "INNER JOIN conversation_participants mine ON mine.conversation_id = c.id AND mine.user_id = ? "
+ "LEFT JOIN conversation_participants other_cp ON c.is_group = 0 AND other_cp.conversation_id = c.id AND other_cp.user_id <> ? "
+ "LEFT JOIN users other ON other.id = other_cp.user_id "
+ "ORDER BY c.updated_at DESC, c.created_at DESC";
List<ConversationItem> conversations = new ArrayList<ConversationItem>();
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
statement.setInt(2, currentUserId);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
ConversationItem item = mapConversationBase(rs);
enrichConversationSnapshot(connection, item, currentUserId);
conversations.add(item);
}
}
}
return conversations;
}
public ConversationItem findConversationForUser(int conversationId, int currentUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "SELECT c.id, c.name, c.avatar_color, c.is_group, c.updated_at, "
+ "other.id AS direct_user_id, other.full_name AS direct_name, other.username AS direct_username, "
+ "other.avatar_color AS direct_avatar_color, "
+ "(SELECT COUNT(*) FROM conversation_participants count_cp WHERE count_cp.conversation_id = c.id) AS member_count "
+ "FROM conversations c "
+ "INNER JOIN conversation_participants mine ON mine.conversation_id = c.id AND mine.user_id = ? "
+ "LEFT JOIN conversation_participants other_cp ON c.is_group = 0 AND other_cp.conversation_id = c.id AND other_cp.user_id <> ? "
+ "LEFT JOIN users other ON other.id = other_cp.user_id "
+ "WHERE c.id = ? LIMIT 1";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
statement.setInt(2, currentUserId);
statement.setInt(3, conversationId);
try (ResultSet rs = statement.executeQuery()) {
if (!rs.next()) {
return null;
}
ConversationItem item = mapConversationBase(rs);
enrichConversationSnapshot(connection, item, currentUserId);
return item;
}
}
}
public void enrichUsersWithConversationMeta(int currentUserId, List<User> users, ServletContext context)
throws SQLException, ClassNotFoundException {
for (User user : users) {
Integer conversationId = findConversationIdBetweenUsers(currentUserId, user.getId(), context);
user.setConversationId(conversationId);
if (conversationId != null) {
ConversationItem snapshot = findConversationForUser(conversationId.intValue(), currentUserId, context);
if (snapshot != null) {
user.setLastMessagePreview(snapshot.getLastMessagePreview());
user.setLastMessageAt(snapshot.getLastMessageAt());
user.setUnreadCount(snapshot.getUnreadCount());
}
} else {
user.setLastMessagePreview("Commencez la discussion");
}
}
}
public void markConversationAsSeen(int conversationId, int currentUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "UPDATE messages SET seen_at = NOW() WHERE conversation_id = ? AND sender_id <> ? AND seen_at IS NULL";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, conversationId);
statement.setInt(2, currentUserId);
statement.executeUpdate();
}
}
private ConversationItem mapConversationBase(ResultSet rs) throws SQLException {
ConversationItem item = new ConversationItem();
item.setId(rs.getInt("id"));
item.setGroup(rs.getInt("is_group") == 1);
item.setMemberCount(rs.getInt("member_count"));
item.setDirectUserId(rs.getObject("direct_user_id") == null ? null : Integer.valueOf(rs.getInt("direct_user_id")));
if (item.isGroup()) {
item.setTitle(defaultIfBlank(rs.getString("name"), "Groupe"));
item.setAvatarColor(defaultIfBlank(rs.getString("avatar_color"), "#E7F3FF"));
item.setSubtitle(item.getMemberCount() + " membres");
} else {
item.setTitle(defaultIfBlank(rs.getString("direct_name"), "Discussion privée"));
item.setAvatarColor(defaultIfBlank(rs.getString("direct_avatar_color"), "#1877f2"));
String username = rs.getString("direct_username");
item.setSubtitle(username == null ? "Discussion privée" : "@" + username);
}
return item;
}
private void enrichConversationSnapshot(Connection connection, ConversationItem item, int currentUserId)
throws SQLException {
String lastMessageSql = "SELECT body, attachment_name, is_deleted, created_at FROM messages "
+ "WHERE conversation_id = ? ORDER BY created_at DESC, id DESC LIMIT 1";
try (PreparedStatement statement = connection.prepareStatement(lastMessageSql)) {
statement.setInt(1, item.getId());
try (ResultSet rs = statement.executeQuery()) {
if (rs.next()) {
item.setLastMessagePreview(formatPreview(rs.getString("body"), rs.getString("attachment_name"), rs.getBoolean("is_deleted")));
item.setLastMessageAt(rs.getTimestamp("created_at"));
} else {
item.setLastMessagePreview(item.isGroup() ? "Nouveau groupe prêt à discuter" : "Dites bonjour 👋");
item.setLastMessageAt(null);
}
}
}
String unreadSql = "SELECT COUNT(*) AS total FROM messages WHERE conversation_id = ? AND sender_id <> ? AND seen_at IS NULL";
try (PreparedStatement statement = connection.prepareStatement(unreadSql)) {
statement.setInt(1, item.getId());
statement.setInt(2, currentUserId);
try (ResultSet rs = statement.executeQuery()) {
if (rs.next()) {
item.setUnreadCount(rs.getInt("total"));
}
}
}
}
private String formatPreview(String body, String attachmentName, boolean deleted) {
if (deleted) {
return "Message supprimé";
}
String cleanedBody = cleanText(body);
if (cleanedBody != null) {
return cleanedBody.length() > 54 ? cleanedBody.substring(0, 54) + "" : cleanedBody;
}
if (attachmentName != null && !attachmentName.trim().isEmpty()) {
return "📎 " + attachmentName;
}
return "Nouveau message";
}
private String defaultIfBlank(String value, String fallback) {
return value == null || value.trim().isEmpty() ? fallback : value;
}
private String cleanText(String value) {
if (value == null) {
return null;
}
String cleaned = value.trim();
return cleaned.isEmpty() ? null : cleaned;
}
}

View File

@ -0,0 +1,236 @@
package com.rjlresaka.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletContext;
import com.rjlresaka.model.FriendRequest;
import com.rjlresaka.model.User;
import com.rjlresaka.util.DatabaseConnection;
public class FriendDAO {
public List<User> findFriends(int currentUserId, ServletContext context) throws SQLException, ClassNotFoundException {
String sql = "SELECT u.* FROM friends f "
+ "INNER JOIN users u ON u.id = CASE WHEN f.user_one_id = ? THEN f.user_two_id ELSE f.user_one_id END "
+ "WHERE f.user_one_id = ? OR f.user_two_id = ? "
+ "ORDER BY u.full_name ASC";
List<User> friends = new ArrayList<User>();
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
statement.setInt(2, currentUserId);
statement.setInt(3, currentUserId);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
User friend = mapUser(rs);
friend.setFriend(true);
friends.add(friend);
}
}
}
return friends;
}
public List<User> findPeople(int currentUserId, ServletContext context) throws SQLException, ClassNotFoundException {
String sql = "SELECT u.*, "
+ "CASE WHEN fr_out.id IS NULL THEN 0 ELSE 1 END AS request_sent, "
+ "CASE WHEN fr_in.id IS NULL THEN 0 ELSE 1 END AS request_received "
+ "FROM users u "
+ "LEFT JOIN friend_requests fr_out ON fr_out.sender_id = ? AND fr_out.receiver_id = u.id AND fr_out.status = 'pending' "
+ "LEFT JOIN friend_requests fr_in ON fr_in.sender_id = u.id AND fr_in.receiver_id = ? AND fr_in.status = 'pending' "
+ "WHERE u.id <> ? "
+ "AND NOT EXISTS ("
+ " SELECT 1 FROM friends f WHERE f.user_one_id = LEAST(?, u.id) AND f.user_two_id = GREATEST(?, u.id)"
+ ") "
+ "ORDER BY CASE WHEN fr_in.id IS NULL THEN 1 ELSE 0 END ASC, u.full_name ASC";
List<User> users = new ArrayList<User>();
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
statement.setInt(2, currentUserId);
statement.setInt(3, currentUserId);
statement.setInt(4, currentUserId);
statement.setInt(5, currentUserId);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
User user = mapUser(rs);
user.setRequestSent(rs.getInt("request_sent") == 1);
user.setRequestReceived(rs.getInt("request_received") == 1);
users.add(user);
}
}
}
return users;
}
public List<FriendRequest> findPendingRequestsReceived(int currentUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "SELECT fr.id, fr.sender_id, fr.receiver_id, fr.created_at, "
+ "u.full_name, u.username, u.avatar_color "
+ "FROM friend_requests fr "
+ "INNER JOIN users u ON u.id = fr.sender_id "
+ "WHERE fr.receiver_id = ? AND fr.status = 'pending' "
+ "ORDER BY fr.created_at DESC";
List<FriendRequest> requests = new ArrayList<FriendRequest>();
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
FriendRequest request = new FriendRequest();
request.setId(rs.getInt("id"));
request.setSenderId(rs.getInt("sender_id"));
request.setReceiverId(rs.getInt("receiver_id"));
request.setSenderName(rs.getString("full_name"));
request.setSenderUsername(rs.getString("username"));
request.setSenderAvatarColor(rs.getString("avatar_color"));
request.setCreatedAt(rs.getTimestamp("created_at"));
requests.add(request);
}
}
}
return requests;
}
public void sendRequest(int senderId, int receiverId, ServletContext context)
throws SQLException, ClassNotFoundException {
if (senderId <= 0 || receiverId <= 0 || senderId == receiverId) {
return;
}
try (Connection connection = DatabaseConnection.getConnection(context)) {
connection.setAutoCommit(false);
try {
if (areFriends(connection, senderId, receiverId)) {
connection.commit();
return;
}
Integer reversePendingId = findPendingRequestId(connection, receiverId, senderId);
if (reversePendingId != null) {
updateRequestStatus(connection, reversePendingId.intValue(), "accepted");
createFriendship(connection, senderId, receiverId);
connection.commit();
return;
}
if (findPendingRequestId(connection, senderId, receiverId) != null) {
connection.commit();
return;
}
String insert = "INSERT INTO friend_requests (sender_id, receiver_id, status) VALUES (?, ?, 'pending')";
try (PreparedStatement statement = connection.prepareStatement(insert)) {
statement.setInt(1, senderId);
statement.setInt(2, receiverId);
statement.executeUpdate();
}
connection.commit();
} catch (SQLException exception) {
connection.rollback();
throw exception;
} finally {
connection.setAutoCommit(true);
}
}
}
public void respondToRequest(int requestId, int currentUserId, boolean accept, ServletContext context)
throws SQLException, ClassNotFoundException {
try (Connection connection = DatabaseConnection.getConnection(context)) {
connection.setAutoCommit(false);
try {
String select = "SELECT sender_id, receiver_id FROM friend_requests WHERE id = ? AND receiver_id = ? AND status = 'pending' LIMIT 1";
try (PreparedStatement statement = connection.prepareStatement(select)) {
statement.setInt(1, requestId);
statement.setInt(2, currentUserId);
try (ResultSet rs = statement.executeQuery()) {
if (!rs.next()) {
connection.commit();
return;
}
int senderId = rs.getInt("sender_id");
int receiverId = rs.getInt("receiver_id");
updateRequestStatus(connection, requestId, accept ? "accepted" : "declined");
if (accept) {
createFriendship(connection, senderId, receiverId);
}
}
}
connection.commit();
} catch (SQLException exception) {
connection.rollback();
throw exception;
} finally {
connection.setAutoCommit(true);
}
}
}
private Integer findPendingRequestId(Connection connection, int senderId, int receiverId) throws SQLException {
String sql = "SELECT id FROM friend_requests WHERE sender_id = ? AND receiver_id = ? AND status = 'pending' LIMIT 1";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, senderId);
statement.setInt(2, receiverId);
try (ResultSet rs = statement.executeQuery()) {
return rs.next() ? Integer.valueOf(rs.getInt("id")) : null;
}
}
}
private boolean areFriends(Connection connection, int firstUserId, int secondUserId) throws SQLException {
String sql = "SELECT id FROM friends WHERE user_one_id = ? AND user_two_id = ? LIMIT 1";
int userOneId = Math.min(firstUserId, secondUserId);
int userTwoId = Math.max(firstUserId, secondUserId);
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, userOneId);
statement.setInt(2, userTwoId);
try (ResultSet rs = statement.executeQuery()) {
return rs.next();
}
}
}
private void createFriendship(Connection connection, int firstUserId, int secondUserId) throws SQLException {
int userOneId = Math.min(firstUserId, secondUserId);
int userTwoId = Math.max(firstUserId, secondUserId);
if (areFriends(connection, userOneId, userTwoId)) {
return;
}
String sql = "INSERT INTO friends (user_one_id, user_two_id) VALUES (?, ?)";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, userOneId);
statement.setInt(2, userTwoId);
statement.executeUpdate();
}
}
private void updateRequestStatus(Connection connection, int requestId, String status) throws SQLException {
String sql = "UPDATE friend_requests SET status = ?, responded_at = NOW() WHERE id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, status);
statement.setInt(2, requestId);
statement.executeUpdate();
}
}
private User mapUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setFullName(rs.getString("full_name"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setPasswordHash(rs.getString("password_hash"));
user.setAvatarColor(rs.getString("avatar_color"));
user.setBio(rs.getString("bio"));
user.setCreatedAt(rs.getTimestamp("created_at"));
user.setUpdatedAt(rs.getTimestamp("updated_at"));
return user;
}
}

View File

@ -0,0 +1,258 @@
package com.rjlresaka.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletContext;
import com.rjlresaka.model.Message;
import com.rjlresaka.model.ReactionStat;
import com.rjlresaka.util.DatabaseConnection;
public class MessageDAO {
private final ConversationDAO conversationDAO = new ConversationDAO();
public List<Message> findMessagesBetweenUsers(int currentUserId, int otherUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
Integer conversationId = conversationDAO.findConversationIdBetweenUsers(currentUserId, otherUserId, context);
if (conversationId == null) {
return new ArrayList<Message>();
}
return findMessagesByConversation(conversationId.intValue(), currentUserId, context);
}
public List<Message> findMessagesByConversation(int conversationId, int currentUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "SELECT m.*, u.full_name, u.avatar_color "
+ "FROM messages m "
+ "INNER JOIN users u ON u.id = m.sender_id "
+ "INNER JOIN conversation_participants cp ON cp.conversation_id = m.conversation_id AND cp.user_id = ? "
+ "WHERE m.conversation_id = ? "
+ "ORDER BY m.created_at ASC, m.id ASC";
List<Message> messages = new ArrayList<Message>();
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
statement.setInt(2, conversationId);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
Message message = mapMessage(rs, currentUserId);
message.getReactions().addAll(findReactions(message.getId(), currentUserId, context));
messages.add(message);
}
}
}
return messages;
}
public int createMessage(int senderId, int receiverId, Message draft, ServletContext context)
throws SQLException, ClassNotFoundException {
int conversationId = conversationDAO.findOrCreateConversationId(senderId, receiverId, context);
createMessageInConversation(senderId, conversationId, draft, context);
return conversationId;
}
public void createMessageInConversation(int senderId, int conversationId, Message draft, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "INSERT INTO messages (conversation_id, sender_id, body, attachment_name, attachment_path, attachment_type, attachment_size) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
String updateConversation = "UPDATE conversations SET updated_at = NOW() WHERE id = ?";
try (Connection connection = DatabaseConnection.getConnection(context)) {
connection.setAutoCommit(false);
try {
if (!hasAccess(connection, conversationId, senderId)) {
throw new SQLException("Accès refusé à cette conversation.");
}
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, conversationId);
statement.setInt(2, senderId);
statement.setString(3, cleanText(draft.getBody()));
statement.setString(4, emptyToNull(draft.getAttachmentName()));
statement.setString(5, emptyToNull(draft.getAttachmentPath()));
statement.setString(6, emptyToNull(draft.getAttachmentType()));
if (draft.getAttachmentSize() > 0) {
statement.setLong(7, draft.getAttachmentSize());
} else {
statement.setNull(7, java.sql.Types.BIGINT);
}
statement.executeUpdate();
}
try (PreparedStatement statement = connection.prepareStatement(updateConversation)) {
statement.setInt(1, conversationId);
statement.executeUpdate();
}
connection.commit();
} catch (SQLException exception) {
connection.rollback();
throw exception;
} finally {
connection.setAutoCommit(true);
}
}
}
public boolean updateMessage(int messageId, int senderId, String body, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "UPDATE messages SET body = ?, is_edited = 1, updated_at = NOW() "
+ "WHERE id = ? AND sender_id = ? AND is_deleted = 0";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, cleanText(body));
statement.setInt(2, messageId);
statement.setInt(3, senderId);
return statement.executeUpdate() > 0;
}
}
public boolean deleteMessage(int messageId, int senderId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "UPDATE messages SET body = NULL, attachment_name = NULL, attachment_path = NULL, attachment_type = NULL, "
+ "attachment_size = NULL, is_deleted = 1, updated_at = NOW() WHERE id = ? AND sender_id = ?";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, messageId);
statement.setInt(2, senderId);
return statement.executeUpdate() > 0;
}
}
public void toggleReaction(int messageId, int userId, String emoji, ServletContext context)
throws SQLException, ClassNotFoundException {
if (emoji == null || emoji.trim().isEmpty()) {
return;
}
String select = "SELECT id FROM message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ? LIMIT 1";
String insert = "INSERT INTO message_reactions (message_id, user_id, emoji) VALUES (?, ?, ?)";
String delete = "DELETE FROM message_reactions WHERE id = ?";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement selectStatement = connection.prepareStatement(select)) {
selectStatement.setInt(1, messageId);
selectStatement.setInt(2, userId);
selectStatement.setString(3, emoji);
try (ResultSet rs = selectStatement.executeQuery()) {
if (rs.next()) {
try (PreparedStatement deleteStatement = connection.prepareStatement(delete)) {
deleteStatement.setInt(1, rs.getInt("id"));
deleteStatement.executeUpdate();
}
} else {
try (PreparedStatement insertStatement = connection.prepareStatement(insert)) {
insertStatement.setInt(1, messageId);
insertStatement.setInt(2, userId);
insertStatement.setString(3, emoji);
insertStatement.executeUpdate();
}
}
}
}
}
public Message findAttachmentForDownload(int messageId, int currentUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "SELECT m.*, u.full_name, u.avatar_color "
+ "FROM messages m "
+ "INNER JOIN users u ON u.id = m.sender_id "
+ "INNER JOIN conversation_participants cp ON cp.conversation_id = m.conversation_id AND cp.user_id = ? "
+ "WHERE m.id = ? LIMIT 1";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
statement.setInt(2, messageId);
try (ResultSet rs = statement.executeQuery()) {
return rs.next() ? mapMessage(rs, currentUserId) : null;
}
}
}
private boolean hasAccess(Connection connection, int conversationId, int userId) throws SQLException {
String sql = "SELECT 1 FROM conversation_participants WHERE conversation_id = ? AND user_id = ? LIMIT 1";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, conversationId);
statement.setInt(2, userId);
try (ResultSet rs = statement.executeQuery()) {
return rs.next();
}
}
}
private List<ReactionStat> findReactions(int messageId, int currentUserId, ServletContext context)
throws SQLException, ClassNotFoundException {
String sql = "SELECT emoji, COUNT(*) AS total, MAX(CASE WHEN user_id = ? THEN 1 ELSE 0 END) AS mine "
+ "FROM message_reactions WHERE message_id = ? GROUP BY emoji ORDER BY total DESC, emoji ASC";
List<ReactionStat> reactions = new ArrayList<ReactionStat>();
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, currentUserId);
statement.setInt(2, messageId);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
ReactionStat stat = new ReactionStat();
stat.setEmoji(rs.getString("emoji"));
stat.setCount(rs.getInt("total"));
stat.setReactedByCurrentUser(rs.getInt("mine") == 1);
reactions.add(stat);
}
}
}
return reactions;
}
private Message mapMessage(ResultSet rs, int currentUserId) throws SQLException {
Message message = new Message();
message.setId(rs.getInt("id"));
message.setConversationId(rs.getInt("conversation_id"));
message.setSenderId(rs.getInt("sender_id"));
message.setSenderName(rs.getString("full_name"));
message.setSenderAvatarColor(rs.getString("avatar_color"));
message.setSenderInitials(initials(rs.getString("full_name")));
message.setBody(rs.getString("body"));
message.setAttachmentName(rs.getString("attachment_name"));
message.setAttachmentPath(rs.getString("attachment_path"));
message.setAttachmentType(rs.getString("attachment_type"));
message.setAttachmentSize(rs.getLong("attachment_size"));
message.setEdited(rs.getBoolean("is_edited"));
message.setDeleted(rs.getBoolean("is_deleted"));
message.setMine(rs.getInt("sender_id") == currentUserId);
message.setCreatedAt(rs.getTimestamp("created_at"));
message.setUpdatedAt(rs.getTimestamp("updated_at"));
return message;
}
private String initials(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return "RR";
}
String[] parts = fullName.trim().split("\\s+");
StringBuilder builder = new StringBuilder();
for (String part : parts) {
if (!part.isEmpty()) {
builder.append(Character.toUpperCase(part.charAt(0)));
}
if (builder.length() == 2) {
break;
}
}
return builder.length() == 0 ? "RR" : builder.toString();
}
private String cleanText(String body) {
if (body == null) {
return null;
}
String cleaned = body.trim();
return cleaned.isEmpty() ? null : cleaned;
}
private String emptyToNull(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
return value;
}
}

View File

@ -57,6 +57,17 @@ public class UserDAO {
}
}
public User findById(int id, ServletContext context) throws SQLException, ClassNotFoundException {
String sql = "SELECT * FROM users WHERE id = ? LIMIT 1";
try (Connection connection = DatabaseConnection.getConnection(context);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, id);
try (ResultSet rs = statement.executeQuery()) {
return rs.next() ? mapUser(rs) : null;
}
}
}
public User findByEmail(String email, ServletContext context) throws SQLException, ClassNotFoundException {
String sql = "SELECT * FROM users WHERE email = ? LIMIT 1";
try (Connection connection = DatabaseConnection.getConnection(context);

View File

@ -0,0 +1,128 @@
package com.rjlresaka.model;
import java.io.Serializable;
import java.sql.Timestamp;
public class ConversationItem implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String title;
private String subtitle;
private String avatarColor;
private String lastMessagePreview;
private Timestamp lastMessageAt;
private int unreadCount;
private boolean active;
private boolean group;
private Integer directUserId;
private int memberCount;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
public String getAvatarColor() {
return avatarColor;
}
public void setAvatarColor(String avatarColor) {
this.avatarColor = avatarColor;
}
public String getLastMessagePreview() {
return lastMessagePreview;
}
public void setLastMessagePreview(String lastMessagePreview) {
this.lastMessagePreview = lastMessagePreview;
}
public Timestamp getLastMessageAt() {
return lastMessageAt;
}
public void setLastMessageAt(Timestamp lastMessageAt) {
this.lastMessageAt = lastMessageAt;
}
public int getUnreadCount() {
return unreadCount;
}
public void setUnreadCount(int unreadCount) {
this.unreadCount = unreadCount;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public boolean isGroup() {
return group;
}
public void setGroup(boolean group) {
this.group = group;
}
public Integer getDirectUserId() {
return directUserId;
}
public void setDirectUserId(Integer directUserId) {
this.directUserId = directUserId;
}
public int getMemberCount() {
return memberCount;
}
public void setMemberCount(int memberCount) {
this.memberCount = memberCount;
}
public String getInitials() {
if (title == null || title.trim().isEmpty()) {
return group ? "GR" : "RR";
}
String[] parts = title.trim().split("\\s+");
StringBuilder builder = new StringBuilder();
for (String part : parts) {
if (!part.isEmpty()) {
builder.append(Character.toUpperCase(part.charAt(0)));
}
if (builder.length() == 2) {
break;
}
}
if (builder.length() == 0) {
return group ? "GR" : "RR";
}
return builder.toString();
}
}

View File

@ -0,0 +1,89 @@
package com.rjlresaka.model;
import java.io.Serializable;
import java.sql.Timestamp;
public class FriendRequest implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private int senderId;
private int receiverId;
private String senderName;
private String senderUsername;
private String senderAvatarColor;
private Timestamp createdAt;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getSenderId() {
return senderId;
}
public void setSenderId(int senderId) {
this.senderId = senderId;
}
public int getReceiverId() {
return receiverId;
}
public void setReceiverId(int receiverId) {
this.receiverId = receiverId;
}
public String getSenderName() {
return senderName;
}
public void setSenderName(String senderName) {
this.senderName = senderName;
}
public String getSenderUsername() {
return senderUsername;
}
public void setSenderUsername(String senderUsername) {
this.senderUsername = senderUsername;
}
public String getSenderAvatarColor() {
return senderAvatarColor;
}
public void setSenderAvatarColor(String senderAvatarColor) {
this.senderAvatarColor = senderAvatarColor;
}
public Timestamp getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Timestamp createdAt) {
this.createdAt = createdAt;
}
public String getSenderInitials() {
if (senderName == null || senderName.trim().isEmpty()) {
return "RR";
}
String[] parts = senderName.trim().split("\\s+");
StringBuilder builder = new StringBuilder();
for (String part : parts) {
if (!part.isEmpty()) {
builder.append(Character.toUpperCase(part.charAt(0)));
}
if (builder.length() == 2) {
break;
}
}
return builder.length() == 0 ? "RR" : builder.toString();
}
}

View File

@ -0,0 +1,175 @@
package com.rjlresaka.model;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private int conversationId;
private int senderId;
private String senderName;
private String senderAvatarColor;
private String senderInitials;
private String body;
private String attachmentName;
private String attachmentPath;
private String attachmentType;
private long attachmentSize;
private boolean edited;
private boolean deleted;
private boolean mine;
private Timestamp createdAt;
private Timestamp updatedAt;
private final List<ReactionStat> reactions = new ArrayList<ReactionStat>();
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getConversationId() {
return conversationId;
}
public void setConversationId(int conversationId) {
this.conversationId = conversationId;
}
public int getSenderId() {
return senderId;
}
public void setSenderId(int senderId) {
this.senderId = senderId;
}
public String getSenderName() {
return senderName;
}
public void setSenderName(String senderName) {
this.senderName = senderName;
}
public String getSenderAvatarColor() {
return senderAvatarColor;
}
public void setSenderAvatarColor(String senderAvatarColor) {
this.senderAvatarColor = senderAvatarColor;
}
public String getSenderInitials() {
return senderInitials;
}
public void setSenderInitials(String senderInitials) {
this.senderInitials = senderInitials;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getAttachmentName() {
return attachmentName;
}
public void setAttachmentName(String attachmentName) {
this.attachmentName = attachmentName;
}
public String getAttachmentPath() {
return attachmentPath;
}
public void setAttachmentPath(String attachmentPath) {
this.attachmentPath = attachmentPath;
}
public String getAttachmentType() {
return attachmentType;
}
public void setAttachmentType(String attachmentType) {
this.attachmentType = attachmentType;
}
public long getAttachmentSize() {
return attachmentSize;
}
public void setAttachmentSize(long attachmentSize) {
this.attachmentSize = attachmentSize;
}
public boolean isEdited() {
return edited;
}
public void setEdited(boolean edited) {
this.edited = edited;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public boolean isMine() {
return mine;
}
public void setMine(boolean mine) {
this.mine = mine;
}
public Timestamp getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Timestamp createdAt) {
this.createdAt = createdAt;
}
public Timestamp getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Timestamp updatedAt) {
this.updatedAt = updatedAt;
}
public List<ReactionStat> getReactions() {
return reactions;
}
public String getDisplayBody() {
if (deleted) {
return "Message supprimé";
}
return body == null ? "" : body;
}
public boolean hasAttachment() {
return attachmentPath != null && !attachmentPath.trim().isEmpty();
}
public boolean hasText() {
return body != null && !body.trim().isEmpty();
}
}

View File

@ -0,0 +1,35 @@
package com.rjlresaka.model;
import java.io.Serializable;
public class ReactionStat implements Serializable {
private static final long serialVersionUID = 1L;
private String emoji;
private int count;
private boolean reactedByCurrentUser;
public String getEmoji() {
return emoji;
}
public void setEmoji(String emoji) {
this.emoji = emoji;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public boolean isReactedByCurrentUser() {
return reactedByCurrentUser;
}
public void setReactedByCurrentUser(boolean reactedByCurrentUser) {
this.reactedByCurrentUser = reactedByCurrentUser;
}
}

View File

@ -0,0 +1,48 @@
package com.rjlresaka.model;
import java.io.Serializable;
public class StoredAttachment implements Serializable {
private static final long serialVersionUID = 1L;
private String originalName;
private String storagePath;
private String contentType;
private long size;
public String getOriginalName() {
return originalName;
}
public void setOriginalName(String originalName) {
this.originalName = originalName;
}
public String getStoragePath() {
return storagePath;
}
public void setStoragePath(String storagePath) {
this.storagePath = storagePath;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public boolean isImage() {
return contentType != null && contentType.startsWith("image/");
}
}

View File

@ -16,6 +16,15 @@ public class User implements Serializable {
private Timestamp createdAt;
private Timestamp updatedAt;
private Integer conversationId;
private String lastMessagePreview;
private Timestamp lastMessageAt;
private int unreadCount;
private boolean active;
private boolean friend;
private boolean requestSent;
private boolean requestReceived;
public int getId() {
return id;
}
@ -88,6 +97,70 @@ public class User implements Serializable {
this.updatedAt = updatedAt;
}
public Integer getConversationId() {
return conversationId;
}
public void setConversationId(Integer conversationId) {
this.conversationId = conversationId;
}
public String getLastMessagePreview() {
return lastMessagePreview;
}
public void setLastMessagePreview(String lastMessagePreview) {
this.lastMessagePreview = lastMessagePreview;
}
public Timestamp getLastMessageAt() {
return lastMessageAt;
}
public void setLastMessageAt(Timestamp lastMessageAt) {
this.lastMessageAt = lastMessageAt;
}
public int getUnreadCount() {
return unreadCount;
}
public void setUnreadCount(int unreadCount) {
this.unreadCount = unreadCount;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public boolean isFriend() {
return friend;
}
public void setFriend(boolean friend) {
this.friend = friend;
}
public boolean isRequestSent() {
return requestSent;
}
public void setRequestSent(boolean requestSent) {
this.requestSent = requestSent;
}
public boolean isRequestReceived() {
return requestReceived;
}
public void setRequestReceived(boolean requestReceived) {
this.requestReceived = requestReceived;
}
public String getInitials() {
if (fullName == null || fullName.trim().isEmpty()) {
return "RR";

View File

@ -0,0 +1,67 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.ConversationDAO;
import com.rjlresaka.model.User;
@WebServlet("/app/groups/create")
public class CreateGroupServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final ConversationDAO conversationDAO = new ConversationDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
String groupName = request.getParameter("groupName");
String[] selectedMembers = request.getParameterValues("memberIds");
List<Integer> memberIds = new ArrayList<Integer>();
if (selectedMembers != null) {
for (String value : selectedMembers) {
int id = parseInt(value);
if (id > 0) {
memberIds.add(Integer.valueOf(id));
}
}
}
if (memberIds.isEmpty()) {
session.setAttribute("flashError", "Choisissez au moins un ami pour créer un groupe.");
response.sendRedirect(request.getContextPath() + "/app/dashboard");
return;
}
try {
int conversationId = conversationDAO.createGroupConversation(authUser.getId(), groupName, memberIds, getServletContext());
session.setAttribute("flashSuccess", "Groupe créé avec succès.");
response.sendRedirect(request.getContextPath() + "/app/dashboard?conversation=" + conversationId);
} catch (Exception exception) {
session.setAttribute("flashError", "Création du groupe impossible : " + exception.getMessage());
response.sendRedirect(request.getContextPath() + "/app/dashboard");
}
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
}

View File

@ -1,6 +1,7 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
@ -10,27 +11,116 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.UserDAO;
import com.rjlresaka.dao.ConversationDAO;
import com.rjlresaka.dao.FriendDAO;
import com.rjlresaka.dao.MessageDAO;
import com.rjlresaka.model.ConversationItem;
import com.rjlresaka.model.FriendRequest;
import com.rjlresaka.model.Message;
import com.rjlresaka.model.User;
@WebServlet("/app/dashboard")
public class DashboardServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final UserDAO userDAO = new UserDAO();
private final ConversationDAO conversationDAO = new ConversationDAO();
private final FriendDAO friendDAO = new FriendDAO();
private final MessageDAO messageDAO = new MessageDAO();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
try {
List<User> users = userDAO.findOtherUsers(authUser.getId(), getServletContext());
request.setAttribute("users", users);
Integer forcedConversationId = null;
int requestedUserId = parseInt(request.getParameter("user"));
if (requestedUserId > 0 && requestedUserId != authUser.getId()) {
forcedConversationId = Integer.valueOf(conversationDAO.findOrCreateConversationId(authUser.getId(), requestedUserId, getServletContext()));
}
List<ConversationItem> conversations = conversationDAO.findConversationsForUser(authUser.getId(), getServletContext());
List<User> friends = friendDAO.findFriends(authUser.getId(), getServletContext());
List<User> people = friendDAO.findPeople(authUser.getId(), getServletContext());
List<FriendRequest> pendingRequests = friendDAO.findPendingRequestsReceived(authUser.getId(), getServletContext());
ConversationItem activeConversation = resolveActiveConversation(request, conversations, forcedConversationId);
List<Message> messages = new ArrayList<Message>();
if (activeConversation != null) {
activeConversation.setActive(true);
conversationDAO.markConversationAsSeen(activeConversation.getId(), authUser.getId(), getServletContext());
messages = messageDAO.findMessagesByConversation(activeConversation.getId(), authUser.getId(), getServletContext());
}
request.setAttribute("conversations", conversations);
request.setAttribute("activeConversation", activeConversation);
request.setAttribute("messages", messages);
request.setAttribute("friends", friends);
request.setAttribute("people", people);
request.setAttribute("pendingRequests", pendingRequests);
request.setAttribute("friendCount", Integer.valueOf(friends.size()));
request.setAttribute("requestCount", Integer.valueOf(pendingRequests.size()));
request.setAttribute("groupCandidateCount", Integer.valueOf(friends.size()));
consumeFlash(session, request);
request.getRequestDispatcher("/WEB-INF/views/dashboard.jsp").forward(request, response);
} catch (Exception exception) {
request.setAttribute("error", "Impossible de charger les utilisateurs pour le moment.");
request.setAttribute("error", "Impossible de charger le tableau de bord pour le moment.");
request.setAttribute("debugMessage", exception.getMessage());
request.getRequestDispatcher("/WEB-INF/views/dashboard.jsp").forward(request, response);
}
}
private ConversationItem resolveActiveConversation(HttpServletRequest request, List<ConversationItem> conversations, Integer forcedConversationId) {
String conversationParam = request.getParameter("conversation");
Integer conversationId = null;
try {
if (conversationParam != null && !conversationParam.trim().isEmpty()) {
conversationId = Integer.valueOf(conversationParam);
}
} catch (NumberFormatException exception) {
conversationId = null;
}
if (conversations.isEmpty()) {
return null;
}
if (conversationId == null && forcedConversationId != null) {
conversationId = forcedConversationId;
}
if (conversationId == null) {
return conversations.get(0);
}
for (ConversationItem item : conversations) {
if (item.getId() == conversationId.intValue()) {
return item;
}
}
return conversations.get(0);
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
private void consumeFlash(HttpSession session, HttpServletRequest request) {
Object success = session.getAttribute("flashSuccess");
Object error = session.getAttribute("flashError");
if (success != null) {
request.setAttribute("success", success.toString());
session.removeAttribute("flashSuccess");
}
if (error != null) {
request.setAttribute("error", error.toString());
session.removeAttribute("flashError");
}
}
}

View File

@ -0,0 +1,50 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.MessageDAO;
import com.rjlresaka.model.User;
@WebServlet("/app/messages/delete")
public class DeleteMessageServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final MessageDAO messageDAO = new MessageDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
int conversationId = parseInt(request.getParameter("conversationId"));
int messageId = parseInt(request.getParameter("messageId"));
try {
boolean deleted = messageDAO.deleteMessage(messageId, authUser.getId(), getServletContext());
session.setAttribute(deleted ? "flashSuccess" : "flashError",
deleted ? "Message supprimé." : "Suppression refusée.");
} catch (Exception exception) {
session.setAttribute("flashError", "Erreur de suppression : " + exception.getMessage());
}
response.sendRedirect(request.getContextPath() + "/app/dashboard?conversation=" + conversationId);
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
}

View File

@ -0,0 +1,60 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.MessageDAO;
import com.rjlresaka.model.Message;
import com.rjlresaka.model.User;
import com.rjlresaka.util.FileStorageUtil;
@WebServlet("/app/files/download")
public class DownloadAttachmentServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final MessageDAO messageDAO = new MessageDAO();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
User authUser = (User) session.getAttribute("authUser");
int messageId = parseInt(request.getParameter("id"));
try {
Message message = messageDAO.findAttachmentForDownload(messageId, authUser.getId(), getServletContext());
if (message == null || !message.hasAttachment()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path filePath = FileStorageUtil.resolveStoredFile(message.getAttachmentPath(), getServletContext());
if (!Files.exists(filePath)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setContentType(message.getAttachmentType() == null ? "application/octet-stream" : message.getAttachmentType());
response.setHeader("Content-Disposition", "attachment; filename="" + message.getAttachmentName() + """);
response.setContentLengthLong(Files.size(filePath));
Files.copy(filePath, response.getOutputStream());
} catch (Exception exception) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, exception.getMessage());
}
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
}

View File

@ -0,0 +1,50 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.FriendDAO;
import com.rjlresaka.model.User;
@WebServlet("/app/friends/respond")
public class FriendRequestActionServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final FriendDAO friendDAO = new FriendDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
int requestId = parseInt(request.getParameter("requestId"));
String action = request.getParameter("action");
boolean accept = "accept".equalsIgnoreCase(action);
try {
friendDAO.respondToRequest(requestId, authUser.getId(), accept, getServletContext());
session.setAttribute("flashSuccess", accept ? "Demande d'ami acceptée." : "Demande refusée.");
} catch (Exception exception) {
session.setAttribute("flashError", "Action impossible : " + exception.getMessage());
}
response.sendRedirect(request.getContextPath() + "/app/dashboard");
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
}

View File

@ -0,0 +1,47 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.FriendDAO;
import com.rjlresaka.model.User;
@WebServlet("/app/friends/request")
public class FriendRequestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final FriendDAO friendDAO = new FriendDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
int receiverId = parseInt(request.getParameter("receiverId"));
try {
friendDAO.sendRequest(authUser.getId(), receiverId, getServletContext());
session.setAttribute("flashSuccess", "Demande d'ami envoyée.");
} catch (Exception exception) {
session.setAttribute("flashError", "Impossible d'envoyer la demande : " + exception.getMessage());
}
response.sendRedirect(request.getContextPath() + "/app/dashboard");
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
}

View File

@ -0,0 +1,99 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Part;
import com.rjlresaka.dao.MessageDAO;
import com.rjlresaka.model.Message;
import com.rjlresaka.model.StoredAttachment;
import com.rjlresaka.model.User;
import com.rjlresaka.util.FileStorageUtil;
@WebServlet("/app/messages/send")
@MultipartConfig(fileSizeThreshold = 1024 * 1024, maxFileSize = 10 * 1024 * 1024, maxRequestSize = 12 * 1024 * 1024)
public class SendMessageServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final MessageDAO messageDAO = new MessageDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
int conversationId = parseInt(request.getParameter("conversationId"));
int partnerId = parseInt(request.getParameter("partnerId"));
String body = trim(request.getParameter("body"));
Part attachmentPart = request.getPart("attachment");
try {
StoredAttachment attachment = FileStorageUtil.store(attachmentPart, getServletContext());
if (body.isEmpty() && attachment == null) {
setFlash(session, "flashError", "Ajoutez un texte ou un fichier avant d'envoyer.");
response.sendRedirect(request.getContextPath() + redirectTarget(conversationId, partnerId));
return;
}
Message draft = new Message();
draft.setBody(body);
if (attachment != null) {
draft.setAttachmentName(attachment.getOriginalName());
draft.setAttachmentPath(attachment.getStoragePath());
draft.setAttachmentType(attachment.getContentType());
draft.setAttachmentSize(attachment.getSize());
}
if (conversationId > 0) {
messageDAO.createMessageInConversation(authUser.getId(), conversationId, draft, getServletContext());
} else if (partnerId > 0) {
conversationId = messageDAO.createMessage(authUser.getId(), partnerId, draft, getServletContext());
} else {
setFlash(session, "flashError", "Conversation introuvable.");
response.sendRedirect(request.getContextPath() + "/app/dashboard");
return;
}
setFlash(session, "flashSuccess", "Message envoyé.");
} catch (Exception exception) {
setFlash(session, "flashError", "Envoi impossible pour le moment : " + exception.getMessage());
}
response.sendRedirect(request.getContextPath() + "/app/dashboard?conversation=" + conversationId);
}
private String redirectTarget(int conversationId, int partnerId) {
if (conversationId > 0) {
return "/app/dashboard?conversation=" + conversationId;
}
if (partnerId > 0) {
return "/app/dashboard?user=" + partnerId;
}
return "/app/dashboard";
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
private String trim(String value) {
return value == null ? "" : value.trim();
}
private void setFlash(HttpSession session, String key, String message) {
session.setAttribute(key, message);
}
}

View File

@ -0,0 +1,49 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.MessageDAO;
import com.rjlresaka.model.User;
@WebServlet("/app/messages/react")
public class ToggleReactionServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final MessageDAO messageDAO = new MessageDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
int conversationId = parseInt(request.getParameter("conversationId"));
int messageId = parseInt(request.getParameter("messageId"));
String emoji = request.getParameter("emoji");
try {
messageDAO.toggleReaction(messageId, authUser.getId(), emoji, getServletContext());
} catch (Exception exception) {
session.setAttribute("flashError", "Réaction indisponible : " + exception.getMessage());
}
response.sendRedirect(request.getContextPath() + "/app/dashboard?conversation=" + conversationId);
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
}

View File

@ -0,0 +1,51 @@
package com.rjlresaka.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.rjlresaka.dao.MessageDAO;
import com.rjlresaka.model.User;
@WebServlet("/app/messages/update")
public class UpdateMessageServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final MessageDAO messageDAO = new MessageDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("authUser") == null) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
User authUser = (User) session.getAttribute("authUser");
int conversationId = parseInt(request.getParameter("conversationId"));
int messageId = parseInt(request.getParameter("messageId"));
String body = request.getParameter("body");
try {
boolean updated = messageDAO.updateMessage(messageId, authUser.getId(), body, getServletContext());
session.setAttribute(updated ? "flashSuccess" : "flashError",
updated ? "Message modifié." : "Modification refusée.");
} catch (Exception exception) {
session.setAttribute("flashError", "Erreur de modification : " + exception.getMessage());
}
response.sendRedirect(request.getContextPath() + "/app/dashboard?conversation=" + conversationId);
}
private int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException exception) {
return 0;
}
}
}

View File

@ -0,0 +1,91 @@
package com.rjlresaka.util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.Part;
import com.rjlresaka.model.StoredAttachment;
public final class FileStorageUtil {
private FileStorageUtil() {
}
public static StoredAttachment store(Part part, ServletContext context) throws IOException {
if (part == null || part.getSize() <= 0) {
return null;
}
String originalName = cleanFilename(part.getSubmittedFileName());
if (originalName.isEmpty()) {
return null;
}
String storedName = UUID.randomUUID().toString().replace("-", "") + "_" + originalName;
Path uploadDirectory = resolveUploadDirectory(context);
Files.createDirectories(uploadDirectory);
Path target = uploadDirectory.resolve(storedName);
try (InputStream inputStream = part.getInputStream()) {
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
}
StoredAttachment attachment = new StoredAttachment();
attachment.setOriginalName(originalName);
attachment.setStoragePath(storedName);
attachment.setContentType(part.getContentType());
attachment.setSize(part.getSize());
return attachment;
}
public static Path resolveStoredFile(String storedName, ServletContext context) throws IOException {
if (storedName == null || storedName.contains("..") || storedName.contains("/") || storedName.contains("\\")) {
throw new IOException("Nom de fichier invalide.");
}
return resolveUploadDirectory(context).resolve(storedName);
}
private static Path resolveUploadDirectory(ServletContext context) {
String configuredDir = context.getInitParameter("upload.dir");
if (configuredDir == null || configuredDir.trim().isEmpty()) {
configuredDir = "uploads";
}
configuredDir = configuredDir.replace("\\", "/").replace("..", "").trim();
String realRoot = context.getRealPath("/");
if (realRoot != null && !realRoot.trim().isEmpty()) {
return Paths.get(realRoot, "WEB-INF", configuredDir);
}
return Paths.get(System.getProperty("java.io.tmpdir"), "rjlresaka", configuredDir);
}
private static String cleanFilename(String filename) {
if (filename == null) {
return "";
}
String cleaned = filename.replace("\\", "/");
int slashIndex = cleaned.lastIndexOf('/');
if (slashIndex >= 0) {
cleaned = cleaned.substring(slashIndex + 1);
}
cleaned = cleaned.trim().replaceAll("[^a-zA-Z0-9._-]", "_");
if (cleaned.length() > 120) {
String extension = "";
int dotIndex = cleaned.lastIndexOf('.');
if (dotIndex > 0) {
extension = cleaned.substring(dotIndex).toLowerCase(Locale.ROOT);
cleaned = cleaned.substring(0, dotIndex);
}
cleaned = cleaned.substring(0, 100) + extension;
}
return cleaned;
}
}

BIN
RJLResaka_final.zip Normal file

Binary file not shown.