feat(contact): wire contact form to real backend — sends email to contact@xpeditis.com
This commit is contained in:
parent
ed0f43ba32
commit
74221d576e
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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 = (
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user