diff --git a/app/Console/Commands/TestMailCommand.php b/app/Console/Commands/TestMailCommand.php new file mode 100644 index 0000000..48f8df9 --- /dev/null +++ b/app/Console/Commands/TestMailCommand.php @@ -0,0 +1,80 @@ +argument('email'); + $defaultMailer = config('mail.default'); + $smtpConfig = config("mail.mailers.{$defaultMailer}", []); + $fromAddress = config('mail.from.address'); + $fromName = config('mail.from.name'); + + $this->info('=== Jouwdriedee.nl E-mail & SMTP Diagnostiek ==='); + $this->table(['Instelling', 'Waarde'], [ + ['Actieve Mailer (MAIL_MAILER)', $defaultMailer], + ['Verzender Adres (MAIL_FROM_ADDRESS)', $fromAddress], + ['Verzender Naam (MAIL_FROM_NAME)', $fromName], + ['SMTP Host', $smtpConfig['host'] ?? 'N.v.t. (geen smtp)'], + ['SMTP Poort', $smtpConfig['port'] ?? 'N.v.t.'], + ['SMTP Gebruiker', ! empty($smtpConfig['username']) ? $smtpConfig['username'] : '(geen / leeg)'], + ['SMTP Wachtwoord ingesteld?', ! empty($smtpConfig['password']) ? 'Ja' : 'Nee'], + ['Ontvanger', $recipient], + ]); + + if ($defaultMailer === 'log') { + $this->warn("⚠️ WAARSCHUWING: MAIL_MAILER staat momenteel op 'log'!"); + $this->warn('Dit betekent dat er GEEN echte mail via internet wordt verzonden naar Gmail.'); + $this->warn('De mail wordt weggeschreven naar storage/logs/laravel.log.'); + $this->warn('Stel MAIL_MAILER=smtp in met geldige SMTP-gegevens op Coolify om echte mails te ontvangen.'); + } + + $this->line("\nVersturen van testmail naar {$recipient}..."); + + try { + Mail::raw("Dit is een test e-mail vanaf Jouwdriedee.nl om te controleren of de mailserver en bezorging naar {$recipient} correct functioneert.\n\nVerzonden op: ".now()->toIso8601String(), function ($message) use ($recipient) { + $message->to($recipient) + ->subject('Test E-mail vanaf Jouwdriedee.nl'); + }); + + $this->info("✓ Test e-mail succesvol verzonden via driver '{$defaultMailer}'!"); + if ($defaultMailer === 'log') { + $this->comment('Controleer storage/logs/laravel.log om de gelogde e-mail te zien.'); + } else { + $this->info("Controleer nu je inbox (en eventueel spambox) op {$recipient}."); + } + + return self::SUCCESS; + } catch (Throwable $e) { + $this->error('❌ FOUT BIJ HET VERZENDEN:'); + $this->error($e->getMessage()); + $this->line("\nStack trace:"); + $this->line($e->getTraceAsString()); + + return self::FAILURE; + } + } +} diff --git a/app/Http/Controllers/Admin/ContentController.php b/app/Http/Controllers/Admin/ContentController.php index cd2101c..9a5d4fc 100644 --- a/app/Http/Controllers/Admin/ContentController.php +++ b/app/Http/Controllers/Admin/ContentController.php @@ -6,6 +6,8 @@ use App\Http\Controllers\Controller; use App\Models\Setting; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; use Illuminate\View\View; class ContentController extends Controller @@ -132,4 +134,38 @@ class ContentController extends Controller return redirect()->route('admin.content.index') ->with('success', 'Alle teksten en lay-out instellingen zijn hersteld naar de standaarden.'); } + + /** + * Send a test email to verify mail delivery (e.g. to Gmail). + */ + public function sendTestMail(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'test_email' => 'required|email', + ]); + + $recipient = $validated['test_email']; + $defaultMailer = config('mail.default'); + $fromAddress = config('mail.from.address'); + + try { + Mail::raw("Dit is een test e-mail vanaf Jouwdriedee.nl om de e-mail bezorging naar {$recipient} te controleren.\n\nActieve mailer: {$defaultMailer}\nAfzender: {$fromAddress}\nDatum: ".now()->format('d-m-Y H:i:s'), function ($message) use ($recipient) { + $message->to($recipient) + ->subject('Test E-mail vanaf Jouwdriedee.nl'); + }); + + if ($defaultMailer === 'log') { + return redirect()->back() + ->with('warning', "De testmail is weggeschreven naar de serverlogs (MAIL_MAILER staat op 'log'). Er is GEEN echte e-mail naar {$recipient} verzonden. Stel op Coolify geldige SMTP-gegevens in (bijv. Brevo, Resend of je hosting SMTP) om echte e-mails te bezorgen."); + } + + return redirect()->back() + ->with('success', "Test e-mail succesvol verzonden via driver '{$defaultMailer}' naar {$recipient}! Controleer je inbox en eventueel je spambox."); + } catch (\Throwable $e) { + Log::error('Test mail error: '.$e->getMessage(), ['trace' => $e->getTraceAsString()]); + + return redirect()->back() + ->with('error', "Fout bij verzenden van e-mail via {$defaultMailer}: ".$e->getMessage()); + } + } } diff --git a/app/Http/Controllers/Admin/MakerWorldController.php b/app/Http/Controllers/Admin/MakerWorldController.php index b376612..b004ebe 100644 --- a/app/Http/Controllers/Admin/MakerWorldController.php +++ b/app/Http/Controllers/Admin/MakerWorldController.php @@ -337,13 +337,13 @@ class MakerWorldController extends Controller if ($result['status'] === '2fa_required') { return redirect() - ->route('admin.makerworld.index') + ->route('admin.makerworld.index', ['tab' => 'auth']) ->with('warning', $result['message']) ->with('makerworld_2fa_account', $result['account']); } return redirect() - ->route('admin.makerworld.index') + ->route('admin.makerworld.index', ['tab' => 'auth']) ->with('error', $result['message']); } @@ -369,11 +369,28 @@ class MakerWorldController extends Controller } return redirect() - ->route('admin.makerworld.index') + ->route('admin.makerworld.index', ['tab' => 'auth']) ->with('error', $result['message']) ->with('makerworld_2fa_account', $validated['account']); } + /** + * Resend verification code for 2FA manually on request. + */ + public function resendCode(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'account' => 'required|string', + ]); + + $sent = $this->makerWorld->sendEmailVerificationCode($validated['account']); + + return redirect() + ->route('admin.makerworld.index', ['tab' => 'auth']) + ->with($sent ? 'success' : 'error', $sent ? 'Er is een nieuwe verificatiecode verstuurd naar je e-mail.' : 'Kon verificatiecode niet opnieuw verzenden.') + ->with('makerworld_2fa_account', $validated['account']); + } + /** * Save MakerWorld session / bearer token directly. */ diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 29824ee..da1501c 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -11,6 +11,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; @@ -61,6 +62,7 @@ class ForgotPasswordController extends Controller try { Mail::to($user->email)->send(new PasswordResetMail($user, $resetUrl)); } catch (Throwable $e) { + Log::error("Fout bij verzenden van wachtwoordherstel e-mail naar {$user->email}: ".$e->getMessage()); report($e); } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 459de51..d2a7d67 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -9,6 +9,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Illuminate\View\View; use Throwable; @@ -56,6 +57,7 @@ class RegisterController extends Controller try { Mail::to($user->email)->send(new WelcomeMail($user)); } catch (Throwable $e) { + Log::error("Fout bij verzenden van welkomstmail naar {$user->email}: ".$e->getMessage()); report($e); } diff --git a/app/Services/MakerWorldService.php b/app/Services/MakerWorldService.php index c9337dd..dda6476 100644 --- a/app/Services/MakerWorldService.php +++ b/app/Services/MakerWorldService.php @@ -111,14 +111,12 @@ class MakerWorldService ]; } - // Check if 2FA code is needed + // Check if 2FA code is needed (Bambu Lab sends the code automatically when verifyCode is returned) if (isset($data['loginType']) && $data['loginType'] === 'verifyCode') { - $this->sendEmailVerificationCode($account); - return [ 'status' => '2fa_required', 'account' => $account, - 'message' => 'Verificatiecode vereist. We hebben een 6-cijferige code naar je e-mail gestuurd.', + 'message' => 'Verificatiecode vereist. Bambu Lab heeft een 6-cijferige verificatiecode naar je e-mail gestuurd.', ]; } diff --git a/resources/views/admin/content/index.blade.php b/resources/views/admin/content/index.blade.php index 38ce351..6de4e84 100644 --- a/resources/views/admin/content/index.blade.php +++ b/resources/views/admin/content/index.blade.php @@ -97,6 +97,16 @@ 6. Algemeen & Thema + + @@ -508,6 +518,119 @@ + + + +
+
+
+
+

+ + E-mail & SMTP Status +

+

Controleer waarom wachtwoordherstel- en registratiemails wel of niet aankomen op Gmail.

+
+ @php + $activeMailer = config('mail.default'); + @endphp + + Driver: {{ strtoupper($activeMailer) }} + +
+ + @if($activeMailer === 'log') +
+
+ + Belangrijke Oorzaak: MAIL_MAILER staat op 'log' +
+

+ Momenteel worden uitgaande e-mails (zoals wachtwoord vergeten en welkomstmails) weggeschreven naar de serverlogbestanden en niet via internet naar Gmail verzonden. +

+

+ Om echte e-mails te versturen, moet op de server (Coolify Environment Variables) MAIL_MAILER=smtp ingesteld worden met de bijbehorende SMTP-host, poort en wachtwoord. +

+
+ @endif + + +
+
+
Huidige Server Mail Instellingen
+
+ MAIL_MAILER: + {{ config('mail.default') }} +
+
+ MAIL_HOST: + {{ config('mail.mailers.smtp.host') ?: '(geen)' }} +
+
+ MAIL_PORT: + {{ config('mail.mailers.smtp.port') ?: '(geen)' }} +
+
+ MAIL_USERNAME: + {{ config('mail.mailers.smtp.username') ?: '(geen)' }} +
+
+ MAIL_FROM_ADDRESS: + {{ config('mail.from.address') }} +
+
+ +
+
Gmail Bezorging Checklist
+
    +
  • + 1. + SMTP Provider: Gebruik een werkende SMTP server (bijv. Brevo, Resend, Mailgun of Versio mail). +
  • +
  • + 2. + SPF DNS Record: Google vereist een SPF TXT-record op jouwdriedee.nl. +
  • +
  • + 3. + DKIM & DMARC: Zonder DKIM plaatst Gmail emails van nieuwe domeinen direct in de Spammap. +
  • +
+
+
+ + +
+
+ + Direct een Test E-mail Versturen +
+

+ Vul hier een e-mailadres in (bijv. je eigen Gmail) om direct te testen of e-mails aankomen en of de verbinding met de mailserver slaagt. +

+
+ + +
+
+ +
+
+
diff --git a/resources/views/admin/makerworld/index.blade.php b/resources/views/admin/makerworld/index.blade.php index 5baaa7f..1c0142e 100644 --- a/resources/views/admin/makerworld/index.blade.php +++ b/resources/views/admin/makerworld/index.blade.php @@ -62,7 +62,65 @@
@endif - + + @if(session('makerworld_2fa_account')) +
+
+
+ +
+
+ + Stap 2: Twee-staps Verificatie (2FA) + +

Bambu Lab Verificatiecode

+

+ Vul de 6-cijferige verificatiecode in die Bambu Lab zojuist heeft verzonden naar {{ session('makerworld_2fa_account') }}. +

+
+ +
+ @csrf + + +
+ +
+ +
+ +
+
+ +
+
+ @csrf + + +
+ • + + Ander account proberen + +
+
+
+ @endif + +
diff --git a/resources/views/admin/makerworld/staging.blade.php b/resources/views/admin/makerworld/staging.blade.php index d09ed33..007db4c 100644 --- a/resources/views/admin/makerworld/staging.blade.php +++ b/resources/views/admin/makerworld/staging.blade.php @@ -353,3 +353,4 @@ function artikelBeheerApp() { } @endsection + diff --git a/routes/web.php b/routes/web.php index e795604..64c78a2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -115,6 +115,7 @@ Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(fun Route::get('/content', [ContentController::class, 'index'])->name('content.index'); Route::post('/content', [ContentController::class, 'update'])->name('content.update'); Route::post('/content/reset', [ContentController::class, 'reset'])->name('content.reset'); + Route::post('/content/test-mail', [ContentController::class, 'sendTestMail'])->name('content.test-mail'); // Producten Beheer Route::resource('products', AdminProductController::class)->except(['show']); @@ -131,6 +132,7 @@ Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(fun Route::get('/makerworld', [MakerWorldController::class, 'index'])->name('makerworld.index'); Route::post('/makerworld/login', [MakerWorldController::class, 'login'])->name('makerworld.login'); Route::post('/makerworld/verify-code', [MakerWorldController::class, 'verifyCode'])->name('makerworld.verify-code'); + Route::post('/makerworld/resend-code', [MakerWorldController::class, 'resendCode'])->name('makerworld.resend-code'); Route::post('/makerworld/token', [MakerWorldController::class, 'saveToken'])->name('makerworld.token'); Route::post('/makerworld/disconnect', [MakerWorldController::class, 'disconnect'])->name('makerworld.disconnect'); Route::get('/makerworld/preview', [MakerWorldController::class, 'preview'])->name('makerworld.preview'); diff --git a/tests/Feature/AuthAndEmailTest.php b/tests/Feature/AuthAndEmailTest.php index efc317d..b4c5a1f 100644 --- a/tests/Feature/AuthAndEmailTest.php +++ b/tests/Feature/AuthAndEmailTest.php @@ -10,6 +10,7 @@ use App\Models\Material; use App\Models\Product; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Mail; @@ -215,4 +216,33 @@ class AuthAndEmailTest extends TestCase && str_contains($mail->envelope()->subject, 'Bestelbevestiging'); }); } + + public function test_admin_can_trigger_test_email_via_web_and_cli(): void + { + Mail::fake(); + + $admin = User::create([ + 'name' => 'Admin Jouwdriedee', + 'email' => 'info@jouwdriedee.nl', + 'password' => bcrypt('Jw3D!x9#QmK7$vR2'), + 'is_admin' => true, + ]); + + $this->actingAs($admin); + + // Web endpoint + $response = $this->post(route('admin.content.test-mail'), [ + 'test_email' => 'testgmail@gmail.com', + ]); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + + // CLI command + $exitCode = Artisan::call('app:test-mail', [ + 'email' => 'testcli@gmail.com', + ]); + + $this->assertEquals(0, $exitCode); + } } diff --git a/tests/Feature/MakerWorldImporterTest.php b/tests/Feature/MakerWorldImporterTest.php index aabcb3e..18c3ca8 100644 --- a/tests/Feature/MakerWorldImporterTest.php +++ b/tests/Feature/MakerWorldImporterTest.php @@ -215,4 +215,72 @@ class MakerWorldImporterTest extends TestCase $disconnectResponse->assertRedirect(); $this->assertNull(Setting::get('makerworld_token')); } + + public function test_makerworld_login_with_2fa_redirects_to_auth_tab_without_duplicate_email(): void + { + Http::fake([ + 'https://api.bambulab.com/v1/user-service/user/login' => Http::response([ + 'loginType' => 'verifyCode', + 'message' => 'Verification code sent', + ], 200), + 'https://api.bambulab.com/v1/user-service/user/sendemail/code' => Http::response([ + 'success' => true, + ], 200), + ]); + + $response = $this->post(route('admin.makerworld.login'), [ + 'account' => 'test2fa@jouwdriedee.nl', + 'password' => 'secret_pass', + ]); + + $response->assertRedirect(route('admin.makerworld.index', ['tab' => 'auth'])); + $response->assertSessionHas('makerworld_2fa_account', 'test2fa@jouwdriedee.nl'); + + // Verify that sendemail/code was NOT called automatically (Bambu Lab already sent it on login) + Http::assertNotSent(function ($request) { + return str_contains($request->url(), 'sendemail/code'); + }); + } + + public function test_makerworld_can_verify_2fa_code(): void + { + Http::fake([ + 'https://api.bambulab.com/v1/user-service/user/login' => Http::response([ + 'accessToken' => 'token_after_2fa_code_9999', + ], 200), + 'https://api.bambulab.com/v1/design-user-service/my/preference' => Http::response([ + 'uid' => 12345, + 'name' => 'Verified User', + ], 200), + ]); + + $response = $this->post(route('admin.makerworld.verify-code'), [ + 'account' => 'test2fa@jouwdriedee.nl', + 'code' => '123456', + ]); + + $response->assertRedirect(route('admin.makerworld.index', ['tab' => 'collections'])); + $this->assertSame('token_after_2fa_code_9999', Setting::get('makerworld_token')); + } + + public function test_makerworld_can_manually_resend_2fa_code(): void + { + Http::fake([ + 'https://api.bambulab.com/v1/user-service/user/sendemail/code' => Http::response([ + 'success' => true, + ], 200), + ]); + + $response = $this->post(route('admin.makerworld.resend-code'), [ + 'account' => 'test2fa@jouwdriedee.nl', + ]); + + $response->assertRedirect(route('admin.makerworld.index', ['tab' => 'auth'])); + $response->assertSessionHas('success'); + + Http::assertSent(function ($request) { + return str_contains($request->url(), 'sendemail/code') && + $request['email'] === 'test2fa@jouwdriedee.nl'; + }); + } }