diff --git a/.env.example b/.env.example index 1e642cb..b25ec48 100644 --- a/.env.example +++ b/.env.example @@ -59,6 +59,7 @@ MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" +CONTACT_EMAIL= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index dbad520..bcb5ca4 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -2,10 +2,71 @@ namespace App\Http\Controllers; +use App\Http\Requests\ContactRequest; +use App\Mail\ContactConfirmationMail; +use App\Mail\ContactMessageMail; +use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\View\View; +use Throwable; + +/** + * Contact form. Nothing is stored: the message is emailed to the store + * (CONTACT_EMAIL) and the sender gets a generic confirmation. Both are sent + * synchronously so a failed store email can be reported back on the form. + * + * Limited per IP in here rather than with throttle middleware, so the limit + * shows as a message on the form instead of a bare 429 page. + */ class ContactController extends Controller { - public function index(string $locale) + private const SEND_LIMIT = 3; + private const SEND_DECAY_SECONDS = 600; + + public function index(string $locale): View { return view('contact'); } + + public function send(string $locale, ContactRequest $request): RedirectResponse + { + $key = 'contact:'.$request->ip(); + + if (RateLimiter::tooManyAttempts($key, self::SEND_LIMIT)) { + return back()->withInput()->with('contact_error', __('storefront.contact.too_many')); + } + + RateLimiter::hit($key, self::SEND_DECAY_SECONDS); + + $data = $request->validated(); + $to = config('services.contact.email'); + + try { + if (blank($to)) { + throw new \RuntimeException('CONTACT_EMAIL is not set.'); + } + + Mail::to($to)->send(new ContactMessageMail( + $data['name'], + $data['email'], + $data['message'], + $locale, + )); + } catch (Throwable $e) { + Log::error('Contact form: store email failed', ['exception' => $e]); + + return back()->withInput()->with('contact_error', __('storefront.contact.send_failed')); + } + + // The store already has the message, so a failed confirmation is only logged. + try { + Mail::to($data['email'])->locale($locale)->send(new ContactConfirmationMail); + } catch (Throwable $e) { + Log::warning('Contact form: confirmation email failed', ['exception' => $e]); + } + + return redirect()->route('contact')->with('status', __('storefront.contact.sent')); + } } diff --git a/app/Http/Requests/ContactRequest.php b/app/Http/Requests/ContactRequest.php new file mode 100644 index 0000000..f79c293 --- /dev/null +++ b/app/Http/Requests/ContactRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', 'max:100'], + 'email' => ['required', 'email', 'max:255'], + 'message' => ['required', 'string', 'max:5000'], + ]; + } +} diff --git a/app/Mail/ContactConfirmationMail.php b/app/Mail/ContactConfirmationMail.php new file mode 100644 index 0000000..4f3828e --- /dev/null +++ b/app/Mail/ContactConfirmationMail.php @@ -0,0 +1,25 @@ +senderEmail, $this->senderName)], + subject: "Νέο μήνυμα επικοινωνίας από {$this->senderName}", + ); + } + + public function content(): Content + { + return new Content(view: 'emails.contact-message'); + } +} diff --git a/config/services.php b/config/services.php index 7c13ec1..d7dbed7 100644 --- a/config/services.php +++ b/config/services.php @@ -35,6 +35,11 @@ ], ], + // Where contact-form messages are sent (ContactController). + 'contact' => [ + 'email' => env('CONTACT_EMAIL'), + ], + 'stoic' => [ 'sso_secret' => env('STOIC_SSO_SECRET'), 'host' => env('STOIC_HOST'), diff --git a/resources/views/account/orders/show.blade.php b/resources/views/account/orders/show.blade.php index dd87c84..5049a75 100644 --- a/resources/views/account/orders/show.blade.php +++ b/resources/views/account/orders/show.blade.php @@ -70,8 +70,15 @@
+ {{-- Linked only while the product still exists and is published; + otherwise the name stays plain text (the order keeps it). --}} + @php($lineProduct = $line->purchasable?->product)

- {{ $line->description }} + @if ($lineProduct?->status === 'published') + {{ $line->description }} + @else + {{ $line->description }} + @endif × {{ $line->quantity }}

diff --git a/resources/views/components/review-card.blade.php b/resources/views/components/review-card.blade.php index 321cb60..0cddb91 100644 --- a/resources/views/components/review-card.blade.php +++ b/resources/views/components/review-card.blade.php @@ -15,11 +15,13 @@ @if (!empty($review['image']))
- Φωτογραφία από τον αγοραστή + + Φωτογραφία από τον αγοραστή +
@endif diff --git a/resources/views/contact.blade.php b/resources/views/contact.blade.php index 467e561..bfc14da 100644 --- a/resources/views/contact.blade.php +++ b/resources/views/contact.blade.php @@ -24,23 +24,35 @@ Έχεις απορία, ιδέα ή θες να μας πεις κάτι; Γράψε μας το μήνυμά σου και θα σου απαντήσουμε το συντομότερο δυνατό.

-
+ + + @csrf
- - + + - - + +
- - + + {{ old('message') }} + @if (session('contact_error')) + + @endif +
Υποβολή
diff --git a/resources/views/emails/contact-confirmation.blade.php b/resources/views/emails/contact-confirmation.blade.php new file mode 100644 index 0000000..0c1c6eb --- /dev/null +++ b/resources/views/emails/contact-confirmation.blade.php @@ -0,0 +1,17 @@ +@extends('emails.layout') + +@section('title', __('storefront.contact.confirmation_subject')) + +@section('preheader', __('storefront.contact.confirmation_preheader')) + +@section('content') +

+ {{ __('storefront.contact.confirmation_heading') }} +

+ +

{{ __('storefront.contact.confirmation_body') }}

+@endsection + +@section('footer') +

{{ __('storefront.contact.confirmation_footer') }}

+@endsection diff --git a/resources/views/emails/contact-message.blade.php b/resources/views/emails/contact-message.blade.php new file mode 100644 index 0000000..3c02daa --- /dev/null +++ b/resources/views/emails/contact-message.blade.php @@ -0,0 +1,21 @@ +@extends('emails.layout') + +@section('title', 'Νέο μήνυμα επικοινωνίας') + +@section('preheader', "Από {$senderName} ({$senderEmail})") + +@section('content') +

+ Νέο μήνυμα επικοινωνίας +

+ +

Όνομα: {{ $senderName }}

+

Email: {{ $senderEmail }}

+

Γλώσσα: {{ strtoupper($senderLocale) }}

+ +

{{ $body }}

+@endsection + +@section('footer') +

Στάλθηκε από τη φόρμα επικοινωνίας του 3dealer. Απάντησε σε αυτό το email για να απαντήσεις στον αποστολέα.

+@endsection diff --git a/routes/web.php b/routes/web.php index 9582b58..24dca18 100644 --- a/routes/web.php +++ b/routes/web.php @@ -57,6 +57,7 @@ Route::get('/search', [SearchController::class, 'show'])->name('search'); Route::get('/contact', [ContactController::class, 'index'])->name('contact'); + Route::post('/contact', [ContactController::class, 'send'])->name('contact.send'); Route::get('/terms-and-conditions', [LegalPageController::class, 'terms'])->name( 'legal.terms',