# Audit Izvještaj – Krčma Petrić

## Datum
21. travnja 2026.

## Alati korišteni za audit
- Skills: `laravel-security`, `laravel-security-audit`, `eloquent-best-practices`, `laravel-best-practices`, `laravel-patterns`, `php-pro`
- Laravel Boost MCP: `application-info`, `database-schema`
- Ručna inspekcija: sve rute, kontroleri, Livewire komponente, modeli, .env

---

## Sažetak

Revizija **sigurnosti, performansi i kvalitete koda** produkcijske Laravel 12 aplikacije. Pronađeno **18+ problema** raspodijeljenih u 4 razine prioriteta. Aplikacija je konfigurirana kao razvojna (`APP_DEBUG=true`, `APP_ENV=development`), svi mailovi se šalju sinkrono, a cache i session koriste spori file driver. Kritični sigurnosni problemi su djelomično ispravili tokom ove sesije (honeypot, registracija, test rute).

### Što je već popravljeno u ovoj sesiji
- ✅ Honeypot zaštita na `ReservationForm` i `ContactForm`
- ✅ Javna registracija korisnika onemogućena (`Auth::routes(['register' => false])`)
- ✅ Test mail rute premještene iza `auth` middlewara (`/admin/test-mail` itd.)
- ✅ Debug rute `/home-original` i `/old-welcome` uklonjene
- ✅ `uploadprogress` i `mongodb` PHP ekstenzije zakomentirane (bile krivo kompajlirane za PHP 8.3)

---

## 🔴 Kritični problemi

### 1. APP_DEBUG=true i APP_ENV=development na produkciji

**Problem:** Svaka PHP greška prikazuje stack trace, putanje fajlova, SQL upite i vrijednosti varijabli svim posjetiteljima. OWASP A05.

**Lokacija:** `.env` linije 2–4

**Rješenje:**
```env
APP_ENV=production
APP_DEBUG=false
LOG_LEVEL=warning
```

---

### 2. Sinkrono slanje e-mailova – blokira request cycle

**Problem:** Svi mailovi se šalju sinkrono unutar requesta. Korisnik čeka 2–5 sekundi dok se mail pošalje. `QUEUE_CONNECTION=sync` znači da se čak i `Mail::queue()` izvršava odmah.

**Lokacije:**
- `app/Livewire/ReservationForm.php` – `Mail::to()->send()`
- `app/Livewire/ContactForm.php` – `Mail::to()->send()`
- `app/Http/Controllers/ReservationConfirmationController.php` – `Mail::to()->send()`
- `app/Http/Controllers/AdminReservationsController.php` – `Mail::to()->send()`

**Rješenje:**
```bash
php artisan queue:table
php artisan migrate
```

```env
# .env
QUEUE_CONNECTION=database
```

```php
// Umjesto Mail::to()->send():
Mail::to($email)->queue(new SomeMail($data));
```

Pokrenuti queue worker kao systemd servis:
```bash
php artisan queue:work --queue=default --tries=3 --sleep=3
```

---

### 3. Iste lozinke za DB i mail

**Problem:** `DB_PASSWORD` i `MAIL_PASSWORD` su identične (`Krcma2026??!!`). Kompromitiranje jedne znači kompromitiranje obje.

**Lokacija:** `.env` linije 30 i 43

**Rješenje:** Generirati dvije različite, jake lozinke.

---

### 4. SESSION_ENCRYPT=false

**Problem:** Session podaci pohranjeni kao plain text.

**Lokacija:** `.env` linija 37

**Rješenje:**
```env
SESSION_ENCRYPT=true
```

---

### 5. XML API rute bez autentikacije

**Problem:** `/api/xml/export`, `/api/xml/export/categories`, `/api/xml/export/daily-menus` su javno dostupne bez ikakve autentikacije.

**Lokacija:** `routes/web.php`

**Rješenje:** Dodati API token autentikaciju ili ograničiti na internu mrežu:
```php
Route::prefix('api/xml')->middleware('auth')->name('api.xml.')->group(function () {
    Route::get('/export', [XmlExportController::class, 'index'])->name('export');
    // ...
});
```

---

### 6. POST /notify bez rate limitinga

**Problem:** `/notify` ruta prihvaća email i šalje mail na njega bez rate limitinga i bez autentikacije — može se koristiti kao email relay/spam alat.

**Lokacija:** `routes/web.php`

**Rješenje:**
```php
Route::post('/notify', function (Request $request) { ... })
    ->middleware('throttle:5,1')
    ->name('notify');
```

---

### 7. Konfiguracija pohranjena u JSON datotekama

**Problem:** `slider.json`, `promo-popup.json`, `tjedni-meni.json` u `storage/app/`. Nema audit log-a, nije transakcijsko, nije backup-irano s bazom.

**Lokacije:**
- `app/Livewire/Admin/SliderManager.php`
- `app/Livewire/Admin/PromoPopupManager.php`
- `app/Livewire/Admin/TjedniMeniManager.php`

**Rješenje:** Migrirati u `configurations` tablicu:
```bash
php artisan make:model Configuration -m
```

```php
Schema::create('configurations', function (Blueprint $table) {
    $table->id();
    $table->string('key')->unique();
    $table->json('value');
    $table->timestamps();
});
```

---

## 🟠 Visoki prioritet

### 1. CACHE_STORE=file i SESSION_DRIVER=file

**Problem:** File-based cache i sesije su spori i ne skaliraju.

**Lokacija:** `.env` linije 35 i 46

**Rješenje:**
```env
CACHE_STORE=redis
SESSION_DRIVER=database
```

Ili za database cache (ako Redis nije dostupan):
```bash
php artisan cache:table
php artisan migrate
```

---

### 2. SQL upiti direktno u Blade view-u

**Problem:** `daily-menu.blade.php` izvršava `DailyMenu::with('items')->...->first()` direktno u `@php` bloku view-a.

**Lokacija:** `resources/views/pages/daily-menu.blade.php`

**Rješenje:** Logiku premjestiti u controller i proslijediti podatke:
```php
// Umjesto @php bloka u view-u:
$todayMenu = DailyMenu::with('items')
    ->where('date', today())
    ->where('is_active', true)
    ->first();
return view('pages.daily-menu', compact('todayMenu'));
```

---

### 3. Nema cachiranja za jelovnik podatke

**Problem:** Jelovnik se čita na skoro svakoj stranici (home, menu, api, sitemap), ali se rijetko mijenja. Nema caching sloja.

**Rješenje:**
```php
// app/Services/MenuService.php
public function getFeaturedItems(): Collection
{
    return Cache::remember('menu.featured', 3600, fn () =>
        MenuItem::where('is_featured', true)
            ->where('is_available', true)
            ->with('category:id,name')
            ->select('id', 'name', 'description', 'price', 'menu_category_id', 'is_featured')
            ->orderBy('order')
            ->get()
    );
}

// Invalidirati cache pri promjeni:
Cache::forget('menu.featured');
```

---

### 4. Nema eager loadinga u FeaturedMenu komponenti

**Problem:** Dva odvojena upita gdje bi bio dovoljan jedan.

**Lokacija:** `app/Livewire/FeaturedMenu.php`

**Rješenje:**
```php
$items = MenuItem::where('is_available', true)
    ->with('category:id,name')
    ->select('id', 'name', 'price', 'menu_category_id', 'is_featured')
    ->orderBy('order')
    ->get();

$this->featuredItems = $items->where('is_featured', true)->values();
$this->menuItems = $items->take(8)->values();
```

---

### 5. Nema select() – učitavaju se svi stupci

**Problem:** Upiti učitavaju sve stupce tablice gdje se koristi samo nekoliko.

**Primjeri:**
- `XmlExportController` – učitava sve, mapira samo `id`, `name`, `description`, `price`
- `FeaturedMenu` – učitava sve stupce `menu_items` tablice

**Rješenje:** Koristiti `select(['id', 'name', 'price', ...])` na upitima.

---

## 🟡 Srednji prioritet

### 1. Nema return type deklaracija na kontrolerima

**Problem:** Admin kontroleri nemaju povratne tipove na metodama.

**Primjeri:**
- `AdminMenuController` – `index()`, `storeCategory()`, `updateCategory()`, itd.
- `AdminDailyMenuController` – sve metode
- `AdminReservationsController` – sve metode

**Rješenje:**
```php
public function index(): View { ... }
public function storeCategory(Request $request): RedirectResponse { ... }
public function confirm(int $id): RedirectResponse { ... }
```

---

### 2. Nema return type na relacijama u modelima

**Problem:** Relacione metode u modelima nemaju eksplicitne return type-ove.

**Primjeri:**
- `MenuCategory::items()` – treba `HasMany`
- `MenuItem::category()` – treba `BelongsTo`
- `DailyMenu::items()` – treba `HasMany`
- `DailyMenuItem::dailyMenu()` – treba `BelongsTo`

**Rješenje:**
```php
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

public function items(): HasMany
{
    return $this->hasMany(MenuItem::class);
}
```

---

### 3. Nema Form Request klasa za admin kontrolere

**Problem:** Validacija je inline u kontrolerima umjesto u Form Request klasama.

**Lokacije:**
- `AdminMenuController::storeItem()` / `updateItem()`
- `AdminDailyMenuController::storeMenu()` / `storeItem()`

**Rješenje:**
```bash
php artisan make:request Admin/StoreMenuItemRequest
php artisan make:request Admin/StoreDailyMenuRequest
```

---

### 4. Fat kontroleri – nedostaju service klase

**Problem:** `AdminReservationsController` sadrži logiku potvrde, otkazivanja, slanja maila i logiranja. Trebao bi `ReservationService`.

**Rješenje:**
```php
// app/Services/ReservationService.php
class ReservationService
{
    public function confirm(Reservation $reservation): void
    {
        $reservation->confirm();
        Mail::to($reservation->email)->queue(new ReservationConfirmedMail($reservation));
        Log::info('Reservation confirmed', ['id' => $reservation->id]);
    }

    public function cancel(Reservation $reservation): void
    {
        $reservation->cancel();
        Mail::to($reservation->email)->queue(new ReservationCancelledMail($reservation));
        Log::info('Reservation cancelled', ['id' => $reservation->id]);
    }
}
```

---

### 5. Hard-coded email adrese u konzolnim komandama

**Problem:** Email adrese `info@krcma-petric.com`, `kristijan.cpn@gmail.com`, `mkopcic@gmail.com` su hard-codirane u više mjesta.

**Lokacije:**
- `app/Console/Commands/ClearLogs.php`
- `app/Console/Commands/WeeklyReservationReport.php`
- `routes/web.php` (admin test rute)

**Rješenje:**
```php
// config/contacts.php
return [
    'admin' => env('ADMIN_EMAIL', 'info@krcma-petric.com'),
    'notify' => [
        env('ADMIN_EMAIL', 'info@krcma-petric.com'),
        'kristijan.cpn@gmail.com',
        'mkopcic@gmail.com',
    ],
];

// Korištenje:
Mail::to(config('contacts.notify'))->queue(new LogsClearedMail($logs));
```

---

### 6. Rate limiting na Livewire formama

**Problem:** `ReservationForm` i `ContactForm` nemaju rate limitinga na razini komponente, honeypot je jedina zaštita.

**Rješenje (Livewire 4):**
```php
use Illuminate\Support\Facades\RateLimiter;

public function submit(): void
{
    $key = 'reservation.' . request()->ip();

    if (RateLimiter::tooManyAttempts($key, 5)) {
        $this->addError('general', 'Previše pokušaja. Pokušajte za nekoliko minuta.');
        return;
    }

    RateLimiter::hit($key, 300); // 5 puta u 5 minuta
    // ...
}
```

---

## 🟢 Niski prioritet / Poboljšanja

### 1. Tracking načina potvrde rezervacije

**Problem:** Nema razlikovanja je li rezervacija potvrđena od strane gosta (link) ili admina.

**Rješenje:**
```bash
php artisan make:migration add_confirmed_by_to_reservations_table
```

```php
$table->enum('confirmed_by', ['guest', 'admin'])->nullable()->after('confirmed_at');
$table->timestamp('cancelled_at')->nullable()->after('confirmed_by');
```

---

### 2. Chunking u SitemapController

**Problem:** `SitemapController` učitava sve `MenuItem` odjednom bez limitacije.

**Rješenje:**
```php
MenuItem::where('is_available', true)
    ->select('id', 'name', 'updated_at')
    ->chunkById(100, function ($items) use ($sitemap) {
        foreach ($items as $item) {
            $sitemap->add(Url::create(route('menu').'#'.$item->id)
                ->setLastModificationDate($item->updated_at));
        }
    });
```

---

### 3. select() optimizacija u XmlExportController

**Lokacija:** `app/Http/Controllers/Api/XmlExportController.php`

**Rješenje:**
```php
$menuItems = MenuItem::select('id', 'name', 'description', 'price', 'menu_category_id', 'is_featured')
    ->with('category:id,name')
    ->where('is_available', true)
    ->get();
```

---

## ✅ Što je dobro urađeno

1. **Honeypot zaštita** – Obje javne forme koriste `spatie/laravel-honeypot` s Livewire integracijom
2. **Logging** – Detaljan logging u svim kritičnim operacijama (potvrda, otkazivanje, mail greške)
3. **Token-based potvrda rezervacija** – Jedinstveni confirmation tokeni (64 znaka, `Str::random`)
4. **Middleware autentikacija** – Sve admin rute zaštićene
5. **Casts u modelima** – Type-safe atributi (`date`, `datetime`, `decimal`, `hashed`)
6. **Validacija s porukama** – Detaljne validacijske poruke na hrvatskom jeziku
7. **Paginacija** – `AdminReservationsController::index()` koristi `paginate(20)`
8. **SEO** – Svaka stranica ima metadata i robots tag
9. **Eager loading** – `AdminMenuController` koristi `with('items')` na listama
10. **Registracija onemogućena** – `Auth::routes(['register' => false])` ✅

---

## Akcijski plan (po prioritetu)

### HITNO – Sigurnost (odmah)

| # | Zadatak | Datoteka | Procjena |
|---|---------|----------|----------|
| 1 | `APP_ENV=production`, `APP_DEBUG=false`, `LOG_LEVEL=warning` | `.env` | 5 min |
| 2 | `SESSION_ENCRYPT=true` | `.env` | 5 min |
| 3 | Promijeniti lozinke za DB i mail (različite) | `.env` + hosting | 15 min |
| 4 | Rate limiting na `/notify` | `routes/web.php` | 15 min |
| 5 | Auth na XML API rute | `routes/web.php` | 15 min |

### VISOKI prioritet – Performanse (ovo tjedan)

| # | Zadatak | Datoteka | Procjena |
|---|---------|----------|----------|
| 6 | `QUEUE_CONNECTION=database` + jobs tablica | `.env` + migracija | 30 min |
| 7 | `Mail::send()` → `Mail::queue()` svugdje | Livewire + Kontroleri | 1 h |
| 8 | `CACHE_STORE=redis/database` | `.env` | 15 min |
| 9 | `SESSION_DRIVER=database` | `.env` | 15 min |
| 10 | Queue worker systemd servis | Server | 30 min |

### SREDNJI prioritet – Arhitektura (do kraja mjeseca)

| # | Zadatak | Datoteka | Procjena |
|---|---------|----------|----------|
| 11 | SQL iz blade view-a → controller | `daily-menu.blade.php` | 30 min |
| 12 | Caching za jelovnik (`MenuService`) | novi servis | 2 h |
| 13 | `config/contacts.php` za email adrese | novi config | 30 min |
| 14 | Rate limiting na Livewire formama | `ReservationForm`, `ContactForm` | 1 h |
| 15 | Return type deklaracije na kontrolerima | Admin kontroleri | 2 h |
| 16 | Return type na relacijama u modelima | Svi modeli | 1 h |
| 17 | Form Request klase za admin forme | novi Requests | 3 h |
| 18 | `ReservationService` klasa | novi servis | 2 h |

### NISKI prioritet – Poboljšanja (planirati)

| # | Zadatak | Datoteka | Procjena |
|---|---------|----------|----------|
| 19 | Migrirati JSON config u bazu | SliderManager, PromoPopup, TjedniMeni | 8 h |
| 20 | `select()` optimizacija upita | XmlExport, FeaturedMenu | 1 h |
| 21 | `chunkById()` u SitemapController | SitemapController | 30 min |
| 22 | `confirmed_by` tracking na rezervacijama | nova migracija | 30 min |
| 23 | Eager loading optimizacija u FeaturedMenu | FeaturedMenu | 30 min |

---

*Audit proveden: 21. travnja 2026. | GitHub Copilot (Claude Sonnet 4.6)*
