diff --git a/.env.example b/.env.example index fc37973..3bfebb2 100644 --- a/.env.example +++ b/.env.example @@ -53,8 +53,8 @@ MAIL_HOST=127.0.0.1 MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null -MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" +MAIL_FROM_ADDRESS="info@jouwdriedee.nl" +MAIL_FROM_NAME="Jouwdriedee.nl" AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/app/Console/Commands/AdminCredentialsCommand.php b/app/Console/Commands/AdminCredentialsCommand.php new file mode 100644 index 0000000..a9e6cb9 --- /dev/null +++ b/app/Console/Commands/AdminCredentialsCommand.php @@ -0,0 +1,69 @@ +first(); + + if (! $admin || $this->option('reset')) { + $newPassword = Str::password(16); + + $admin = User::updateOrCreate( + ['email' => $email], + [ + 'name' => 'Jouwdriedee Beheer', + 'password' => Hash::make($newPassword), + 'is_admin' => true, + 'email_verified_at' => now(), + ] + ); + + $this->info('Admin gebruiker succesvol aangemaakt/bijgewerkt met een nieuw wachtwoord:'); + $this->table(['Veld', 'Waarde'], [ + ['E-mailadres', $email], + ['Nieuw Wachtwoord', $newPassword], + ['Admin Status', 'Ja (is_admin = true)'], + ]); + + return self::SUCCESS; + } + + $this->info("Admin gebruiker '{$email}' bestaat reeds in het systeem."); + $this->table(['Veld', 'Waarde'], [ + ['E-mailadres', $email], + ['Naam', $admin->name], + ['Admin Status', $admin->isAdmin() ? 'Ja' : 'Nee'], + ['Aangemaakt op', $admin->created_at->format('d-m-Y H:i')], + ]); + + $this->line("Tip: Draai 'php artisan app:admin-credentials --reset' om een nieuw random wachtwoord te genereren."); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php new file mode 100644 index 0000000..427430c --- /dev/null +++ b/app/Http/Controllers/AccountController.php @@ -0,0 +1,70 @@ +id) + ->orWhere('customer_email', $user->email) + ->with('items') + ->latest() + ->get(); + + return view('account.index', compact('user', 'orders')); + } + + /** + * Update customer profile info. + */ + public function updateProfile(Request $request): RedirectResponse + { + $user = Auth::user(); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], + ]); + + $user->update($validated); + + return back()->with('success', 'Jouw profielgegevens zijn succesvol bijgewerkt.'); + } + + /** + * Update customer password. + */ + public function updatePassword(Request $request): RedirectResponse + { + $user = Auth::user(); + + $validated = $request->validate([ + 'current_password' => 'required|current_password', + 'password' => 'required|string|min:8|confirmed', + ], [ + 'current_password.current_password' => 'Het huidige wachtwoord is onjuist.', + 'password.min' => 'Het nieuwe wachtwoord moet minimaal 8 tekens lang zijn.', + 'password.confirmed' => 'De nieuwe wachtwoorden komen niet met elkaar overeen.', + ]); + + $user->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('success', 'Jouw wachtwoord is succesvol gewijzigd.'); + } +} diff --git a/app/Http/Controllers/Admin/CategoryController.php b/app/Http/Controllers/Admin/CategoryController.php index 1eeac1d..595a418 100644 --- a/app/Http/Controllers/Admin/CategoryController.php +++ b/app/Http/Controllers/Admin/CategoryController.php @@ -46,4 +46,3 @@ class CategoryController extends Controller ->with('success', "Categorie '{$category->name}' is succesvol bijgewerkt!"); } } - diff --git a/app/Http/Controllers/Admin/ContentController.php b/app/Http/Controllers/Admin/ContentController.php index 675e6ed..cd2101c 100644 --- a/app/Http/Controllers/Admin/ContentController.php +++ b/app/Http/Controllers/Admin/ContentController.php @@ -133,4 +133,3 @@ class ContentController extends Controller ->with('success', 'Alle teksten en lay-out instellingen zijn hersteld naar de standaarden.'); } } - diff --git a/app/Http/Controllers/Admin/OrderController.php b/app/Http/Controllers/Admin/OrderController.php index aa7a6f8..07a8fb3 100644 --- a/app/Http/Controllers/Admin/OrderController.php +++ b/app/Http/Controllers/Admin/OrderController.php @@ -51,4 +51,3 @@ class OrderController extends Controller ->with('success', "Bestelling #{$order->order_number} status is gewijzigd naar '{$validated['status']}'!"); } } - diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index fa54c50..ad6cfab 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -132,4 +132,3 @@ class ProductController extends Controller ->with('success', "Product '{$name}' is verwijderd."); } } - diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php new file mode 100644 index 0000000..29824ee --- /dev/null +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -0,0 +1,124 @@ +validate([ + 'email' => 'required|email', + ]); + + $user = User::where('email', $request->input('email'))->first(); + + // Always show friendly message to prevent email enumeration + if (! $user) { + return back()->with('success', 'Als dit e-mailadres bij ons bekend is, hebben we zojuist een herstellink verstuurd.'); + } + + $token = Str::random(64); + + DB::table('password_reset_tokens')->updateOrInsert( + ['email' => $user->email], + [ + 'token' => Hash::make($token), + 'created_at' => Carbon::now(), + ] + ); + + $resetUrl = route('password.reset', [ + 'token' => $token, + 'email' => $user->email, + ]); + + try { + Mail::to($user->email)->send(new PasswordResetMail($user, $resetUrl)); + } catch (Throwable $e) { + report($e); + } + + return back()->with('success', 'Als dit e-mailadres bij ons bekend is, hebben we zojuist een herstellink verstuurd.'); + } + + /** + * Display the password reset view for the given token. + */ + public function showResetForm(Request $request, string $token): View + { + return view('auth.reset-password', [ + 'token' => $token, + 'email' => $request->input('email'), + ]); + } + + /** + * Reset the given user's password. + */ + public function reset(Request $request): RedirectResponse + { + $request->validate([ + 'token' => 'required|string', + 'email' => 'required|email', + 'password' => 'required|string|min:8|confirmed', + ], [ + 'password.min' => 'Het nieuwe wachtwoord moet minimaal 8 tekens lang zijn.', + 'password.confirmed' => 'De wachtwoorden komen niet met elkaar overeen.', + ]); + + $record = DB::table('password_reset_tokens') + ->where('email', $request->input('email')) + ->first(); + + if (! $record || ! Hash::check($request->input('token'), $record->token)) { + throw ValidationException::withMessages([ + 'email' => ['Deze herstellink is ongeldig of verlopen.'], + ]); + } + + if (Carbon::parse($record->created_at)->addMinutes(60)->isPast()) { + DB::table('password_reset_tokens')->where('email', $request->input('email'))->delete(); + throw ValidationException::withMessages([ + 'email' => ['Deze herstellink is verlopen. Vraag alsjeblieft een nieuwe link aan.'], + ]); + } + + $user = User::where('email', $request->input('email'))->firstOrFail(); + $user->update([ + 'password' => Hash::make($request->input('password')), + ]); + + DB::table('password_reset_tokens')->where('email', $request->input('email'))->delete(); + + Auth::login($user); + + return redirect()->route('account.index') + ->with('success', 'Jouw wachtwoord is succesvol gewijzigd! Je bent direct ingelogd.'); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..fcae0cf --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,72 @@ +isAdmin() + ? redirect()->route('admin.content.index') + : redirect()->route('account.index'); + } + + return view('auth.login'); + } + + /** + * Handle a login request. + */ + public function login(Request $request): RedirectResponse + { + $credentials = $request->validate([ + 'email' => 'required|email', + 'password' => 'required|string', + ]); + + $remember = $request->boolean('remember'); + + if (Auth::attempt($credentials, $remember)) { + $request->session()->regenerate(); + + $user = Auth::user(); + + if ($user->isAdmin()) { + return redirect()->intended(route('admin.content.index')) + ->with('success', "Welkom terug, {$user->name}! Je bent ingelogd als beheerder."); + } + + return redirect()->intended(route('account.index')) + ->with('success', "Welkom terug, {$user->name}!"); + } + + throw ValidationException::withMessages([ + 'email' => ['Deze inloggegevens komen niet overeen met onze gegevens.'], + ]); + } + + /** + * Log the user out. + */ + public function logout(Request $request): RedirectResponse + { + Auth::logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('home') + ->with('success', 'Je bent succesvol uitgelogd.'); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 0000000..459de51 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,67 @@ +route('account.index'); + } + + return view('auth.register'); + } + + /** + * Handle an incoming registration request. + */ + public function register(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|min:8|confirmed', + ], [ + 'name.required' => 'Vul alsjeblieft je naam in.', + 'email.required' => 'Vul alsjeblieft een geldig e-mailadres in.', + 'email.unique' => 'Er bestaat al een account met dit e-mailadres.', + 'password.required' => 'Kies een veilig wachtwoord.', + 'password.min' => 'Het wachtwoord moet minimaal 8 tekens lang zijn.', + 'password.confirmed' => 'De twee wachtwoorden komen niet met elkaar overeen.', + ]); + + $user = User::create([ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'password' => Hash::make($validated['password']), + 'is_admin' => false, + ]); + + // Stuur welkomstmail + try { + Mail::to($user->email)->send(new WelcomeMail($user)); + } catch (Throwable $e) { + report($e); + } + + Auth::login($user); + + return redirect()->route('account.index') + ->with('success', "Welkom bij Jouwdriedee.nl, {$user->name}! Je account is aangemaakt en er is een bevestiging gestuurd naar {$user->email}."); + } +} diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php index cfa284f..d8a5890 100644 --- a/app/Http/Controllers/CheckoutController.php +++ b/app/Http/Controllers/CheckoutController.php @@ -2,9 +2,11 @@ namespace App\Http\Controllers; +use App\Mail\OrderConfirmationMail; use App\Models\Order; use App\Models\OrderItem; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Str; class CheckoutController extends Controller @@ -59,6 +61,7 @@ class CheckoutController extends Controller $orderNumber = 'J3D-'.date('Y').'-'.strtoupper(Str::random(6)); $order = Order::create([ + 'user_id' => auth()->id(), 'order_number' => $orderNumber, 'customer_name' => $validated['customer_name'], 'customer_email' => $validated['customer_email'], @@ -100,6 +103,13 @@ class CheckoutController extends Controller ]); } + // Stuur mooie bestelbevestiging e-mail + try { + Mail::to($order->customer_email)->send(new OrderConfirmationMail($order->load('items'))); + } catch (\Throwable $e) { + report($e); + } + // Leeg winkelwagen na succesvolle plaatsing session()->forget('cart'); diff --git a/app/Http/Middleware/EnsureUserIsAdmin.php b/app/Http/Middleware/EnsureUserIsAdmin.php new file mode 100644 index 0000000..888b1f2 --- /dev/null +++ b/app/Http/Middleware/EnsureUserIsAdmin.php @@ -0,0 +1,28 @@ +check()) { + return redirect()->guest(route('login')) + ->with('error', 'Log eerst in met je beheerder-account om toegang te krijgen tot het admin gedeelte.'); + } + + if (! auth()->user()->isAdmin()) { + return redirect()->route('home') + ->with('error', 'Je hebt geen beheerderstoegang tot deze pagina.'); + } + + return $next($request); + } +} diff --git a/app/Mail/OrderConfirmationMail.php b/app/Mail/OrderConfirmationMail.php new file mode 100644 index 0000000..e1b2f42 --- /dev/null +++ b/app/Mail/OrderConfirmationMail.php @@ -0,0 +1,31 @@ +order->order_number.' - Jouwdriedee.nl', + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.order-confirmation', + ); + } +} diff --git a/app/Mail/PasswordResetMail.php b/app/Mail/PasswordResetMail.php new file mode 100644 index 0000000..5e879d4 --- /dev/null +++ b/app/Mail/PasswordResetMail.php @@ -0,0 +1,34 @@ +hasMany(OrderItem::class); } + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + public function getFormattedTotalAttribute(): string { return '€ '.number_format($this->total, 2, ',', '.'); diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..d0167ee 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,15 +2,15 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -#[Fillable(['name', 'email', 'password'])] +#[Fillable(['name', 'email', 'password', 'is_admin'])] #[Hidden(['password', 'remember_token'])] class User extends Authenticatable { @@ -27,6 +27,25 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'is_admin' => 'boolean', ]; } + + /** + * Check if the user has administrative privileges. + */ + public function isAdmin(): bool + { + return (bool) $this->is_admin; + } + + /** + * Orders placed by this user. + * + * @return HasMany + */ + public function orders(): HasMany + { + return $this->hasMany(Order::class); + } } diff --git a/app/helpers.php b/app/helpers.php index 7cc025a..f9ae67c 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -8,4 +8,3 @@ if (! function_exists('setting')) { return Setting::get($key, $default); } } - diff --git a/bootstrap/app.php b/bootstrap/app.php index 3c8f740..7f30e4e 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: '*'); + $middleware->alias([ + 'admin' => EnsureUserIsAdmin::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/config/mail.php b/config/mail.php index e32e88d..42b6eaf 100644 --- a/config/mail.php +++ b/config/mail.php @@ -111,8 +111,8 @@ return [ */ 'from' => [ - 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + 'address' => env('MAIL_FROM_ADDRESS', 'info@jouwdriedee.nl'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Jouwdriedee.nl')), ], ]; diff --git a/database/migrations/2026_11_01_124727_add_is_admin_to_users_table.php b/database/migrations/2026_11_01_124727_add_is_admin_to_users_table.php new file mode 100644 index 0000000..519e738 --- /dev/null +++ b/database/migrations/2026_11_01_124727_add_is_admin_to_users_table.php @@ -0,0 +1,28 @@ +boolean('is_admin')->default(false)->after('password'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('is_admin'); + }); + } +}; diff --git a/database/migrations/2026_11_01_124734_add_user_id_to_orders_table.php b/database/migrations/2026_11_01_124734_add_user_id_to_orders_table.php new file mode 100644 index 0000000..3df5ff5 --- /dev/null +++ b/database/migrations/2026_11_01_124734_add_user_id_to_orders_table.php @@ -0,0 +1,28 @@ +foreignId('user_id')->nullable()->after('id')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('orders', function (Blueprint $table) { + $table->dropColumn('user_id'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 5ee890c..f5c7a9a 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -6,12 +6,24 @@ use App\Models\Category; use App\Models\Color; use App\Models\Material; use App\Models\Product; +use App\Models\User; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { public function run(): void { + // 0. Hoofdbeheerder (Admin) met sterk willekeurig wachtwoord + User::updateOrCreate( + ['email' => 'info@jouwdriedee.nl'], + [ + 'name' => 'Jouwdriedee Beheer', + 'password' => Hash::make('Jw3D!x9#QmK7$vR2'), + 'is_admin' => true, + 'email_verified_at' => now(), + ] + ); // 1. Materialen $materials = [ [ diff --git a/resources/views/account/index.blade.php b/resources/views/account/index.blade.php new file mode 100644 index 0000000..4723249 --- /dev/null +++ b/resources/views/account/index.blade.php @@ -0,0 +1,222 @@ +@extends('layouts.app') + +@section('title', 'Mijn Account | Jouwdriedee.nl') + +@section('content') +
+ + +
+
+
+ {{ strtoupper(substr($user->name, 0, 1)) }} +
+
+
+

+ Hallo, {{ $user->name }}! +

+ @if($user->isAdmin()) + + Admin Beheerder + + @endif +
+

+ {{ $user->email }} • Lid sinds {{ $user->created_at->format('d-m-Y') }} +

+
+
+ +
+ @if($user->isAdmin()) + + + Admin Dashboard + + @endif + +
+ @csrf + +
+
+
+ + +
+ + + +
+ + +
+ @if($orders->isEmpty()) +
+
+ 🛍️ +
+

Nog geen bestellingen geplaatst

+

+ Je hebt nog geen 3D prints besteld. Ontdek onze fidget toys, Pokémon figuren en popculture modellen! +

+ +
+ @else +
+ @foreach($orders as $order) +
+ + +
+
+ #{{ $order->order_number }} + • {{ $order->created_at->format('d-m-Y om H:i') }} +
+ +
+ + {{ str_replace('_', ' ', $order->status) }} + + + + {{ $order->formatted_total }} + +
+
+ + +
+ @foreach($order->items as $item) +
+
+
{{ $item->product_name }}
+
+ @if($item->material) Materiaal: {{ strtoupper($item->material) }} • @endif + @if($item->color) Kleur: {{ $item->color }} • @endif + Schaal: {{ $item->scale ?? '100%' }} +
+
+
+ {{ $item->quantity }}x + € {{ number_format($item->total_price, 2, ',', '.') }} +
+
+ @endforeach +
+ + +
+ Bezorgadres: {{ $order->shipping_street }} {{ $order->shipping_number }}, {{ $order->shipping_postal_code }} {{ $order->shipping_city }} + + + PostNL + +
+ +
+ @endforeach +
+ @endif +
+ + +
+ + +
+

+ + Persoonlijke gegevens +

+ +
+ @csrf + @method('PUT') + +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+

+ + Wachtwoord wijzigen +

+ +
+ @csrf + @method('PUT') + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ +
+ +
+@endsection diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..7098725 --- /dev/null +++ b/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.app') + +@section('title', 'Wachtwoord Vergeten | Jouwdriedee.nl') + +@section('content') +
+
+ + +
+
+ +
+

Wachtwoord vergeten

+

Vul je e-mailadres in en ontvang een beveiligde herstellink

+
+ + +
+
+ @csrf + + +
+ +
+ + +
+ @error('email') +

{{ $message }}

+ @enderror +
+ + +
+ +
+
+ + +
+ +
+
+@endsection diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..7f080bb --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,95 @@ +@extends('layouts.app') + +@section('title', 'Inloggen | Jouwdriedee.nl') + +@section('content') +
+
+ + +
+
+ +
+

Welkom terug

+

Log in met jouw e-mailadres en wachtwoord

+
+ + +
+
+ @csrf + + +
+ +
+ + +
+ @error('email') +

{{ $message }}

+ @enderror +
+ + +
+ +
+ + +
+ @error('password') +

{{ $message }}

+ @enderror +
+ + +
+ +
+ + +
+ +
+
+ +
+ Nog geen account? + + Direct registreren + +
+
+ +
+
+@endsection diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php new file mode 100644 index 0000000..9739e9c --- /dev/null +++ b/resources/views/auth/register.blade.php @@ -0,0 +1,118 @@ +@extends('layouts.app') + +@section('title', 'Registreren | Jouwdriedee.nl') + +@section('content') +
+
+ + +
+
+ +
+

Account aanmaken

+

Meld je aan om bestellingen te volgen en sneller te bestellen

+
+ + +
+
+ @csrf + + +
+ +
+ + +
+ @error('name') +

{{ $message }}

+ @enderror +
+ + +
+ +
+ + +
+ @error('email') +

{{ $message }}

+ @enderror +
+ + +
+ +
+ + +
+ @error('password') +

{{ $message }}

+ @enderror +
+ + +
+ +
+ + +
+
+ + +
+ +
+
+ +
+ Heb je al een account? + + Hier inloggen + +
+
+ +
+
+@endsection diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..dbfb252 --- /dev/null +++ b/resources/views/auth/reset-password.blade.php @@ -0,0 +1,83 @@ +@extends('layouts.app') + +@section('title', 'Nieuw Wachtwoord Instellen | Jouwdriedee.nl') + +@section('content') +
+
+ + +
+
+ +
+

Nieuw wachtwoord

+

Kies een nieuw en veilig wachtwoord voor jouw account

+
+ + +
+
+ @csrf + + + +
+ + + @error('email') +

{{ $message }}

+ @enderror +
+ + +
+ + + @error('password') +

{{ $message }}

+ @enderror +
+ + +
+ + +
+ + +
+ +
+
+
+ +
+
+@endsection diff --git a/resources/views/emails/layouts/master.blade.php b/resources/views/emails/layouts/master.blade.php new file mode 100644 index 0000000..4b79e9c --- /dev/null +++ b/resources/views/emails/layouts/master.blade.php @@ -0,0 +1,99 @@ + + + + + + @yield('title', 'Jouwdriedee.nl') + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ 3D + + Jouwdriedee.NL + Nederlandse 3D Printstudio & Webshop +
+
+
+ @yield('content') +
+
+ Jouwdriedee.nl • Hoogwaardige 3D Prints & Collectibles +
+
+ Geprint met precisie op Bambu Lab printers met 100% veilig Bio-PLA+ en PETG filament. +
+ + + + + +
+ • Vragen of speciale wensen? Stuur een e-mail naar info@jouwdriedee.nl • +
+ +
+ © {{ date('Y') }} Jouwdriedee.nl. Alle rechten voorbehouden. +
+
+
+ + diff --git a/resources/views/emails/order-confirmation.blade.php b/resources/views/emails/order-confirmation.blade.php new file mode 100644 index 0000000..c7f5578 --- /dev/null +++ b/resources/views/emails/order-confirmation.blade.php @@ -0,0 +1,104 @@ +@extends('emails.layouts.master') + +@section('title', 'Bestelbevestiging #' . $order->order_number . ' | Jouwdriedee.nl') + +@section('content') +
+ +
+ ✓ Bestelling Ontvangen +
+ +

+ Bedankt voor je bestelling, {{ $order->customer_name }}! +

+ +

+ We hebben je bestelling #{{ $order->order_number }} in goede orde ontvangen. Onze 3D-printers worden voorbereid en jouw items worden met zorg en precisie geprint. +

+ + + + + + + + + + @foreach($order->items as $item) + + + + + + @endforeach + + + + + + + + + + + + + + +
ItemAantalPrijs
+
+ {{ $item->product_name }} +
+
+ @if($item->material) Materiaal: {{ strtoupper($item->material) }} • @endif + @if($item->color) Kleur: {{ $item->color }} • @endif + Schaal: {{ $item->scale ?? '100%' }} +
+
+ {{ $item->quantity }}x + + € {{ number_format($item->total_price, 2, ',', '.') }} +
Subtotaal + € {{ number_format($order->subtotal, 2, ',', '.') }} +
Verzendkosten (PostNL) + {{ $order->shipping_cost > 0 ? '€ ' . number_format($order->shipping_cost, 2, ',', '.') : 'Gratis' }} +
Totaal incl. btw + {{ $order->formatted_total }} +
+ + + + + + +
+
+ 📦 Afleveradres: +
+
+ {{ $order->customer_name }}
+ {{ $order->shipping_street }} {{ $order->shipping_number }}
+ {{ $order->shipping_postal_code }} {{ $order->shipping_city }}
+ {{ $order->shipping_country ?? 'Nederland' }} +
+
+ + +
+ Wat gebeurt er nu?
+ 1. We printen jouw model op hoge resolutie.
+ 2. We controleren de scharnieren en details met de hand.
+ 3. Je ontvangt een verzendbevestiging zodra het pakketje is overgedragen aan PostNL (binnen 2-3 werkdagen). +
+ + @auth +
+ + Volg Bestelling in Mijn Account → + +
+ @endauth + +
+@endsection diff --git a/resources/views/emails/password-reset.blade.php b/resources/views/emails/password-reset.blade.php new file mode 100644 index 0000000..9e61ddf --- /dev/null +++ b/resources/views/emails/password-reset.blade.php @@ -0,0 +1,37 @@ +@extends('emails.layouts.master') + +@section('title', 'Wachtwoord Opnieuw Instellen | Jouwdriedee.nl') + +@section('content') +
+ +
+ 🔒 Beveiliging +
+ +

+ Wachtwoord opnieuw instellen +

+ +

+ Hallo {{ $user->name }},
+ We hebben een verzoek ontvangen om het wachtwoord van jouw account bij Jouwdriedee.nl te resetten. Klik op onderstaande knop om een nieuw wachtwoord te kiezen: +

+ +
+ + Wachtwoord Opnieuw Instellen → + +
+ +
+ Let op: Deze link is om veiligheidsredenen slechts 60 minuten geldig. Heb je zelf geen wachtwoordreset aangevraagd? Dan kun je deze e-mail veilig negeren. Jouw huidige wachtwoord blijft ongewijzigd. +
+ +

+ Werkt de knop niet? Kopieer en plak de volgende URL direct in je browser:
+ {{ $resetUrl }} +

+ +
+@endsection diff --git a/resources/views/emails/welcome.blade.php b/resources/views/emails/welcome.blade.php new file mode 100644 index 0000000..3149b01 --- /dev/null +++ b/resources/views/emails/welcome.blade.php @@ -0,0 +1,51 @@ +@extends('emails.layouts.master') + +@section('title', 'Welkom bij Jouwdriedee.nl!') + +@section('content') +
+ +
+ 🎉 Account Aangemaakt +
+ +

+ Welkom bij Jouwdriedee, {{ $user->name }}! +

+ +

+ Leuk dat je een account hebt aangemaakt bij onze 3D printstudio. Vanaf nu kun je eenvoudig jouw bestellingen volgen, favoriete modellen bewaren en nóg sneller afrekenen. +

+ + + + + + +
+
+ Wat kun je verwachten van onze 3D prints? +
+
+ • Haarscherpe Bambu Lab kwaliteit: Laagdiktes vanaf 0.12 mm voor soepele scharnieren.
+ • 16+ Kleurvariaties: Van zijdeglans Silk tot levendige fluor en pastel tinten.
+ • Duurzaam PLA+ filament: 100% biologisch afbreekbaar en reukloos. +
+
+ + +
+ + Bekijk de Collectie → + + + Mijn Account + +
+ +

+ Heb je een vraag over een print of wil je iets op maat laten maken? Neem gerust contact op met ons via info@jouwdriedee.nl. +

+ +
+@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index bfba5d9..ed04cd0 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -102,7 +102,7 @@ $cartItems = is_array(session('cart')) ? session('cart') : []; $cartCount = array_sum(array_column($cartItems, 'quantity')); @endphp - + @if($cartCount > 0) @@ -112,6 +112,43 @@ @endif + + @guest + + + + + @else +
+ + +
+ + + Mijn Account + + @if(auth()->user()->isAdmin()) + + + Admin Beheer + + @endif +
+
+ @csrf + +
+
+
+ @endguest + + + + @endguest + diff --git a/routes/web.php b/routes/web.php index 41bb181..2c9f36f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,10 +1,14 @@ name('checkout.i Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store'); Route::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success'); -// Admin & CMS Beheer -Route::prefix('admin')->name('admin.')->group(function () { +// Authenticatie & Klantenportaal +Route::middleware('guest')->group(function () { + Route::get('/inloggen', [LoginController::class, 'showLoginForm'])->name('login'); + Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit'); + Route::redirect('/login', '/inloggen'); + + Route::get('/registreren', [RegisterController::class, 'showRegistrationForm'])->name('register'); + Route::post('/registreren', [RegisterController::class, 'register'])->name('register.submit'); + Route::redirect('/register', '/registreren'); + + Route::get('/wachtwoord-vergeten', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request'); + Route::post('/wachtwoord-vergeten', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email'); + Route::get('/wachtwoord-resetten/{token}', [ForgotPasswordController::class, 'showResetForm'])->name('password.reset'); + Route::post('/wachtwoord-resetten', [ForgotPasswordController::class, 'reset'])->name('password.update'); +}); + +Route::middleware('auth')->group(function () { + Route::post('/uitloggen', [LoginController::class, 'logout'])->name('logout'); + Route::get('/uitloggen', [LoginController::class, 'logout']); // Fallback voor GET uitloggen + + // Klantendashboard / Mijn Account + Route::get('/mijn-account', [AccountController::class, 'index'])->name('account.index'); + Route::put('/mijn-account/profiel', [AccountController::class, 'updateProfile'])->name('account.profile'); + Route::put('/mijn-account/wachtwoord', [AccountController::class, 'updatePassword'])->name('account.password'); +}); + +// Admin & CMS Beheer (Beveiligd: alleen ingelogde admins) +Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(function () { Route::redirect('/', '/admin/content'); // Layout, Stijl & Teksten CMS diff --git a/tests/Feature/AdminContentTest.php b/tests/Feature/AdminContentTest.php index 258856e..f8baee8 100644 --- a/tests/Feature/AdminContentTest.php +++ b/tests/Feature/AdminContentTest.php @@ -6,6 +6,7 @@ use App\Models\Category; use App\Models\Order; use App\Models\Product; use App\Models\Setting; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -13,10 +14,46 @@ class AdminContentTest extends TestCase { use RefreshDatabase; - public function test_admin_content_editor_loads_successfully(): void + protected User $admin; + + protected function setUp(): void + { + parent::setUp(); + + $this->admin = User::create([ + 'name' => 'Admin Jouwdriedee', + 'email' => 'info@jouwdriedee.nl', + 'password' => bcrypt('Jw3D!x9#QmK7$vR2'), + 'is_admin' => true, + ]); + } + + public function test_guest_is_redirected_to_login_when_accessing_admin(): void { $response = $this->get(route('admin.content.index')); + $response->assertRedirect(route('login')); + } + + public function test_regular_customer_cannot_access_admin(): void + { + $customer = User::create([ + 'name' => 'Klant Piet', + 'email' => 'piet@example.com', + 'password' => bcrypt('secret1234'), + 'is_admin' => false, + ]); + + $response = $this->actingAs($customer)->get(route('admin.content.index')); + + $response->assertRedirect(route('home')); + $response->assertSessionHas('error'); + } + + public function test_admin_content_editor_loads_successfully(): void + { + $response = $this->actingAs($this->admin)->get(route('admin.content.index')); + $response->assertStatus(200); $response->assertSee('Homepage Hero'); $response->assertSee('Shop Content & Stijl CMS', false); @@ -35,7 +72,7 @@ class AdminContentTest extends TestCase 'announcement_text' => 'Tijdelijke actie: gratis verzending!', ]; - $response = $this->post(route('admin.content.update'), $payload); + $response = $this->actingAs($this->admin)->post(route('admin.content.update'), $payload); $response->assertRedirect(route('admin.content.index')); $response->assertSessionHas('success'); @@ -60,7 +97,7 @@ class AdminContentTest extends TestCase Setting::set('hero_title', 'Tijdelijke Titel'); $this->assertSame('Tijdelijke Titel', Setting::get('hero_title')); - $response = $this->post(route('admin.content.reset')); + $response = $this->actingAs($this->admin)->post(route('admin.content.reset')); $response->assertRedirect(route('admin.content.index')); $this->assertNull(Setting::get('hero_title')); @@ -84,17 +121,17 @@ class AdminContentTest extends TestCase ]); // Products index - $indexResponse = $this->get(route('admin.products.index')); + $indexResponse = $this->actingAs($this->admin)->get(route('admin.products.index')); $indexResponse->assertStatus(200); $indexResponse->assertSee('Test Fidget Draak'); // Edit page - $editResponse = $this->get(route('admin.products.edit', $product)); + $editResponse = $this->actingAs($this->admin)->get(route('admin.products.edit', $product)); $editResponse->assertStatus(200); $editResponse->assertSee('Test Fidget Draak'); // Update product - $updateResponse = $this->put(route('admin.products.update', $product), [ + $updateResponse = $this->actingAs($this->admin)->put(route('admin.products.update', $product), [ 'name' => 'Aangepaste Fidget Draak', 'category_id' => $category->id, 'base_price' => 18.00, @@ -128,12 +165,12 @@ class AdminContentTest extends TestCase 'status' => 'pending', ]); - $response = $this->get(route('admin.orders.index')); + $response = $this->actingAs($this->admin)->get(route('admin.orders.index')); $response->assertStatus(200); $response->assertSee('ORD-TEST-999'); $response->assertSee('Jan Jansen'); - $showResponse = $this->get(route('admin.orders.show', $order)); + $showResponse = $this->actingAs($this->admin)->get(route('admin.orders.show', $order)); $showResponse->assertStatus(200); $showResponse->assertSee('ORD-TEST-999'); } diff --git a/tests/Feature/AuthAndEmailTest.php b/tests/Feature/AuthAndEmailTest.php new file mode 100644 index 0000000..efc317d --- /dev/null +++ b/tests/Feature/AuthAndEmailTest.php @@ -0,0 +1,218 @@ +post(route('register.submit'), [ + 'name' => 'Sophie de Vries', + 'email' => 'sophie@example.com', + 'password' => 'VeiligWachtwoord123!', + 'password_confirmation' => 'VeiligWachtwoord123!', + ]); + + $response->assertRedirect(route('account.index')); + $this->assertAuthenticated(); + + $this->assertDatabaseHas('users', [ + 'email' => 'sophie@example.com', + 'name' => 'Sophie de Vries', + 'is_admin' => false, + ]); + + Mail::assertSent(WelcomeMail::class, function ($mail) { + return $mail->hasTo('sophie@example.com') + && str_contains($mail->envelope()->subject, 'Welkom bij Jouwdriedee.nl'); + }); + } + + public function test_customer_can_login_with_valid_credentials(): void + { + $user = User::create([ + 'name' => 'Lars Bakker', + 'email' => 'lars@example.com', + 'password' => Hash::make('MijnGeheimeWachtwoord'), + 'is_admin' => false, + ]); + + $response = $this->post(route('login.submit'), [ + 'email' => 'lars@example.com', + 'password' => 'MijnGeheimeWachtwoord', + ]); + + $response->assertRedirect(route('account.index')); + $this->assertAuthenticatedAs($user); + } + + public function test_admin_login_redirects_to_admin_content_dashboard(): void + { + $admin = User::create([ + 'name' => 'Jouwdriedee Beheer', + 'email' => 'info@jouwdriedee.nl', + 'password' => Hash::make('Jw3D!x9#QmK7$vR2'), + 'is_admin' => true, + ]); + + $response = $this->post(route('login.submit'), [ + 'email' => 'info@jouwdriedee.nl', + 'password' => 'Jw3D!x9#QmK7$vR2', + ]); + + $response->assertRedirect(route('admin.content.index')); + $this->assertAuthenticatedAs($admin); + } + + public function test_login_fails_with_invalid_credentials(): void + { + User::create([ + 'name' => 'Lars Bakker', + 'email' => 'lars@example.com', + 'password' => Hash::make('MijnGeheimeWachtwoord'), + ]); + + $response = $this->post(route('login.submit'), [ + 'email' => 'lars@example.com', + 'password' => 'FoutWachtwoord', + ]); + + $response->assertSessionHasErrors('email'); + $this->assertGuest(); + } + + public function test_customer_can_request_password_reset_and_receives_email(): void + { + Mail::fake(); + + $user = User::create([ + 'name' => 'Emma Visser', + 'email' => 'emma@example.com', + 'password' => Hash::make('Wachtwoord123'), + ]); + + $response = $this->post(route('password.email'), [ + 'email' => 'emma@example.com', + ]); + + $response->assertSessionHas('success'); + + Mail::assertSent(PasswordResetMail::class, function ($mail) { + return $mail->hasTo('emma@example.com') + && str_contains($mail->envelope()->subject, 'Wachtwoord opnieuw instellen'); + }); + + $this->assertDatabaseHas('password_reset_tokens', [ + 'email' => 'emma@example.com', + ]); + } + + public function test_customer_can_reset_password_with_valid_token(): void + { + $user = User::create([ + 'name' => 'Emma Visser', + 'email' => 'emma@example.com', + 'password' => Hash::make('OudWachtwoord123'), + ]); + + $rawToken = 'test-secret-token-1234567890'; + DB::table('password_reset_tokens')->insert([ + 'email' => 'emma@example.com', + 'token' => Hash::make($rawToken), + 'created_at' => now(), + ]); + + $response = $this->post(route('password.update'), [ + 'token' => $rawToken, + 'email' => 'emma@example.com', + 'password' => 'GloednieuwWachtwoord123!', + 'password_confirmation' => 'GloednieuwWachtwoord123!', + ]); + + $response->assertRedirect(route('account.index')); + $this->assertAuthenticatedAs($user); + + $user->refresh(); + $this->assertTrue(Hash::check('GloednieuwWachtwoord123!', $user->password)); + + $this->assertDatabaseMissing('password_reset_tokens', [ + 'email' => 'emma@example.com', + ]); + } + + public function test_checkout_sends_order_confirmation_email(): void + { + Mail::fake(); + + Material::updateOrCreate(['code' => 'pla'], [ + 'name' => 'Bio-PLA+', + 'code' => 'pla', + 'price_multiplier' => 1.0, + 'is_active' => true, + ]); + + $category = Category::create([ + 'name' => 'Fidgets', + 'slug' => 'fidgets', + ]); + + $product = Product::create([ + 'name' => 'Kristaldraak Mini', + 'slug' => 'kristaldraak-mini', + 'category_id' => $category->id, + 'base_price' => 15.00, + 'description' => 'Test draakje', + 'is_active' => true, + ]); + + // Put item in cart + session()->put('cart', [ + 'kristaldraak-mini-pla-rood' => [ + 'id' => $product->id, + 'name' => 'Kristaldraak Mini', + 'slug' => 'kristaldraak-mini', + 'price' => 15.00, + 'quantity' => 2, + 'material' => 'pla', + 'color' => 'Vurig Rood', + 'scale' => '100%', + 'infill' => '20%', + ], + ]); + + $response = $this->post(route('checkout.store'), [ + 'customer_name' => 'Klant Tester', + 'customer_email' => 'klant@test.nl', + 'customer_phone' => '0612345678', + 'shipping_street' => 'Hoofdstraat', + 'shipping_number' => '42', + 'shipping_postal_code' => '1000AA', + 'shipping_city' => 'Utrecht', + 'payment_method' => 'ideal', + ]); + + $response->assertRedirect(); + + Mail::assertSent(OrderConfirmationMail::class, function ($mail) { + return $mail->hasTo('klant@test.nl') + && str_contains($mail->envelope()->subject, 'Bestelbevestiging'); + }); + } +} diff --git a/tests/Feature/MakerWorldImporterTest.php b/tests/Feature/MakerWorldImporterTest.php index 48c5352..aabcb3e 100644 --- a/tests/Feature/MakerWorldImporterTest.php +++ b/tests/Feature/MakerWorldImporterTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature; use App\Models\Category; use App\Models\Product; use App\Models\Setting; +use App\Models\User; use App\Services\MakerWorldService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; @@ -14,6 +15,20 @@ class MakerWorldImporterTest extends TestCase { use RefreshDatabase; + protected function setUp(): void + { + parent::setUp(); + + $admin = User::create([ + 'name' => 'Admin Jouwdriedee', + 'email' => 'info@jouwdriedee.nl', + 'password' => bcrypt('Jw3D!x9#QmK7$vR2'), + 'is_admin' => true, + ]); + + $this->actingAs($admin); + } + protected function fakeMakerWorldApi(): void { Http::fake([