Autosave: 20260405-222200
This commit is contained in:
parent
db8313c88e
commit
3b03cdb4a1
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 😂 1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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
BIN
RJLResaka/WebContent/assets/img/logo_resaka.png
Normal file
BIN
RJLResaka/WebContent/assets/img/logo_resaka.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
299
RJLResaka/src/com/rjlresaka/dao/ConversationDAO.java
Normal file
299
RJLResaka/src/com/rjlresaka/dao/ConversationDAO.java
Normal 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;
|
||||
}
|
||||
}
|
||||
236
RJLResaka/src/com/rjlresaka/dao/FriendDAO.java
Normal file
236
RJLResaka/src/com/rjlresaka/dao/FriendDAO.java
Normal 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;
|
||||
}
|
||||
}
|
||||
258
RJLResaka/src/com/rjlresaka/dao/MessageDAO.java
Normal file
258
RJLResaka/src/com/rjlresaka/dao/MessageDAO.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
128
RJLResaka/src/com/rjlresaka/model/ConversationItem.java
Normal file
128
RJLResaka/src/com/rjlresaka/model/ConversationItem.java
Normal 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();
|
||||
}
|
||||
}
|
||||
89
RJLResaka/src/com/rjlresaka/model/FriendRequest.java
Normal file
89
RJLResaka/src/com/rjlresaka/model/FriendRequest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
175
RJLResaka/src/com/rjlresaka/model/Message.java
Normal file
175
RJLResaka/src/com/rjlresaka/model/Message.java
Normal 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();
|
||||
}
|
||||
}
|
||||
35
RJLResaka/src/com/rjlresaka/model/ReactionStat.java
Normal file
35
RJLResaka/src/com/rjlresaka/model/ReactionStat.java
Normal 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;
|
||||
}
|
||||
}
|
||||
48
RJLResaka/src/com/rjlresaka/model/StoredAttachment.java
Normal file
48
RJLResaka/src/com/rjlresaka/model/StoredAttachment.java
Normal 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/");
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
67
RJLResaka/src/com/rjlresaka/servlet/CreateGroupServlet.java
Normal file
67
RJLResaka/src/com/rjlresaka/servlet/CreateGroupServlet.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
RJLResaka/src/com/rjlresaka/servlet/SendMessageServlet.java
Normal file
99
RJLResaka/src/com/rjlresaka/servlet/SendMessageServlet.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
RJLResaka/src/com/rjlresaka/util/FileStorageUtil.java
Normal file
91
RJLResaka/src/com/rjlresaka/util/FileStorageUtil.java
Normal 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
BIN
RJLResaka_final.zip
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user