diff --git a/app/Http/Controllers/Admin/Inventory/ProductionController.php b/app/Http/Controllers/Admin/Inventory/ProductionController.php index 4a44cea..19c5b14 100644 --- a/app/Http/Controllers/Admin/Inventory/ProductionController.php +++ b/app/Http/Controllers/Admin/Inventory/ProductionController.php @@ -33,8 +33,13 @@ class ProductionController extends Controller ]); } - public function create(Request $request): View + public function create(Request $request): View|RedirectResponse { + // Audit M-2: Produktionen buchen Bestand — nur admin >= 7. + if (! auth()->user()->isAdmin()) { + return redirect()->route('home'); + } + $defaultLocationId = Location::query()->where('name', 'like', '%öln%')->value('id') ?? Location::query()->where('active', true)->first()?->id; @@ -101,8 +106,11 @@ class ProductionController extends Controller ]); } - public function edit(Production $production): View + public function edit(Production $production): View|RedirectResponse { + if (! auth()->user()->isAdmin()) { + return redirect()->route('home'); + } $production->load([ 'product', 'location', @@ -161,8 +169,11 @@ class ProductionController extends Controller return redirect()->route('admin.inventory.productions.show', $production); } - public function copy(Production $production): View + public function copy(Production $production): View|RedirectResponse { + if (! auth()->user()->isAdmin()) { + return redirect()->route('home'); + } $production->load([ 'product', 'productionIngredients.ingredient', diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 0a434f0..3667ab6 100755 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -222,8 +222,13 @@ class ProductController extends Controller public function imageAttribute($product_id, $attr, $val = false) { + // Audit M-7: nur erlaubte Spalten — $attr kommt aus der URL und würde + // sonst die $fillable-Whitelist per dynamischer Property umgehen. + if (! in_array($attr, ['active', 'pos'], true) || ! is_numeric($val)) { + abort(400); + } - if (is_numeric($val) && $val < 0) { + if ($val < 0) { $val = 0; } diff --git a/app/Http/Requests/Inventory/StorePackagingItemRequest.php b/app/Http/Requests/Inventory/StorePackagingItemRequest.php index 26f9e32..6eac1d5 100644 --- a/app/Http/Requests/Inventory/StorePackagingItemRequest.php +++ b/app/Http/Requests/Inventory/StorePackagingItemRequest.php @@ -24,7 +24,7 @@ class StorePackagingItemRequest extends FormRequest 'category' => ['required', Rule::in(['packaging', 'shipping'])], 'weight_grams' => ['nullable', 'numeric', 'min:0'], 'min_stock_alert' => ['nullable', 'integer', 'min:0'], - 'url' => ['nullable', 'string', 'max:2048'], + 'url' => ['nullable', 'string', 'url:http,https', 'max:2048'], 'product_id' => ['nullable', 'integer', 'exists:products,id'], 'active' => ['sometimes', 'boolean'], ]; diff --git a/app/Http/Requests/Inventory/StoreProductionRequest.php b/app/Http/Requests/Inventory/StoreProductionRequest.php index b5a5ac3..d0a19ab 100644 --- a/app/Http/Requests/Inventory/StoreProductionRequest.php +++ b/app/Http/Requests/Inventory/StoreProductionRequest.php @@ -9,7 +9,9 @@ class StoreProductionRequest extends FormRequest { public function authorize(): bool { - return true; + // Audit M-2: Produktionen buchen Produktbestand — gleiche Schranke + // wie manuelle Bestandsbewegungen (admin >= 7). + return (bool) $this->user()?->isAdmin(); } /** diff --git a/app/Http/Requests/Inventory/StoreSupplierRequest.php b/app/Http/Requests/Inventory/StoreSupplierRequest.php index ef0b408..b399c11 100644 --- a/app/Http/Requests/Inventory/StoreSupplierRequest.php +++ b/app/Http/Requests/Inventory/StoreSupplierRequest.php @@ -18,10 +18,10 @@ class StoreSupplierRequest extends FormRequest { return [ 'name' => ['required', 'string', 'max:255'], - 'url' => ['nullable', 'string', 'max:2048'], + 'url' => ['nullable', 'string', 'url:http,https', 'max:2048'], 'order_method' => ['nullable', 'in:email,online_shop'], 'order_email' => ['nullable', 'email', 'max:255'], - 'order_url' => ['nullable', 'string', 'max:2048'], + 'order_url' => ['nullable', 'string', 'url:http,https', 'max:2048'], 'delivery_time' => ['nullable', 'string', 'max:255'], 'delivery_time_days' => ['nullable', 'integer', 'min:0', 'max:65535'], 'contact_person' => ['nullable', 'string', 'max:255'], diff --git a/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php b/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php index da67a49..4b4785f 100644 --- a/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php +++ b/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php @@ -24,7 +24,7 @@ class UpdatePackagingItemRequest extends FormRequest 'category' => ['required', Rule::in(['packaging', 'shipping'])], 'weight_grams' => ['nullable', 'numeric', 'min:0'], 'min_stock_alert' => ['nullable', 'integer', 'min:0'], - 'url' => ['nullable', 'string', 'max:2048'], + 'url' => ['nullable', 'string', 'url:http,https', 'max:2048'], 'product_id' => ['nullable', 'integer', 'exists:products,id'], 'active' => ['sometimes', 'boolean'], ]; diff --git a/app/Http/Requests/Inventory/UpdateSupplierRequest.php b/app/Http/Requests/Inventory/UpdateSupplierRequest.php index 271ad44..4d8de67 100644 --- a/app/Http/Requests/Inventory/UpdateSupplierRequest.php +++ b/app/Http/Requests/Inventory/UpdateSupplierRequest.php @@ -18,10 +18,10 @@ class UpdateSupplierRequest extends FormRequest { return [ 'name' => ['required', 'string', 'max:255'], - 'url' => ['nullable', 'string', 'max:2048'], + 'url' => ['nullable', 'string', 'url:http,https', 'max:2048'], 'order_method' => ['nullable', 'in:email,online_shop'], 'order_email' => ['nullable', 'email', 'max:255'], - 'order_url' => ['nullable', 'string', 'max:2048'], + 'order_url' => ['nullable', 'string', 'url:http,https', 'max:2048'], 'delivery_time' => ['nullable', 'string', 'max:255'], 'delivery_time_days' => ['nullable', 'integer', 'min:0', 'max:65535'], 'contact_person' => ['nullable', 'string', 'max:255'], diff --git a/app/Http/Requests/StoreIngredientRequest.php b/app/Http/Requests/StoreIngredientRequest.php index 9b859ab..6f532d2 100644 --- a/app/Http/Requests/StoreIngredientRequest.php +++ b/app/Http/Requests/StoreIngredientRequest.php @@ -30,7 +30,7 @@ class StoreIngredientRequest extends FormRequest 'delivery_time_days' => ['nullable', 'integer', 'min:0', 'max:65535'], 'suppliers' => ['nullable', 'array'], 'suppliers.*.id' => ['nullable', 'integer', 'exists:suppliers,id'], - 'suppliers.*.url' => ['nullable', 'string', 'max:2048'], + 'suppliers.*.url' => ['nullable', 'string', 'url:http,https', 'max:2048'], 'storage_room_id' => ['nullable', 'integer', 'exists:storage_rooms,id'], 'storage_shelf_id' => ['nullable', 'integer', 'exists:storage_shelves,id'], 'storage_slot_id' => ['nullable', 'integer', 'exists:storage_slots,id'], diff --git a/resources/views/admin/inventory/product-stock/index.blade.php b/resources/views/admin/inventory/product-stock/index.blade.php index 2597eff..3eb355c 100644 --- a/resources/views/admin/inventory/product-stock/index.blade.php +++ b/resources/views/admin/inventory/product-stock/index.blade.php @@ -132,10 +132,10 @@ + + {{ __('Produzieren') }} @endif - - {{ __('Produzieren') }} diff --git a/resources/views/admin/inventory/productions/index.blade.php b/resources/views/admin/inventory/productions/index.blade.php index fb42b20..dc6e131 100644 --- a/resources/views/admin/inventory/productions/index.blade.php +++ b/resources/views/admin/inventory/productions/index.blade.php @@ -10,9 +10,11 @@

{{ __('Erfasste Produktionschargen inkl. Rohstoff- und Verpackungsverbrauch') }}

- - {{ __('Neue Produktion') }} - + @if (Auth::user()->isAdmin()) + + {{ __('Neue Produktion') }} + + @endif
@@ -36,13 +38,13 @@ - - - - - - @if(Auth::user()->isAdmin()) + + + + + +
@csrf diff --git a/resources/views/admin/inventory/stock-entries/index.blade.php b/resources/views/admin/inventory/stock-entries/index.blade.php index 36e4070..41bd3e9 100644 --- a/resources/views/admin/inventory/stock-entries/index.blade.php +++ b/resources/views/admin/inventory/stock-entries/index.blade.php @@ -46,9 +46,10 @@ @endif @if(Auth::user()->isAdmin()) - - - + + @csrf + +
@endif diff --git a/resources/views/admin/inventory/stock-entries/show.blade.php b/resources/views/admin/inventory/stock-entries/show.blade.php index df0d8eb..83d2ae5 100644 --- a/resources/views/admin/inventory/stock-entries/show.blade.php +++ b/resources/views/admin/inventory/stock-entries/show.blade.php @@ -22,7 +22,10 @@
{{ __('Zurück zur Liste') }} @if(Auth::user()->isAdmin()) - {{ __('Duplizieren') }} +
+ @csrf + +
@endif @if(Auth::user()->isAdmin() && $model->isPending()) {{ __('Bearbeiten') }} diff --git a/resources/views/admin/product/form.blade.php b/resources/views/admin/product/form.blade.php index 23fbad7..2c02b08 100755 --- a/resources/views/admin/product/form.blade.php +++ b/resources/views/admin/product/form.blade.php @@ -637,10 +637,12 @@ — @if ($product->id) - +
+ @csrf + @method('DELETE') + +
@else diff --git a/resources/views/admin/product/index.blade.php b/resources/views/admin/product/index.blade.php index 7b75e71..c1ffbc3 100755 --- a/resources/views/admin/product/index.blade.php +++ b/resources/views/admin/product/index.blade.php @@ -97,9 +97,12 @@   - +
+ @csrf + @method('DELETE') + +
@endforeach diff --git a/routes/web.php b/routes/web.php index a52e2d1..a6ec8a0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -323,7 +323,7 @@ Route::domain(config('app.domain'))->group(function () { Route::post('stock-disposals', [InventoryStockDisposalController::class, 'store'])->name('stock-disposals.store'); Route::delete('stock-disposals/{stock_disposal}', [InventoryStockDisposalController::class, 'destroy'])->name('stock-disposals.destroy'); Route::put('stock-entries/{stock_entry}/receive', [InventoryStockEntryController::class, 'receive'])->name('stock-entries.receive'); - Route::get('stock-entries/{stock_entry}/copy', [InventoryStockEntryController::class, 'copy'])->name('stock-entries.copy'); + Route::post('stock-entries/{stock_entry}/copy', [InventoryStockEntryController::class, 'copy'])->name('stock-entries.copy'); Route::resource('stock-entries', InventoryStockEntryController::class); Route::resource('productions', InventoryProductionController::class)->only(['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']); Route::get('productions/{production}/copy', [InventoryProductionController::class, 'copy'])->name('productions.copy'); @@ -337,7 +337,7 @@ Route::domain(config('app.domain'))->group(function () { Route::post('/admin/product/store', [ProductController::class, 'store'])->name('admin_product_store'); Route::get('/admin/product/edit/{id}', [ProductController::class, 'edit'])->name('admin_product_edit'); Route::get('/admin/product/copy/{id}', [ProductController::class, 'copy'])->name('admin_product_copy'); - Route::get('/admin/product/delete/{id}/{do?}/{did?}', [ProductController::class, 'delete'])->name('admin_product_delete'); + Route::delete('/admin/product/delete/{id}/{do?}/{did?}', [ProductController::class, 'delete'])->name('admin_product_delete'); // products images Route::post('/admin/product/image/upload', [ProductController::class, 'imageUpload'])->name('admin_product_image_upload'); Route::get('/admin/product/image/delete/{image_id}/{product_id}', [ProductController::class, 'imageDelete'])->name('admin_product_image_delete'); diff --git a/tests/Feature/ProductPhase51Test.php b/tests/Feature/ProductPhase51Test.php index 1566dfd..742c3ad 100644 --- a/tests/Feature/ProductPhase51Test.php +++ b/tests/Feature/ProductPhase51Test.php @@ -170,7 +170,7 @@ test('produkt-kopie uebernimmt beide rezepturen', function () { test('produktions-formular zeigt nur aktive produkte', function () { p51EnsureCountry(); Location::factory()->create(['name' => 'Köln']); - $user = p51MakeUser(1); + $user = p51MakeUser(7); Product::query()->create(['name' => 'P51-AKTIV-PROD', 'title' => 'P51-AKTIV-PROD', 'active' => true]); Product::query()->create(['name' => 'P51-INAKTIV-PROD', 'title' => 'P51-INAKTIV-PROD', 'active' => false]); diff --git a/tests/Feature/ProductSetTest.php b/tests/Feature/ProductSetTest.php index 58d4515..006f01c 100644 --- a/tests/Feature/ProductSetTest.php +++ b/tests/Feature/ProductSetTest.php @@ -176,7 +176,7 @@ test('set kann nicht produziert werden', function () { test('produktions-formular zeigt keine sets', function () { p51EnsureCountryForSet(); Location::factory()->create(['name' => 'Köln']); - $user = setMakeUser(1); + $user = setMakeUser(7); setMakeProduct('SET-FORM-EINZEL'); $set = setMakeProduct('SET-FORM-SET'); diff --git a/tests/Feature/ProductionHardeningTest.php b/tests/Feature/ProductionHardeningTest.php index 72b936b..62081e7 100644 --- a/tests/Feature/ProductionHardeningTest.php +++ b/tests/Feature/ProductionHardeningTest.php @@ -158,3 +158,37 @@ test('ausschuss ueber restbestand wird abgelehnt (M-1)', function () { $response->assertSessionHasErrors('quantity'); expect(StockDisposal::query()->count())->toBe(0); }); + +test('copyreader darf keine produktion buchen (M-2)', function () { + $viewer = User::query()->create([ + 'email' => uniqid('ph_', true).'@test.example', + 'password' => bcrypt('password'), + ]); + $viewer->forceFill(['admin' => 1, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save(); + $this->actingAs($viewer->fresh(), 'user'); + + $product = Product::query()->create(['name' => 'PH-M2', 'title' => 'PH-M2', 'active' => true, 'no_recipe_required' => true]); + $location = Location::factory()->create(); + + $this->post(route('admin.inventory.productions.store'), [ + 'product_id' => $product->id, + 'location_id' => $location->id, + 'produced_at' => '2026-07-15', + 'quantity' => 5, + 'outputs' => [['product_id' => $product->id, 'quantity' => 5, 'batch_number' => null]], + ])->assertForbidden(); + + expect(Production::query()->count())->toBe(0); +}); + +test('javascript-url wird bei lieferanten abgelehnt (M-10)', function () { + $user = phUser(); + $this->actingAs($user, 'user'); + + $this->post(route('admin.inventory.suppliers.store'), [ + 'name' => 'PH-XSS-Lieferant', + 'url' => 'javascript:alert(1)', + ])->assertSessionHasErrors('url'); + + expect(Supplier::query()->where('name', 'PH-XSS-Lieferant')->exists())->toBeFalse(); +}); diff --git a/tests/Feature/StockEntryPriceTest.php b/tests/Feature/StockEntryPriceTest.php index b34e062..10c7647 100644 --- a/tests/Feature/StockEntryPriceTest.php +++ b/tests/Feature/StockEntryPriceTest.php @@ -169,7 +169,7 @@ test('copy creates pending duplicate without receiving data', function () { $this->actingAs($admin, 'user'); - $this->get(route('admin.inventory.stock-entries.copy', $source)) + $this->post(route('admin.inventory.stock-entries.copy', $source)) ->assertRedirect(); $copy = StockEntry::query()->where('status', 'pending')->latest('id')->firstOrFail(); @@ -198,6 +198,6 @@ test('copy is blocked for non admin', function () { $this->actingAs($copyreader->fresh(), 'user'); - $this->get(route('admin.inventory.stock-entries.copy', $source)) + $this->post(route('admin.inventory.stock-entries.copy', $source)) ->assertRedirect(route('home')); });