diff --git a/Dilomarkt/Composer-Setup.exe b/Dilomarkt/Composer-Setup.exe new file mode 100644 index 0000000..76df8e2 Binary files /dev/null and b/Dilomarkt/Composer-Setup.exe differ diff --git a/Dilomarkt/dilomarkt-api/README.md b/Dilomarkt/dilomarkt-api/README.md index 0165a77..4fc0ffb 100644 --- a/Dilomarkt/dilomarkt-api/README.md +++ b/Dilomarkt/dilomarkt-api/README.md @@ -57,3 +57,33 @@ ## Security Vulnerabilities ## License The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). + +## SMTP Setup for Real Verification Emails + +To deliver verification and password-reset emails to real inboxes (Gmail, Outlook, Yahoo, etc.), configure a real SMTP provider in `.env`. + +Required `.env` values: + +- `MAIL_MAILER=smtp` +- `MAIL_HOST=` +- `MAIL_PORT=587` +- `MAIL_USERNAME=` +- `MAIL_PASSWORD=` +- `MAIL_ENCRYPTION=tls` +- `MAIL_FROM_ADDRESS=` +- `MAIL_FROM_NAME="Dilomarkt"` +- `QUEUE_CONNECTION=sync` + +After changing `.env`, run: + +```bash +php artisan config:clear +``` + +Then test by registering with a real address and requesting password reset. + +Important: + +- SMTP credentials must belong to a provider that allows outbound mail to external domains. +- Use a verified sender address from that provider. +- Delivery can still be filtered by recipient spam rules; verify SPF/DKIM for best inbox placement. diff --git a/Dilomarkt/dilomarkt-api/app/Http/Controllers/AuthController.php b/Dilomarkt/dilomarkt-api/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..62443a3 --- /dev/null +++ b/Dilomarkt/dilomarkt-api/app/Http/Controllers/AuthController.php @@ -0,0 +1,178 @@ +validate([ + 'first_name' => ['required', 'string', 'max:255'], + 'last_name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'unique:users,email'], + 'password' => ['required', 'confirmed', 'min:8'], + 'role' => ['required', 'in:buyer,seller'], + ]); + + $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); + + try { + $user = User::create([ + 'first_name' => $request->first_name, + 'last_name' => $request->last_name, + 'name' => trim($request->first_name . ' ' . $request->last_name), + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'role' => $request->role, + 'verification_code' => $code, + 'email_verified_at' => null, + ]); + + $this->sendVerificationEmail($user, $code); + } catch (Throwable $e) { + if (isset($user)) { + $user->delete(); + } + + return response()->json([ + 'status' => 'email_failed', + 'message' => 'Verifizierungsmail konnte nicht gesendet werden. Bitte später erneut versuchen.', + ], 503); + } + + return response()->json([ + 'status' => 'verification_sent', + 'message' => 'Bitte verifiziere deinen Account mit dem Code aus der E-Mail.', + 'email' => $user->email, + ]); + } + + public function verify(Request $request) + { + $request->validate([ + 'email' => ['required', 'email'], + 'code' => ['required', 'string', 'size:6'], + ]); + + $user = User::where('email', $request->email)->first(); + + if (! $user) { + return response()->json(['message' => 'Benutzer nicht gefunden.'], 422); + } + + if ($user->verification_code !== $request->code) { + return response()->json(['message' => 'Der Verifizierungscode ist ungültig.'], 422); + } + + $user->forceFill([ + 'email_verified_at' => now(), + 'verification_code' => null, + ])->save(); + + return response()->json([ + 'status' => 'verified', + 'message' => 'Dein Konto wurde verifiziert.', + ]); + } + + public function login(Request $request) + { + $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required'], + ]); + + $user = User::where('email', $request->email)->first(); + + if (! $user || ! Hash::check($request->password, $user->password)) { + return response()->json(['message' => 'Ungültige E-Mail oder falsches Passwort.'], 401); + } + + if (! $user->hasVerifiedEmail()) { + return response()->json([ + 'status' => 'not_verified', + 'message' => 'Bitte verifiziere zuerst deinen Account.', + ], 403); + } + + $token = Str::random(40); + $user->forceFill(['api_token' => $token])->save(); + + return response()->json([ + 'status' => 'logged_in', + 'token' => $token, + 'user' => [ + 'id' => $user->id, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'email' => $user->email, + 'role' => $user->role, + ], + ]); + } + + public function forgotPassword(Request $request) + { + $request->validate(['email' => ['required', 'email']]); + + $user = User::where('email', $request->email)->first(); + + if ($user) { + $token = Str::random(60); + try { + $user->forceFill(['password_reset_token' => $token])->save(); + $this->sendPasswordResetEmail($user, $token); + } catch (Throwable $e) { + return response()->json([ + 'status' => 'email_failed', + 'message' => 'Reset-E-Mail konnte nicht gesendet werden. Bitte später erneut versuchen.', + ], 503); + } + } + + return response()->json([ + 'status' => 'reset_sent', + 'message' => 'Falls die E-Mail existiert, wurde ein Link gesendet.', + ]); + } + + public function resetPassword(Request $request) + { + $request->validate([ + 'token' => ['required', 'string'], + 'password' => ['required', 'confirmed', 'min:8'], + ]); + + $user = User::where('password_reset_token', $request->token)->first(); + + if (! $user) { + return response()->json(['message' => 'Ungültiger oder abgelaufener Link.'], 422); + } + + $user->forceFill([ + 'password' => Hash::make($request->password), + 'password_reset_token' => null, + ])->save(); + + return response()->json([ + 'status' => 'password_reset', + 'message' => 'Dein Passwort wurde erfolgreich geändert.', + ]); + } + + private function sendVerificationEmail(User $user, string $code): void + { + $user->notify(new \App\Notifications\VerifyEmailNotification($code)); + } + + private function sendPasswordResetEmail(User $user, string $token): void + { + $user->notify(new \App\Notifications\ResetPasswordNotification($token)); + } +} diff --git a/Dilomarkt/dilomarkt-api/app/Http/Controllers/SellerController.php b/Dilomarkt/dilomarkt-api/app/Http/Controllers/SellerController.php new file mode 100644 index 0000000..d66b644 --- /dev/null +++ b/Dilomarkt/dilomarkt-api/app/Http/Controllers/SellerController.php @@ -0,0 +1,222 @@ +bearerToken(); + if (! $token) return null; + return User::where('api_token', $token)->where('role', 'seller')->first(); + } + + /** Map common German zip codes to coordinates. */ + private function resolvePlz(string $plz): array + { + $coords = [ + '42103' => [51.2562, 7.1508], '42107' => [51.2637, 7.1362], + '42119' => [51.2700, 7.1800], '42277' => [51.2800, 7.2100], + '42651' => [51.1800, 7.0800], '42853' => [51.1800, 7.1900], + '40210' => [51.2217, 6.7762], '44135' => [51.5139, 7.4653], + '45127' => [51.4556, 7.0116], '47051' => [51.4344, 6.7623], + '50667' => [50.9333, 6.9500], + ]; + return $coords[$plz] ?? [51.2562, 7.1508]; + } + + /** GET /api/seller/shop — returns shop info + products for the authenticated seller. */ + public function getShop(Request $request) + { + $user = $this->auth($request); + if (! $user) return response()->json(['message' => 'Nicht autorisiert.'], 401); + + $shop = DB::table('providers')->where('user_id', $user->id)->first(); + if (! $shop) { + return response()->json(['shop' => null, 'products' => []]); + } + + $products = DB::table('products')->where('provider_id', $shop->id)->orderBy('id')->get(); + return response()->json(['shop' => $shop, 'products' => $products]); + } + + /** POST /api/seller/shop — create or update the seller's shop profile. */ + public function upsertShop(Request $request) + { + $user = $this->auth($request); + if (! $user) return response()->json(['message' => 'Nicht autorisiert.'], 401); + + $request->validate([ + 'name' => ['required', 'string', 'max:100'], + 'type' => ['required', 'in:Fachhandel,Baumarkt'], + 'address' => ['required', 'string', 'max:200'], + 'city' => ['required', 'string', 'max:100'], + 'zip' => ['required', 'string', 'max:10'], + ]); + + [$lat, $lng] = $this->resolvePlz($request->zip); + $initials = strtoupper(mb_substr($request->name, 0, 2)); + $existing = DB::table('providers')->where('user_id', $user->id)->first(); + + if ($existing) { + DB::table('providers')->where('id', $existing->id)->update([ + 'name' => $request->name, + 'type' => $request->type, + 'address' => $request->address, + 'city' => $request->city, + 'zip' => $request->zip, + 'lat' => $lat, + 'lng' => $lng, + 'initials' => $initials, + 'updated_at' => now(), + ]); + $shop = DB::table('providers')->find($existing->id); + } else { + $id = DB::table('providers')->insertGetId([ + 'user_id' => $user->id, + 'name' => $request->name, + 'type' => $request->type, + 'address' => $request->address, + 'city' => $request->city, + 'zip' => $request->zip, + 'lat' => $lat, + 'lng' => $lng, + 'initials' => $initials, + 'since' => now()->year, + 'verified' => false, + 'created_at' => now(), + 'updated_at' => now(), + ]); + $shop = DB::table('providers')->find($id); + } + + return response()->json(['message' => 'Shop gespeichert.', 'shop' => $shop]); + } + + /** POST /api/seller/products — add a new product to the seller's shop. */ + public function addProduct(Request $request) + { + $user = $this->auth($request); + if (! $user) return response()->json(['message' => 'Nicht autorisiert.'], 401); + + $shop = DB::table('providers')->where('user_id', $user->id)->first(); + if (! $shop) return response()->json(['message' => 'Kein Shop eingerichtet.'], 422); + + $request->validate([ + 'title' => ['required', 'string', 'max:200'], + 'price' => ['required', 'numeric', 'min:0'], + 'category' => ['required', 'string'], + 'stock' => ['required', 'integer', 'min:0'], + 'description' => ['required', 'string'], + 'icon' => ['required', 'string', 'max:8'], + ]); + + $id = DB::table('products')->insertGetId([ + 'provider_id' => $shop->id, + 'title' => $request->title, + 'price' => $request->price, + 'category' => $request->category, + 'stock' => $request->stock, + 'description' => $request->description, + 'icon' => $request->icon, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return response()->json(['message' => 'Produkt hinzugefügt.', 'product' => DB::table('products')->find($id)]); + } + + /** PUT /api/seller/products/{id} — update one of the seller's products. */ + public function updateProduct(Request $request, int $id) + { + $user = $this->auth($request); + if (! $user) return response()->json(['message' => 'Nicht autorisiert.'], 401); + + $shop = DB::table('providers')->where('user_id', $user->id)->first(); + if (! $shop) return response()->json(['message' => 'Kein Shop eingerichtet.'], 422); + + $product = DB::table('products')->where('id', $id)->where('provider_id', $shop->id)->first(); + if (! $product) return response()->json(['message' => 'Produkt nicht gefunden.'], 404); + + $request->validate([ + 'title' => ['required', 'string', 'max:200'], + 'price' => ['required', 'numeric', 'min:0'], + 'category' => ['required', 'string'], + 'stock' => ['required', 'integer', 'min:0'], + 'description' => ['required', 'string'], + 'icon' => ['required', 'string', 'max:8'], + ]); + + DB::table('products')->where('id', $id)->update([ + 'title' => $request->title, + 'price' => $request->price, + 'category' => $request->category, + 'stock' => $request->stock, + 'description' => $request->description, + 'icon' => $request->icon, + 'updated_at' => now(), + ]); + + return response()->json(['message' => 'Produkt aktualisiert.', 'product' => DB::table('products')->find($id)]); + } + + /** DELETE /api/seller/products/{id} — remove one of the seller's products. */ + public function deleteProduct(Request $request, int $id) + { + $user = $this->auth($request); + if (! $user) return response()->json(['message' => 'Nicht autorisiert.'], 401); + + $shop = DB::table('providers')->where('user_id', $user->id)->first(); + if (! $shop) return response()->json(['message' => 'Kein Shop eingerichtet.'], 422); + + $deleted = DB::table('products')->where('id', $id)->where('provider_id', $shop->id)->delete(); + if (! $deleted) return response()->json(['message' => 'Produkt nicht gefunden.'], 404); + + return response()->json(['message' => 'Produkt gelöscht.']); + } + + /** GET /api/seller/messages — returns all buyer conversations for this seller's shop. */ + public function getMessages(Request $request) + { + $user = $this->auth($request); + if (! $user) return response()->json(['message' => 'Nicht autorisiert.'], 401); + + $shop = DB::table('providers')->where('user_id', $user->id)->first(); + if (! $shop) return response()->json(['conversations' => []]); + + // All messages for this shop ordered newest first + $rows = DB::table('messages') + ->join('products', 'messages.product_id', '=', 'products.id') + ->leftJoin('users', 'messages.buyer_id', '=', 'users.id') + ->where('messages.provider_id', $shop->id) + ->select( + 'messages.product_id', + 'messages.buyer_id', + 'messages.body as last_message', + 'messages.sender as last_sender', + 'messages.created_at as last_at', + 'products.title as product_title', + DB::raw("COALESCE(users.first_name || ' ' || users.last_name, 'Unbekannt') as buyer_name") + ) + ->orderBy('messages.created_at', 'desc') + ->get(); + + // Keep only the most recent message per (product_id, buyer_id) pair + $seen = []; + $conversations = []; + foreach ($rows as $row) { + $key = $row->product_id . '_' . $row->buyer_id; + if (! isset($seen[$key])) { + $seen[$key] = true; + $conversations[] = $row; + } + } + + return response()->json(['conversations' => $conversations]); + } +} diff --git a/Dilomarkt/dilomarkt-api/app/Models/User.php b/Dilomarkt/dilomarkt-api/app/Models/User.php index 68f3a66..6d1410a 100644 --- a/Dilomarkt/dilomarkt-api/app/Models/User.php +++ b/Dilomarkt/dilomarkt-api/app/Models/User.php @@ -19,9 +19,15 @@ class User extends Authenticatable * @var list */ protected $fillable = [ + 'first_name', + 'last_name', 'name', 'email', 'password', + 'role', + 'verification_code', + 'password_reset_token', + 'api_token', ]; /** @@ -46,4 +52,9 @@ protected function casts(): array 'password' => 'hashed', ]; } + + public function hasVerifiedEmail(): bool + { + return ! is_null($this->email_verified_at); + } } diff --git a/Dilomarkt/dilomarkt-api/app/Notifications/ResetPasswordNotification.php b/Dilomarkt/dilomarkt-api/app/Notifications/ResetPasswordNotification.php new file mode 100644 index 0000000..f2486dc --- /dev/null +++ b/Dilomarkt/dilomarkt-api/app/Notifications/ResetPasswordNotification.php @@ -0,0 +1,30 @@ +subject('Passwort zurücksetzen bei Dilomarkt') + ->greeting('Hallo!') + ->line('Klicke auf den Link unten, um dein Passwort zurückzusetzen.') + ->action('Passwort zurücksetzen', $frontendUrl . '/reset-password/' . $this->token) + ->salutation('Mit freundlichen Grüßen, das Dilomarkt Team'); + } +} diff --git a/Dilomarkt/dilomarkt-api/app/Notifications/VerifyEmailNotification.php b/Dilomarkt/dilomarkt-api/app/Notifications/VerifyEmailNotification.php new file mode 100644 index 0000000..77f5122 --- /dev/null +++ b/Dilomarkt/dilomarkt-api/app/Notifications/VerifyEmailNotification.php @@ -0,0 +1,29 @@ +subject('Dein Verifizierungscode für Dilomarkt') + ->greeting('Hallo!') + ->line('Vielen Dank für deine Registrierung bei Dilomarkt.') + ->line('Dein Verifizierungscode lautet: ' . $this->code) + ->line('Bitte gib diesen Code in der App ein, um dein Konto zu aktivieren.') + ->salutation('Mit freundlichen Grüßen, das Dilomarkt Team'); + } +} diff --git a/Dilomarkt/dilomarkt-api/config/cors.php b/Dilomarkt/dilomarkt-api/config/cors.php index 0c1cc3b..ad08a6d 100644 --- a/Dilomarkt/dilomarkt-api/config/cors.php +++ b/Dilomarkt/dilomarkt-api/config/cors.php @@ -2,7 +2,7 @@ return [ 'paths' => ['api/*'], 'allowed_methods' => ['*'], - 'allowed_origins' => ['http://localhost:5173'], + 'allowed_origins' => ['http://localhost:5173', 'http://localhost:5174'], 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], 'exposed_headers' => [], diff --git a/Dilomarkt/dilomarkt-api/database/migrations/0001_01_01_000000_create_users_table.php b/Dilomarkt/dilomarkt-api/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..9e2156d 100644 --- a/Dilomarkt/dilomarkt-api/database/migrations/0001_01_01_000000_create_users_table.php +++ b/Dilomarkt/dilomarkt-api/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,10 +13,15 @@ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); + $table->string('first_name'); + $table->string('last_name'); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->string('role')->default('buyer'); + $table->string('verification_code')->nullable(); + $table->string('password_reset_token')->nullable(); $table->rememberToken(); $table->timestamps(); }); diff --git a/Dilomarkt/dilomarkt-api/database/migrations/2026_06_30_000001_add_api_token_to_users_table.php b/Dilomarkt/dilomarkt-api/database/migrations/2026_06_30_000001_add_api_token_to_users_table.php new file mode 100644 index 0000000..3216539 --- /dev/null +++ b/Dilomarkt/dilomarkt-api/database/migrations/2026_06_30_000001_add_api_token_to_users_table.php @@ -0,0 +1,18 @@ +string('api_token', 40)->nullable()->unique()->after('password_reset_token'); + }); + } + + public function down(): void { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('api_token'); + }); + } +}; diff --git a/Dilomarkt/dilomarkt-api/database/migrations/2026_06_30_000002_add_user_id_to_providers_table.php b/Dilomarkt/dilomarkt-api/database/migrations/2026_06_30_000002_add_user_id_to_providers_table.php new file mode 100644 index 0000000..067c5c1 --- /dev/null +++ b/Dilomarkt/dilomarkt-api/database/migrations/2026_06_30_000002_add_user_id_to_providers_table.php @@ -0,0 +1,19 @@ +unsignedBigInteger('user_id')->nullable()->after('id'); + }); + } + + public function down(): void { + Schema::table('providers', function (Blueprint $table) { + $table->dropColumn('user_id'); + }); + } +}; diff --git a/Dilomarkt/dilomarkt-api/routes/api.php b/Dilomarkt/dilomarkt-api/routes/api.php index e0d0dd6..e4acde6 100644 --- a/Dilomarkt/dilomarkt-api/routes/api.php +++ b/Dilomarkt/dilomarkt-api/routes/api.php @@ -1,8 +1,16 @@ make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use App\Models\User; +use App\Notifications\VerifyEmailNotification; + +// Make a temporary fake user (not saved) +$user = new User(); +$user->email = 'ebosak322@gmail.com'; +$user->name = 'Test User'; + +try { + $user->notify(new VerifyEmailNotification('123456')); + echo "SUCCESS: Notification sent via notify()\n"; +} catch (\Exception $e) { + echo "FAILED via notify(): " . get_class($e) . "\n"; + echo $e->getMessage() . "\n"; +} diff --git a/Dilomarkt/dilomarkt-api/tests/Feature/AuthTest.php b/Dilomarkt/dilomarkt-api/tests/Feature/AuthTest.php new file mode 100644 index 0000000..81304e0 --- /dev/null +++ b/Dilomarkt/dilomarkt-api/tests/Feature/AuthTest.php @@ -0,0 +1,39 @@ +postJson('/api/auth/register', [ + 'first_name' => 'Ada', + 'last_name' => 'Lovelace', + 'email' => 'ada@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'role' => 'buyer', + ]); + + $response->assertOk(); + $response->assertJsonPath('status', 'verification_sent'); + + $user = User::where('email', 'ada@example.com')->first(); + $this->assertNotNull($user); + $this->assertNotNull($user->verification_code); + + $verifyResponse = $this->postJson('/api/auth/verify', [ + 'email' => 'ada@example.com', + 'code' => $user->verification_code, + ]); + + $verifyResponse->assertOk(); + $this->assertNotNull($user->fresh()->email_verified_at); + } +} diff --git a/Dilomarkt/package-lock.json b/Dilomarkt/package-lock.json index 91e9229..fff5141 100644 --- a/Dilomarkt/package-lock.json +++ b/Dilomarkt/package-lock.json @@ -57,6 +57,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -481,33 +482,10 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -689,9 +667,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -709,9 +684,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -729,9 +701,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -749,9 +718,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -769,9 +735,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -789,9 +752,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -837,6 +797,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", @@ -908,6 +902,7 @@ "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1330,6 +1325,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1875,9 +1871,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1899,9 +1892,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1923,9 +1913,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1947,9 +1934,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2513,6 +2497,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2601,6 +2586,7 @@ "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -2789,6 +2775,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/compiler-sfc": "3.5.35", diff --git a/Dilomarkt/src/App.vue b/Dilomarkt/src/App.vue index d6a4ced..c337d35 100644 --- a/Dilomarkt/src/App.vue +++ b/Dilomarkt/src/App.vue @@ -1,7 +1,34 @@ \ No newline at end of file diff --git a/Dilomarkt/src/api.js b/Dilomarkt/src/api.js index 0ca28cf..064ad69 100644 --- a/Dilomarkt/src/api.js +++ b/Dilomarkt/src/api.js @@ -1,5 +1,32 @@ const BASE = 'http://localhost:8000/api' +// ── Seller API (requires Bearer token) ─────────────────────────────────────── +function sellerHeaders() { + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('dilomarkt_token') || ''}`, + } +} + +async function sellerFetch(url, method = 'GET', payload = null) { + const opts = { method, headers: sellerHeaders() } + if (payload) opts.body = JSON.stringify(payload) + const res = await fetch(url, opts) + let data + try { data = await res.json() } catch { data = {} } + if (!res.ok) throw new Error(data.message || 'Fehler') + return data +} + +export const getSellerShop = () => sellerFetch(`${BASE}/seller/shop`) +export const saveSellerShop = (p) => sellerFetch(`${BASE}/seller/shop`, 'POST', p) +export const addSellerProduct = (p) => sellerFetch(`${BASE}/seller/products`, 'POST', p) +export const updateSellerProduct = (id, p) => sellerFetch(`${BASE}/seller/products/${id}`, 'PUT', p) +export const deleteSellerProduct = (id) => sellerFetch(`${BASE}/seller/products/${id}`, 'DELETE') +export const getSellerMessages = () => sellerFetch(`${BASE}/seller/messages`) +// ───────────────────────────────────────────────────────────────────────────── + export async function fetchProducts(params = {}) { const q = new URLSearchParams(params).toString() const res = await fetch(`${BASE}/products?${q}`) @@ -40,3 +67,19 @@ export async function sendMessage(payload) { if (!res.ok) throw new Error('Nachricht konnte nicht gesendet werden') return res.json() } + +const AUTH_HEADERS = { 'Content-Type': 'application/json', 'Accept': 'application/json' } + +async function authFetch(url, payload) { + const res = await fetch(url, { method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify(payload) }) + let data + try { data = await res.json() } catch { data = {} } + if (!res.ok) throw new Error(data.message || 'Ein Fehler ist aufgetreten') + return data +} + +export const registerUser = (p) => authFetch(`${BASE}/auth/register`, p) +export const verifyUser = (p) => authFetch(`${BASE}/auth/verify`, p) +export const loginUser = (p) => authFetch(`${BASE}/auth/login`, p) +export const forgotPassword = (p) => authFetch(`${BASE}/auth/forgot-password`, p) +export const resetPassword = (p) => authFetch(`${BASE}/auth/reset-password`, p) diff --git a/Dilomarkt/src/router/index.js b/Dilomarkt/src/router/index.js index 5f7879a..13c67d0 100644 --- a/Dilomarkt/src/router/index.js +++ b/Dilomarkt/src/router/index.js @@ -3,6 +3,14 @@ import HomeView from '@/views/HomeView.vue' import SearchView from '@/views/SearchView.vue' import ProductView from '@/views/ProductView.vue' import ProviderView from '@/views/ProviderView.vue' +import RegisterView from '@/views/RegisterView.vue' +import VerifyView from '@/views/VerifyView.vue' +import LoginView from '@/views/LoginView.vue' +import ResetPasswordView from '@/views/ResetPasswordView.vue' +import DashboardView from '@/views/DashboardView.vue' +import ProfileView from '@/views/ProfileView.vue' +import OrdersView from '@/views/OrdersView.vue' +import SellerView from '@/views/SellerView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -12,6 +20,14 @@ const router = createRouter({ { path: '/suche', name: 'search', component: SearchView }, { path: '/produkt/:id', name: 'product', component: ProductView }, { path: '/anbieter/:id', name: 'provider', component: ProviderView }, + { path: '/register', name: 'register', component: RegisterView }, + { path: '/verify', name: 'verify', component: VerifyView }, + { path: '/login', name: 'login', component: LoginView }, + { path: '/reset-password/:token', name: 'reset-password', component: ResetPasswordView }, + { path: '/dashboard', name: 'dashboard', component: DashboardView }, + { path: '/profil', name: 'profile', component: ProfileView }, + { path: '/bestellungen', name: 'orders', component: OrdersView }, + { path: '/mein-shop', name: 'seller', component: SellerView }, ], }) diff --git a/Dilomarkt/src/useAuth.js b/Dilomarkt/src/useAuth.js new file mode 100644 index 0000000..17c5883 --- /dev/null +++ b/Dilomarkt/src/useAuth.js @@ -0,0 +1,24 @@ +import { ref } from 'vue' + +// Module-level singleton — the same ref is shared across every component that imports this +const _user = ref(JSON.parse(localStorage.getItem('dilomarkt_user') || 'null')) + +export function useAuth() { + function setUser(u) { + _user.value = u + if (u) { + localStorage.setItem('dilomarkt_user', JSON.stringify(u)) + } else { + localStorage.removeItem('dilomarkt_user') + localStorage.removeItem('dilomarkt_token') + localStorage.removeItem('dilomarkt_seller_shop') // clear seller cache on logout + } + } + + function setToken(token) { + if (token) localStorage.setItem('dilomarkt_token', token) + else localStorage.removeItem('dilomarkt_token') + } + + return { user: _user, setUser, setToken } +} diff --git a/Dilomarkt/src/views/ChatModal.vue b/Dilomarkt/src/views/ChatModal.vue index 9aeb8d2..ee1bbb3 100644 --- a/Dilomarkt/src/views/ChatModal.vue +++ b/Dilomarkt/src/views/ChatModal.vue @@ -36,6 +36,7 @@ diff --git a/Dilomarkt/src/views/HomeView.vue b/Dilomarkt/src/views/HomeView.vue index b7b330a..076e169 100644 --- a/Dilomarkt/src/views/HomeView.vue +++ b/Dilomarkt/src/views/HomeView.vue @@ -51,7 +51,7 @@ diff --git a/Dilomarkt/src/views/OrdersView.vue b/Dilomarkt/src/views/OrdersView.vue new file mode 100644 index 0000000..f3b06b3 --- /dev/null +++ b/Dilomarkt/src/views/OrdersView.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/Dilomarkt/src/views/ProfileView.vue b/Dilomarkt/src/views/ProfileView.vue new file mode 100644 index 0000000..d175e3b --- /dev/null +++ b/Dilomarkt/src/views/ProfileView.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/Dilomarkt/src/views/RegisterView.vue b/Dilomarkt/src/views/RegisterView.vue new file mode 100644 index 0000000..177c272 --- /dev/null +++ b/Dilomarkt/src/views/RegisterView.vue @@ -0,0 +1,116 @@ + + + diff --git a/Dilomarkt/src/views/ResetPasswordView.vue b/Dilomarkt/src/views/ResetPasswordView.vue new file mode 100644 index 0000000..aa8a979 --- /dev/null +++ b/Dilomarkt/src/views/ResetPasswordView.vue @@ -0,0 +1,35 @@ + + + diff --git a/Dilomarkt/src/views/SearchView.vue b/Dilomarkt/src/views/SearchView.vue index 7c86dd2..15cff1d 100644 --- a/Dilomarkt/src/views/SearchView.vue +++ b/Dilomarkt/src/views/SearchView.vue @@ -2,28 +2,28 @@
- - +
- Alle - {{ c }} + Alle + {{ c }}
@@ -55,7 +55,7 @@ + + diff --git a/Dilomarkt/src/views/VerifyView.vue b/Dilomarkt/src/views/VerifyView.vue new file mode 100644 index 0000000..80ee3f0 --- /dev/null +++ b/Dilomarkt/src/views/VerifyView.vue @@ -0,0 +1,36 @@ + + + diff --git a/Dilomarkt/tsconfig.app.json b/Dilomarkt/tsconfig.app.json index c0f2d86..20dfde6 100644 --- a/Dilomarkt/tsconfig.app.json +++ b/Dilomarkt/tsconfig.app.json @@ -1,5 +1,5 @@ { - "extends": "@vue/tsconfig/tsconfig.dom.json", + "extends": "./node_modules/@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "exclude": ["src/**/__tests__/*"], "compilerOptions": {