diff --git a/apps/backend/src/application/controllers/auth.controller.ts b/apps/backend/src/application/controllers/auth.controller.ts index fda3c3f..d35f172 100644 --- a/apps/backend/src/application/controllers/auth.controller.ts +++ b/apps/backend/src/application/controllers/auth.controller.ts @@ -8,6 +8,8 @@ import { Get, Inject, NotFoundException, + InternalServerErrorException, + Logger, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { AuthService } from '../auth/auth.service'; @@ -18,7 +20,9 @@ import { RefreshTokenDto, ForgotPasswordDto, ResetPasswordDto, + ContactFormDto, } from '../dto/auth-login.dto'; +import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port'; import { Public } from '../decorators/public.decorator'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; @@ -39,10 +43,13 @@ import { InvitationService } from '../services/invitation.service'; @ApiTags('Authentication') @Controller('auth') export class AuthController { + private readonly logger = new Logger(AuthController.name); + constructor( private readonly authService: AuthService, @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' }; } + /** + * 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 = { + 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 = ` +
+
+

Nouveau message de contact

+
+
+ + + + + + + + + + ${dto.company ? `` : ''} + ${dto.phone ? `` : ''} + + + + +
Nom${dto.firstName} ${dto.lastName}
Email${dto.email}
Entreprise${dto.company}
Téléphone${dto.phone}
Sujet${subjectLabel}
+
+

Message :

+

${dto.message}

+
+
+
+

Xpeditis — Formulaire de contact

+
+
+ `; + + 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 */ diff --git a/apps/backend/src/application/dto/auth-login.dto.ts b/apps/backend/src/application/dto/auth-login.dto.ts index 26be29a..93a292c 100644 --- a/apps/backend/src/application/dto/auth-login.dto.ts +++ b/apps/backend/src/application/dto/auth-login.dto.ts @@ -37,6 +37,42 @@ export class LoginDto { 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 { @ApiProperty({ example: 'john.doe@acme.com', diff --git a/apps/frontend/app/contact/page.tsx b/apps/frontend/app/contact/page.tsx index baaa963..440701e 100644 --- a/apps/frontend/app/contact/page.tsx +++ b/apps/frontend/app/contact/page.tsx @@ -19,6 +19,7 @@ import { ArrowRight, } from 'lucide-react'; import { LandingHeader, LandingFooter } from '@/components/layout'; +import { sendContactForm } from '@/lib/api/auth'; export default function ContactPage() { const [formData, setFormData] = useState({ @@ -51,11 +52,22 @@ export default function ContactPage() { setError(''); setIsSubmitting(true); - // Simulate form submission - await new Promise((resolve) => setTimeout(resolve, 1500)); - - setIsSubmitting(false); - setIsSubmitted(true); + try { + await sendContactForm({ + firstName: formData.firstName, + lastName: formData.lastName, + 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 = ( diff --git a/apps/frontend/src/lib/api/auth.ts b/apps/frontend/src/lib/api/auth.ts index 4470c10..0129251 100644 --- a/apps/frontend/src/lib/api/auth.ts +++ b/apps/frontend/src/lib/api/auth.ts @@ -71,6 +71,22 @@ export async function getCurrentUser(): Promise { return get('/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 * POST /api/v1/auth/forgot-password diff --git a/apps/frontend/src/lib/api/index.ts b/apps/frontend/src/lib/api/index.ts index 6ee2f25..913d43e 100644 --- a/apps/frontend/src/lib/api/index.ts +++ b/apps/frontend/src/lib/api/index.ts @@ -24,8 +24,8 @@ export { ApiError, } from './client'; -// Authentication (7 endpoints) -export { register, login, refreshToken, logout, getCurrentUser, forgotPassword, resetPassword } from './auth'; +// Authentication (8 endpoints) +export { register, login, refreshToken, logout, getCurrentUser, forgotPassword, resetPassword, sendContactForm } from './auth'; // Rates (4 endpoints) export { searchRates, searchCsvRates, getAvailableCompanies, getFilterOptions } from './rates';