Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions client/src/pages/TripInvitation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function TripInvitation({
email: "",
message: "",
});

const [copyLink, setCopyLink] = useState(false);
const [loading, setLoading] = useState(false);

const formatDate = (dateString: string) => {
Expand All @@ -62,6 +62,7 @@ function TripInvitation({

const cancelInvitation = (e: React.MouseEvent<HTMLButtonElement>) => {
setInvitationForm({ email: "", message: "" });
setCopyLink(false);
if (onClose) onClose(e);
};

Expand All @@ -74,7 +75,7 @@ function TripInvitation({
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("Lien dinvitation copié 📋");
toast.success("Lien d'invitation copié");
} catch {
toast.error("Impossible de copier le lien");
}
Expand All @@ -100,15 +101,22 @@ function TripInvitation({
throw new Error(data.error || "Erreur lors de l'envoi");
}

await copyToClipboard(data.invitationLink);
if (copyLink) {
await copyToClipboard(data.invitationLink);
}

toast.success("Email envoyé avec succès");

setInvitationForm({ email: "", message: "" });
setCopyLink(false);
} catch {
toast.error("Erreur lors de l'envoi de l'invitation");
} finally {
setLoading(false);
}
};


return (
<section
className="tripinvitation-overlay"
Expand All @@ -120,7 +128,6 @@ function TripInvitation({
}}
aria-modal="true"
>

<ToastContainer position="top-right" autoClose={3000} theme="light" />
<article className="tripinvitation-invitation-form">
<div className="tripinvitation-head">
Expand Down Expand Up @@ -162,6 +169,7 @@ function TripInvitation({
onChange={updateInvitationForm}
required
placeholder="adresse@email.com"
disabled={loading}
/>
</label>

Expand All @@ -172,15 +180,28 @@ function TripInvitation({
value={invitationForm.message}
onChange={updateInvitationForm}
placeholder="Ajoutez un message personnalisé..."
disabled={loading}
/>
</label>

<label>
<div className="tripinvitation-copy-checkbox">
<input
type="checkbox"
checked={copyLink}
onChange={(e) => setCopyLink(e.target.checked)}
disabled={loading}
/>
Copier aussi le lien d'invitation
</div>
</label>

<button
type="submit"
className="tripinvitation-btn-send-invitation"
disabled={loading}
disabled={loading || !invitationForm.email}
>
{loading ? "Copie..." : "Copier le lien d'invitation"}
{loading ? "Envoi..." : "Envoyer par email"}
</button>

<button
Expand Down
13 changes: 13 additions & 0 deletions client/src/pages/styles/TripInvitation.css
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,19 @@
.tripinvitation-btn-cancel-invitation:hover {
background: #f3f4f6;
}
.tripinvitation-copy-checkbox {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}

.tripinvitation-copy-checkbox input[type="checkbox"] {
margin-top: 0.05rem;
flex-shrink: 0;
}

@media (max-width: 768px) {
.tripinvitation-invitation-form {
Expand Down
83 changes: 78 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions server/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ CLIENT_URL=http://localhost:3000

GOOGLE_API_KEY=YOUR_API_KEY

RESEND_API_KEY=YOUR_API_KEY
RESEND_FROM="YOUR NAME <NAME@DOMAIN.com>"


# About specific needs, please ask your trainer about the deploiement project name and follow the pattern
# You can add as much variable as needed. Don't forget to tell your trainer about it. Otherwise, it could break on deploiement
PROJECT_NAME_SPECIFIC_NAME=YOUR_SPECIFIC_VALUE
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"joi": "^18.0.2",
"jsonwebtoken": "^9.0.3",
"mysql2": "^3.16.0",
"react-toastify": "^11.0.5"
"react-toastify": "^11.0.5",
"resend": "^6.9.4"
},
"devDependencies": {
"@faker-js/faker": "^9.6.0",
Expand Down
11 changes: 11 additions & 0 deletions server/src/libs/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Resend } from "resend";

const apiKey = process.env.RESEND_API_KEY;

if (!apiKey) {
throw new Error("RESEND_API_KEY environment variable is not defined");
}

const resend = new Resend(apiKey);

export default resend;
32 changes: 31 additions & 1 deletion server/src/modules/invitation/invitationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { RequestHandler } from "express";
import tripRepository from "../trip/tripRepository";
import userRepository from "../user/userRepository";
import invitationRepository from "./invitationRepository";
import resend from "../../libs/resend";

const read: RequestHandler = async (req, res, next) => {
try {
Expand Down Expand Up @@ -109,8 +110,37 @@ const add: RequestHandler = async (req, res, next) => {
}

const invitationLink = `${clientUrl}/trip/${tripId}/invitation/${invitationId}`;

const escapeHtml = (unsafe: string) =>
unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");

const safeMessage = escapeHtml(message);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: message is not type-validated before calling escapeHtml, so non-string payloads can trigger a runtime .replace is not a function error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/modules/invitation/invitationActions.ts, line 122:

<comment>`message` is not type-validated before calling `escapeHtml`, so non-string payloads can trigger a runtime `.replace is not a function` error.</comment>

<file context>
@@ -110,27 +110,34 @@ const add: RequestHandler = async (req, res, next) => {
+        .replace(/"/g, "&quot;")
+        .replace(/'/g, "&#039;");
+
+    const safeMessage = escapeHtml(message);
 
     const { data, error } = await resend.emails.send({
</file context>
Suggested change
const safeMessage = escapeHtml(message);
const safeMessage = escapeHtml(typeof message === "string" ? message : String(message ?? ""));
Fix with Cubic


const { data, error } = await resend.emails.send({
from: process.env.RESEND_FROM as string,
to: email,
subject: "Invitation à rejoindre un voyage sur TripTogether",
html: `
<p>Bonjour,</p>
<p>Vous avez reçu une invitation à rejoindre un voyage sur TripTogether.</p>
<p>Message :</p>
<blockquote>${safeMessage}</blockquote>
<p>Pour voir l'invitation et répondre, cliquez sur le lien ci-dessous :</p>
<p><a href="${invitationLink}">${invitationLink}</a></p>
<p>À bientôt sur TripTogether !</p>
`,
});

if (error) {
console.error("Erreur envoi email invitation:", error);
}

res.status(201).json({ invitationLink });
res.status(201).json({ invitationLink, emailSent: !error, emailData: data });
} catch (err) {
next(err);
}
Expand Down
Loading