feat(contact): wire contact form to real backend — sends email to contact@xpeditis.com

This commit is contained in:
David 2026-04-02 14:19:50 +02:00
parent ed0f43ba32
commit 74221d576e
5 changed files with 151 additions and 8 deletions

View File

@ -8,6 +8,8 @@ import {
Get, Get,
Inject, Inject,
NotFoundException, NotFoundException,
InternalServerErrorException,
Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
@ -18,7 +20,9 @@ import {
RefreshTokenDto, RefreshTokenDto,
ForgotPasswordDto, ForgotPasswordDto,
ResetPasswordDto, ResetPasswordDto,
ContactFormDto,
} from '../dto/auth-login.dto'; } from '../dto/auth-login.dto';
import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port';
import { Public } from '../decorators/public.decorator'; import { Public } from '../decorators/public.decorator';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
@ -39,10 +43,13 @@ import { InvitationService } from '../services/invitation.service';
@ApiTags('Authentication') @ApiTags('Authentication')
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
private readonly logger = new Logger(AuthController.name);
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
private readonly invitationService: InvitationService private readonly invitationService: InvitationService,
@Inject(EMAIL_PORT) private readonly emailService: EmailPort
) {} ) {}
/** /**
@ -216,6 +223,78 @@ export class AuthController {
return { message: 'Logout successful' }; return { message: 'Logout successful' };
} }
/**
* Contact form forwards message to contact@xpeditis.com
*/
@Public()
@Post('contact')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Contact form',
description: 'Send a contact message to the Xpeditis team.',
})
@ApiResponse({ status: 200, description: 'Message sent successfully' })
async contact(@Body() dto: ContactFormDto): Promise<{ message: string }> {
const subjectLabels: Record<string, string> = {
demo: 'Demande de démonstration',
pricing: 'Questions sur les tarifs',
partnership: 'Partenariat',
support: 'Support technique',
press: 'Relations presse',
careers: 'Recrutement',
other: 'Autre',
};
const subjectLabel = subjectLabels[dto.subject] || dto.subject;
const html = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #10183A; padding: 24px; border-radius: 8px 8px 0 0;">
<h1 style="color: #34CCCD; margin: 0; font-size: 20px;">Nouveau message de contact</h1>
</div>
<div style="background: #f9f9f9; padding: 24px; border: 1px solid #e0e0e0;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666; width: 130px; font-size: 14px;">Nom</td>
<td style="padding: 8px 0; color: #222; font-weight: bold; font-size: 14px;">${dto.firstName} ${dto.lastName}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666; font-size: 14px;">Email</td>
<td style="padding: 8px 0; font-size: 14px;"><a href="mailto:${dto.email}" style="color: #34CCCD;">${dto.email}</a></td>
</tr>
${dto.company ? `<tr><td style="padding: 8px 0; color: #666; font-size: 14px;">Entreprise</td><td style="padding: 8px 0; color: #222; font-size: 14px;">${dto.company}</td></tr>` : ''}
${dto.phone ? `<tr><td style="padding: 8px 0; color: #666; font-size: 14px;">Téléphone</td><td style="padding: 8px 0; color: #222; font-size: 14px;">${dto.phone}</td></tr>` : ''}
<tr>
<td style="padding: 8px 0; color: #666; font-size: 14px;">Sujet</td>
<td style="padding: 8px 0; color: #222; font-size: 14px;">${subjectLabel}</td>
</tr>
</table>
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #ddd;">
<p style="color: #666; font-size: 14px; margin: 0 0 8px 0;">Message :</p>
<p style="color: #222; font-size: 14px; white-space: pre-wrap; margin: 0;">${dto.message}</p>
</div>
</div>
<div style="background: #f0f0f0; padding: 12px 24px; border-radius: 0 0 8px 8px; text-align: center;">
<p style="color: #999; font-size: 12px; margin: 0;">Xpeditis Formulaire de contact</p>
</div>
</div>
`;
try {
await this.emailService.send({
to: 'contact@xpeditis.com',
replyTo: dto.email,
subject: `[Contact] ${subjectLabel}${dto.firstName} ${dto.lastName}`,
html,
});
} catch (error) {
this.logger.error(`Failed to send contact email: ${error}`);
throw new InternalServerErrorException("Erreur lors de l'envoi du message. Veuillez réessayer.");
}
return { message: 'Message envoyé avec succès.' };
}
/** /**
* Forgot password sends reset email * Forgot password sends reset email
*/ */

View File

@ -37,6 +37,42 @@ export class LoginDto {
rememberMe?: boolean; rememberMe?: boolean;
} }
export class ContactFormDto {
@ApiProperty({ example: 'Jean', description: 'First name' })
@IsString()
@MinLength(1)
firstName: string;
@ApiProperty({ example: 'Dupont', description: 'Last name' })
@IsString()
@MinLength(1)
lastName: string;
@ApiProperty({ example: 'jean@acme.com', description: 'Sender email' })
@IsEmail({}, { message: 'Invalid email format' })
email: string;
@ApiPropertyOptional({ example: 'Acme Logistics', description: 'Company name' })
@IsString()
@IsOptional()
company?: string;
@ApiPropertyOptional({ example: '+33 6 12 34 56 78', description: 'Phone number' })
@IsString()
@IsOptional()
phone?: string;
@ApiProperty({ example: 'demo', description: 'Subject category' })
@IsString()
@MinLength(1)
subject: string;
@ApiProperty({ example: 'Bonjour, je souhaite...', description: 'Message body' })
@IsString()
@MinLength(10)
message: string;
}
export class ForgotPasswordDto { export class ForgotPasswordDto {
@ApiProperty({ @ApiProperty({
example: 'john.doe@acme.com', example: 'john.doe@acme.com',

View File

@ -19,6 +19,7 @@ import {
ArrowRight, ArrowRight,
} from 'lucide-react'; } from 'lucide-react';
import { LandingHeader, LandingFooter } from '@/components/layout'; import { LandingHeader, LandingFooter } from '@/components/layout';
import { sendContactForm } from '@/lib/api/auth';
export default function ContactPage() { export default function ContactPage() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@ -51,11 +52,22 @@ export default function ContactPage() {
setError(''); setError('');
setIsSubmitting(true); setIsSubmitting(true);
// Simulate form submission try {
await new Promise((resolve) => setTimeout(resolve, 1500)); await sendContactForm({
firstName: formData.firstName,
setIsSubmitting(false); lastName: formData.lastName,
setIsSubmitted(true); email: formData.email,
company: formData.company || undefined,
phone: formData.phone || undefined,
subject: formData.subject,
message: formData.message,
});
setIsSubmitted(true);
} catch (err: any) {
setError(err.message || "Une erreur est survenue lors de l'envoi. Veuillez réessayer.");
} finally {
setIsSubmitting(false);
}
}; };
const handleChange = ( const handleChange = (

View File

@ -71,6 +71,22 @@ export async function getCurrentUser(): Promise<UserPayload> {
return get<UserPayload>('/api/v1/auth/me'); return get<UserPayload>('/api/v1/auth/me');
} }
/**
* Contact form send message to contact@xpeditis.com
* POST /api/v1/auth/contact
*/
export async function sendContactForm(data: {
firstName: string;
lastName: string;
email: string;
company?: string;
phone?: string;
subject: string;
message: string;
}): Promise<{ message: string }> {
return post<{ message: string }>('/api/v1/auth/contact', data, false);
}
/** /**
* Forgot password request reset email * Forgot password request reset email
* POST /api/v1/auth/forgot-password * POST /api/v1/auth/forgot-password

View File

@ -24,8 +24,8 @@ export {
ApiError, ApiError,
} from './client'; } from './client';
// Authentication (7 endpoints) // Authentication (8 endpoints)
export { register, login, refreshToken, logout, getCurrentUser, forgotPassword, resetPassword } from './auth'; export { register, login, refreshToken, logout, getCurrentUser, forgotPassword, resetPassword, sendContactForm } from './auth';
// Rates (4 endpoints) // Rates (4 endpoints)
export { searchRates, searchCsvRates, getAvailableCompanies, getFilterOptions } from './rates'; export { searchRates, searchCsvRates, getAvailableCompanies, getFilterOptions } from './rates';