UPGRADE — Lvntr Starter Kit

Bu dosya büyük sürümler arası geçiş rehberidir. Her sürüm kendi bölümünü taşır; son sürüm en üstte. Küçük hata düzeltmeleri yalnız CHANGELOG.md'de listelenir — bu dosyaya sadece publish edilmiş (yani sk:install ile user app'ine kopyalanan) dosyalara dokunan değişiklikler girer, çünkü bu tip değişiklikleri composer update tek başına taşımaz.


v13.7.3 → v13.7.4

"Debug Mode" rozeti artık ortama değil system_admin rolüne bakıyor

Etkilenen: app/Http/Middleware/HandleInertiaRequests.php dosyasını düzenlemiş uygulamalar. Etkilenmeyen: dosyaya dokunmamış uygulamalar — sk:update dosyayı hash ile takip ettiği için yeni sürümü kendisi getirir.

appDebug production'ın tamamında false paylaşılıyordu; yani APP_DEBUG=true değerinin hâlâ açık olduğunu gösteren tek işaret olan header'daki "Debug Mode" rozeti, açık kalmasının gerçekten bedeli olduğu tek sunucuda sönüyordu. Bayrak artık ortama değil izleyene bakıyor: system_admin rozeti her ortamda görüyor, başka hiç kimseye bayrak gitmiyor — böylece ortam kontrolünün yazılma sebebi olan parmak izi kaygısı da yerinde kalıyor.

sk:update dosyanızın ayrıştığını bildirirse (düzenlemenizi korur), iki satırlık değişikliği elle uygulayın:

// önce
'appDebug' => fn () => app()->environment('production') ? false : (bool) config('app.debug'),

// sonra
'appDebug' => fn () => (bool) config('app.debug')
    && ($request->user()?->hasRole('system_admin') ?? false),

php artisan sk:update --dry-run hiçbir şey yazılmadan önce farkı gösterir. appEnv değişmedi — "Dev Mode" rozeti zaten yalnızca local'de çiziliyor. Kontrol ek sorgu maliyeti getirmez: roles ilişkisi aynı metottaki auth paylaşımı için hâlihazırda yükleniyor.


v13.6.16 → v13.7.0

Etkilenen: FileManager (ya da avatar) media diski local ya da public olan kurulumlar — temporary/signed-URL desteği olmayan her disk. Etkilenmeyen: S3 ve temporary URL destekleyen herhangi bir disk (değişmedi).

FileItemDTO::fromModel(), disk getTemporaryUrl()'de hata fırlattığında (her local/public disk fırlatır) daha önce Media::getUrl()'e düşüyordu. Bu URL kalıcı, kimlik doğrulaması gerektirmiyor ve sonsuza dek çalışıyor: FileManagerAuthorizer'ı tamamen atlıyor, bir izin iptalinden sonra da dosyayı sunmaya devam ediyor, dosya çöp kutusuna taşındıktan sonra da sunmaya devam ediyor. Artık bunun yerine files.download'ın zaten kullandığı aynı authorizeRead() kontrolünden ve context guard'dan geçen yeni bir yetkili files.preview route'una düşülüyor; kimlik doğrulanmış bir oturum gerektiriyor (aynı origin'deki bir <img>/<a> için tarayıcı session cookie'sini otomatik gönderir).

Kontrol edilecek: local/public disk üzerindeki bir FileManager listelemesinden uygulamanızın sakladığı, e-postayla gönderdiği ya da başka şekilde dağıttığı herhangi bir URL (kaydedilmiş bir link, client tarafında cache'lenmiş bir değer, bir bildirimde gönderilen link) artık eski çıplak /storage/... yoluna işaret ediyor ve yalnızca o dosya o yolda fiziksel olarak durduğu sürece çalışmaya devam ediyor — geriye dönük olarak yetkilendirme KAZANMAYACAK, yeni preview URL'ine de OTOMATİK GÜNCELLENMEYECEK. Çalışmaya devam etmesi gerekiyorsa böyle bir link taze bir FileManager listelemesinden (ya da download linkinden) yeniden üretilmeli, erişim kontrollü olması gerekiyorsa files.preview/files.download linki olarak yeniden verilmeli. FileItemDTO->url'i her istekte taze okuyan bir tüketici (normal durum — shipped frontend'in hiçbir yeri onu kalıcılaştırmıyor) hiçbir değişiklik yapmak zorunda değil.

Görsel yayımlamak hâlâ mümkün — public_url kullanın. FileManager listeleme kaydı artık ikinci bir URL alanı taşıyor: disk 'visibility' => 'public' ilan edilmişse public_url = Media::getUrl(), herkese açık okunmayan disklerde null. url oturuma bağlı önizleme bağlantısı olarak kalır ve dosya tarayıcısının her yerde kullandığı alandır. Yönetim oturumundan uzun yaşayan içeriğe dosya URL'si yazan her şey — kit'te bunun örneği zengin metin editörünün gömdüğü <img src> — public_url okumalı, yalnızca null ise url'ye düşmelidir. Kit'in kendi EditorInput.vue dosyası bunu zaten yapıyor. Kendi yayımlama yüzeyinizi FileItemDTO->url üzerine kurduysanız public_url'e geçirin.

Range istekleri. files.preview route'u local sürücülü disklerde BinaryFileResponse üzerinden servis eder; Range/206 çalışır, satır içi oynatıcı ileri/geri sarabilir. Gerçek dosya sistemi yolu olmayan uzak sürücüler akış yanıtını korur ve eskisi gibi tüm gövdeyi 200 ile döndürür.

Lvntr\StarterKit\Traits\HasMediaCollections::getMediaForForm() (avatar ve diğer forma bağlı media için kullanılır) hâlâ doğrudan Media::getUrl() çağırıyor ve bu turda kasıtlı olarak değiştirilmedi — ayrı, daha dar bir yüzey (bir listeleme değil, bağlı bir form field'ı) ve aynı ham-URL desenini taşıyor; burada ele alınmadı.

Aura sayfa başlığı içerik kartına taşındı — AdminLayout.vue ve AdminHeader.vue birlikte göç eder

Etkilenenler: aura temasında stubs/resources/js/layouts/AdminLayout.vue dosyasını özelleştirmiş kurulumlar. Etkilenmeyenler: main teması ya da iki layout dosyasına da dokunmamış uygulamalar.

Aura; sayfa başlığını, alt başlığını ve geri butonunu topbar'da göstermesi için AdminHeader'a devrediyordu. Başlık artık içerik kartının kendi head'inde çiziliyor; paketle gelen AdminLayout.vue pageTitle/pageSubtitle/showBack prop'larını bağlamıyor ve back olayını dinlemiyor. sk:update her yayımlanmış dosyayı kendi hash'iyle izlediği için, yalnız layout'u düzenlemiş bir uygulama düzenlediği kopyayı korur ve yeni header'ı yine de alır — bu birleşimde eski layout, artık hiçbir yerde render edilmeyen bir başlık ve geri butonu gönderiyor olurdu.

Bu nedenle AdminHeader.vue üç prop'u da kabul etmeye, back olayını yaymaya ve değerler verildiğinde topbar başlık bloğunu ve geri butonunu çizmeye devam ediyor. Varsayılan olarak bunları hiçbir şey bağlamaz, dolayısıyla temiz kurulum etkilenmez. (showBack/back hiçbir etiketli sürümde yer almadı — yalnız iki 13.7.0 geliştirme commit'i arasında vardı — bu nedenle köprünün bu yarısı yalnız sürüm etiketinden değil main'den kurulmuş uygulamaları ilgilendirir.) Layout'u özelleştirdiyseniz ya yeni sayfa başlığı bağlantısını paketle gelen AdminLayout.vue dosyasından alın (php artisan sk:update --dry-run ile karşılaştırın) ya da prop'ları bağlamayı sürdürün — ikisi de bir sayfa başlığı çizer, yalnız farklı yerlerde. Köprü yalnız geriye uyumluluk içindir ve ileriki bir majör sürümde kaldırılacaktır.

stubs/package.json'dan iki @tiptap/* paketi kaldırıldı

@tiptap/extension-task-item ve @tiptap/extension-task-list, stub'ın doğrudan bağımlılıklarından kaldırıldı — hiçbiri kitin kendi kodunda (EditorInput.vue ya da başka bir yerde) import edilmiyor. Kendi kodunuz bunlardan birini doğrudan import ediyorsa (özel bir rich-text extension'ı, editörün üzerine kurulmuş bir task-list özelliği) kendi uygulamanızın package.json'ına geri ekleyin; sk:update/composer update/npm install artık bunları sizin için, transitively bile, çekmiyor.

npm install ERESOLVE ile patlayabilir — package-lock.json'ı silip yeniden kurun

Etkilenen: 13.6.x ile kurduğu package-lock.json'ı koruyup 13.7.x'e geçen her uygulama. Etkilenmeyen: lockfile'ı zaten yeniden üreten temiz bir sk:install.

13.7.0 tüm frontend araç zincirini güncel sürümlerine taşıdı, @tiptap/* de dahil (^3.27.1 → ^3.31.3). sk:update yeni kısıtları package.json'ınıza birleştiriyor, ama package-lock.json'ınız hâlâ eski grafiği sabitliyor — ve her @tiptap/extension-* paketi @tiptap/core'a tam sürüm peer bağımlılığı tanımlıyor (peer @tiptap/core@"3.27.1"), yani npm ailenin yarısını yükseltip gerisini bırakamıyor. Pes ediyor:

npm error ERESOLVE could not resolve
npm error Found: @tiptap/core@3.27.1
npm error   @tiptap/core@"^3.31.3" from the root project
npm error   peer @tiptap/core@"3.27.1" from @tiptap/extension-blockquote@3.27.1

Bayat lockfile'ı ve ondan kurulmuş ağacı silip yeniden kurun:

rm -rf node_modules package-lock.json
npm install && npm run build

Lockfile, uygulamanızın zaten tanımladığı package.json aralıklarından yeniden üretiliyor; onaylamadığınız bir kısıtın dışına hiçbir şey çıkmıyor. --force ya da --legacy-peer-deps'e başvurmayın: ikisi de node_modules'a, etrafındaki extension'ların hiç derlenmediği bir @tiptap/core bırakır — editör kurulum anında değil, çalışma anında bozulur.

LogoutUserAction artık mevcut credential'a bağlı refresh token'ı da iptal ediyor

app/Http/Controllers/Admin/*'ın logout yolu etkilenmiyor, ama stub app/Domain/Auth/Actions/LogoutUserAction.php etkileniyor: daha önce yalnızca $user->token()?->revoke() çağırıp duruyordu; bu, az önce iptal edilen access token'a bağlı canlı bir OAuth refresh token bırakıyordu — bir refresh token tasarım gereği access token'ından daha uzun yaşar, yani "logout" olduktan hemen sonra çağıran yeni bir access token basabiliyordu. Action artık önce refresh token'ı, sonra access token'ı iptal eden yeni bir Lvntr\StarterKit\Domain\User\Concerns\RevokesOAuthCredentials trait'ini kullanıyor.

Bu published bir stub — sk:update düzenlediğiniz bir kopyanın üzerine sessizce yazmayacak. LogoutUserAction'ı özelleştirdiyseniz (bir audit log çağrısı, bir Fortify::logout() hook'u, özel bir response eklediyseniz) düzeltme size otomatik ulaşmaz: diskteki hash artık yayınlanan hashle eşleşmiyor, bu yüzden üç-yönlü karşılaştırma dosyayı consumer düzenlemesi sayıp atlıyor (yukarıdaki "consumer tarafından değiştirilmiş published dosya" notuna bakın). Değişikliği elle taşıyın — use Lvntr\StarterKit\Domain\User\Concerns\RevokesOAuthCredentials;, sınıfa use RevokesOAuthCredentials; ekleyip $user->token()?->revoke() satırını $this->revokeCurrentOAuthCredentials($user); ile değiştirin. Burada sk:update --force'a başvurmayın: komut dosya argümanı almıyor, bu yüzden --force değişiklik korumasını yalnız bu dosya için değil tüm published dosyalar için kaldırır ve bütün özelleştirmelerinizi ezer. Stub'ı hiç özelleştirmemiş bir kurulum, düzeltmeyi bir sonraki sk:update'te otomatik alır.

Lvntr\StarterKit\Domain\User\Actions\RevokeUserAccessAction'daki aynı boşluk (bir operatör bir kullanıcının erişimini iptal ettiğinde, örn. Users ekranından) vendor kodunda düzeltildi ve yalnızca composer update gerektiriyor — taşınacak bir stub yok.

encryption:key --allow-acl-loss bayrağı kazandı — artık miras alınan bir ACL uyuşmazlığını da kapsıyor, yalnızca kaybı değil

.env'inizde yalnızca grup sahipliğine güvenmek yerine bir POSIX ACL izni varsa (setfacl -m u:www-data:r .env, ya da macOS'ta chmod +a karşılığı), encryption:key artık bu ACL'in replacement dosyaya taşındığını doğrulayamadığı sürece rotasyonu tamamlamayı reddediyor — önceden sahip/grup/mode'u taşıyordu ama ACL'i sessizce düşürüyordu; fileperms() bunu ne öncesinde ne sonrasında görebiliyor. Dosyaya özel ACL'i olmayan kurulumların büyük çoğunluğu davranış değişikliği görmez.

Bayrak artık AYNA yönü de kapsıyor: geçici dosya .env'in kendi dizini içinde oluşturulduğundan, dizin seviyesinde bir ACL inheritance kuralı (setfacl -d -m …, ya da macOS'ta chmod +a "… file_inherit" karşılığı) .env'in hiç sahip olmadığı bir izni bu dosyaya koyabiliyor — mode kontrolü için aynı şekilde görünmez ve yeni anahtarın tek bir byte'ı bile yazılmadan önce orada. Rotasyon artık bu miras alınan girdi de normalize edilmediği sürece tamamlanmayı reddediyor. Yalnızca .env'in dizini bir inheritance kuralı taşıyorsa ilgili; çoğu kurulum davranış değişikliği görmez.

Ret tetiklenirse ve rotasyondan hemen sonra ACL'i elle uzlaştırmayı planlıyorsanız, işlemi durdurmak yerine bir uyarıya düşürmek için --allow-acl-loss geçin (konsola yazdırılır ve loga yazılır — uyarı yalnızca dosya yolu ve ACL metnini taşır, asla anahtar materyalini değil). Bunu varsayılan bir alışkanlık olarak geçmeyin: bu ret, rotasyondan sonra web sunucusunun okuyamadığı — ya da olmaması gereken bir prensibe erişim veren — bir .env'in kozmetik bir uyarı değil, bozuk bir deploy olduğu için var.

release.sh artık gh ve etiketlenecek commit için yeşil bir uzak CI koşusu istiyor

Bu yalnızca bu paket için ./release.sh çalıştıranı etkiler — herhangi bir tüketici uygulamasını etkilemez. Yayın akışı artık şu şekilde: commit → git push origin main → CI yeşillensin → ./release.sh. Script, yerel kalite kapısını çalıştırmadan önce gh run list --commit <HEAD sha> sorguluyor ve gh yoksa, oturum açılmamışsa (gh auth login), commit hiç push edilmemişse ya da o commit için herhangi bir workflow'un en son koşusu success/skipped değilse duruyor. --skip-checks, öncekiyle aynı şekilde bunu da yerel kapının geri kalanıyla birlikte atlıyor.

sk:install artık kendisinin kurmadığı bir uygulamada çalışmayı reddediyor

sk:install daha önce bir projenin zaten kurulu olduğunu anlamak için yalnızca kendi hash kaydına (storage/starter-kit/hashes.json) güveniyordu — bu kayıt git tarafından yok sayılır, yani stateless bir deploy ya da temizlenen bir storage/ dizini canlı bir uygulamayı yepyeni gösterebiliyordu. Komut artık banner'dan önce fail-closed bir tespit adımı çalıştırıyor: kit'in şema tablolarını ve yalnızca bir kurulumun oluşturabileceği birkaç yolu arıyor. Bu kanıtlardan biri varsa ama kayıt yoksa komut hiçbir şey yazmadan durur ve tam olarak ne bulduğunu yazdırır.

Durma mesajı iki çıkış yolu adlandırır: kurulu bir uygulamayı değiştirmek için sk:update, kaydı diskteki dosyalardan yeniden inşa etmek için php artisan sk:install --adopt (önizlemek için --dry-run ekleyin) — bu hiçbir dosya kopyalamaz, hiçbir migration çalıştırmaz, .env'e hiç dokunmaz. --force gerçek bir uç durum için durmayı yine aşar; ama bunu "yukarıda listelenen yolların üzerine yaz" olarak okuyun ve zorlanmış bir koşunun ilk kurulum sayılMADIĞINI (varsayılan-domain eject yok, ilk-kurulum .env tohumlaması yok) unutmayın.

.env artık asla üzerine yazılmıyor — mevcut bir .env'e sahip bir uygulamaya ilk kurulum artık merge ediyor

İlk kurulum, mevcut bir .env'in üzerine .env.example'ı olduğu gibi kopyalardı — bu, sk:install'in zaten bir .env taşıyan sıradan composer create-project şeklinde çalıştığı her durumda DB_PASSWORD, APP_KEY ve yapılandırılmış her şeyi yok ediyordu. .env artık her iki yolda da üzerine yazılmıyor. Dosya zaten varsa installer merge ediyor: .env.example'da olup .env'de eksik olan her anahtar ekleniyor, ilk-kuruluma-özel anahtarlar ise yalnızca yoksa tohumlanıyor. Mevcut hiçbir anahtarın değeri asla yeniden yazılmıyor. .env, yalnızca hiç yoksa .env.example'dan oluşturuluyor. Merge edilmiş dosya boş bırakırsa APP_KEY hâlâ üretiliyor, böylece uygulama boot edebiliyor.

Consumer tarafından değiştirilmiş bir published dosya varsayılan olarak atlanıyor — opt-out --force

Hem sk:install hem sk:update artık bir published yolun üzerine yazılıp yazılmayacağına aynı üç-yönlü karşılaştırmayla (shipped stub hash'i vs. diskteki hash vs. son install/update'te kayda geçen hash) karar veriyor. Diskteki kopya kayda geçenle artık eşleşmiyorsa fark bir consumer düzenlemesi sayılıyor ve dosya sessizce üzerine yazılmak yerine atlanıp raporlanıyor — bu artık yalnızca sk:update'in değil, sk:install'in yeniden-publish yolunun da kapsamında. Yine de üzerine yazmak için --force verin; önce commit alın ki Git önceki sürümü erişilebilir tutsun.

Bu, aynı korumadaki geriye kalan tek boşluğu da kapatıyor: yeniden kurulumda, hash kaydında hiç izi olmayan bir dosya — çünkü yeni bir paket sürümü, bu uygulamada daha önce hiç göndermediği bir yola dosya göndermeye başladı — --force fark etmeksizin üzerine yazılıyordu. Artık bir consumer düzenlemesiyle aynı muameleyi görüyor: --force verilmedikçe üzerine yazılmak yerine korunup raporlanıyor. Bu koruma yalnızca yetkili bir kayıt varken geçerli; gerçek bir ilk kurulum, kıyaslayacak henüz hiçbir şey olmadığından izlensin izlenmesin her yolu yine yayınlıyor.

İnaktif kullanıcılar oturum ortasında kesiliyor

Login yolu zaten aktif olmayan bir hesabı reddediyordu, ama zaten açık bir oturuma erişemiyordu — bir kullanıcıyı deaktive eden bir operatör o kullanıcının session cookie'sinin kendi kendine sona ermesini beklemek zorundaydı. Yeni bir EnsureUserIsActive middleware'i (otomatik olarak web ve api guard'larına bağlanır) artık her istekte kimliği doğrulanmış kullanıcının status'unu kontrol ediyor; operatörün deny-list'iyle eşleşiyorsa bir web oturumunu kapatıp login'e yönlendiriyor, bir API isteği için ise 403 döndürüyor.

Bu, her belirsiz durumda kasıtlı olarak fail-open: kimliği doğrulanmış kullanıcısı olmayan bir guard, çözülemeyen bir guard, status attribute'u olmayan bir user modeli, string olmayan bir status ve — en kritik — deny-list'te olmayan bir değer, hepsi olduğu gibi geçiyor. Middleware asla "aktif değil, o hâlde engellenmeli" diye çıkarım yapmıyor; yalnızca açıkça listelenmiş bir status'u engelliyor. Varsayılan deny-list ['inactive', 'banned'] — shipped userStatus tanımıyla eşleşiyor; kendi vokabülerini kullanan bir kurulum kendi değerlerini starter-kit.security.active_status_denied üzerinden ekliyor. starter-kit.security.enforce_active_status = false kill switch'tir — bootstrap/app.php'a hiç dokunmadan middleware'i tamamen devre dışı bırakmak için ayarlayın. Bu security bloğu var olmadan önce config/starter-kit.php'ı publish etmiş bir consumer da kapsam içinde: middleware, published config'de yeni anahtarlar eksikse shipped varsayılanlarla eşleşen class constant'larına düşüyor.

Kurulum komutları artık zorunlu bir adım başarısız olduğunda sıfırdan farklı bir çıkış kodu döndürüyor

sk:install, sk:update, sk:upgrade ve yayınlanan site:install stub'ı migrate, db:seed, vendor:publish, sk:seed-permissions, passport:keys, key:generate gibi alt komutların sonucunu hiç okumadan çağırıyordu; bu yüzden başarısız bir migration yine DONE yazıyor, resume checkpoint'inde adımı tamamlanmış olarak kaydediyor ve komut 0 ile çıkıyordu — bir CI job'ı yarım kurulmuş bir uygulamanın üzerinden yeşil geçebiliyordu. Artık her alt komut sonucu denetleniyor; başarısız bir zorunlu adım (publish, migration, seeder, izin tohumlama, Passport anahtarları, şifreleme anahtarları) artık koşuyu sıfırdan farklı bir çıkış koduyla durduruyor, checkpoint'i beklemede bırakıyor (sk:install --resume kaldığı yerden devam ediyor) ve stub-hash registry yazımını atlıyor.

Sessizce başarısız olan bir installer adımına rağmen şu an geçen bir CI hattı bu yükseltmeden sonra başarısız olmaya başlayacaktır — bu, etrafından dolaşılacak bir regresyon değil, amaçlanan sinyaldir. Frontend ve tooling adımları (npm install, Wayfinder üretimi, npm run build, composer dump-autoload, cache temizlikleri) bilerek ölümcül değil: yalnızca uyarıyor, elle çalıştırılacak komutu yazdırıyor ve kapanış özetinde tekrar listeleniyor; böylece Node ya da Composer'ı olmayan bir makine bugünkü gibi kurulmaya devam ediyor. site:install değişikliği stubs/ üzerinden geliyor, dolayısıyla yalnızca yeni kurulumlara ve sk:update ile tazelenen uygulamalara ulaşıyor — mevcut, dokunulmamış bir consumer site:install kopyası değişmiyor.

Kurulum sırasında ulaşılamayan bir veritabanı eskiden yine Lvntr Starter Kit installed successfully! yazdırıyor ve 0 ile çıkıyordu — veritabanı bloğu (migration, seeder, izin tohumlama) yalnızca ekrandaki bir uyarıyla atlanıyordu. Bu koşu artık kurulumu eksik olarak bitiriyor: hiçbir stub-hash registry yazılmıyor, tamamlanan dosya-sistemi adımlarına ait resume checkpoint'i korunuyor ve komut sıfırdan farklı bir çıkış koduyla çıkıyor. Veritabanı bağlantısını düzeltip php artisan sk:install --resume çalıştırın; böylece baştan başlamak yerine tam kaldığı yerden devam eder.

migrate:fresh artık yazılı bir onay istiyor

sk:install, tabloları zaten dolu mevcut bir veritabanı bulduğunda "Tüm tabloları sil ve migration'ları sıfırdan çalıştır" seçeneğini de içeren bir select() menüsü sunuyordu; bu seçenek sıradan bir evet/hayır select() yanıtıyla onaylanıyordu — geri dönüşü olmayan bir migrate:fresh'ten yalnızca bir yanlış tuşa basma kadar uzaktaydı. Bu seçenek artık yazılı bir onay arkasına alındı: operatör silme işlemi çalışmadan önce bir text() prompt'unda veritabanı adını (ya da harfiyen fresh kelimesini) yazmak zorunda; bunun dışındaki her yanıt — boş bir cevap ya da refleks bir y dahil — hiçbir şey silinmeden ek türden migrate yoluna düşüyor. Yıkıcı seçenek ayrıca — prompt sebebini açıklıyor — APP_ENV production benzeri göründüğünde, APP_DEBUG kapalıyken, oturum hiç prompt gösteremediğinde (--no-interaction, CI, TTY yok) ya da mevcut bir tablo zaten satır içeriyorsa (okunamayan bir tablo veri içeriyor sayılır) baştan sunulmuyor. Yazılı onayın kendisi etrafında bir kaçış yolu yok; tek atlama yolu boş bir veritabanına karşı çalışmak ya da migrate'i kullanmak.

sk:install artık kurtarma yolu olarak belgelenmiyor

docs/install.tr.md ve docs/update.tr.md, mevcut bir projede php artisan sk:install komutunu yeniden çalıştırmayı idempotent bir proje-geneli kurtarma adımı olarak tarif ediyordu. Öyle değil ve bu tavsiye geri çekildi — deploy stratejinizde storage/starter-kit/ dizinini kalıcı operasyon durumu olarak ele alın, tıpkı storage/app/ gibi sürümler arasında yaşamalıdır.

Bu tavsiyenin geri çekilmesine yol açan risklerden ikisi artık açık değil, yukarıda ele alınıyor: eksik bir registry artık sk:install'ın kurulu bir uygulamayı sessizce ilk kurulum sayması sonucunu doğurmuyor (yukarıdaki "sk:install artık kendisinin kurmadığı bir uygulamada çalışmayı reddediyor" bölümüne bakın), mevcut bir .env'e sahip bir uygulamaya yapılan ilk kurulum da artık onun üzerine yazmıyor (yukarıdaki ".env artık asla üzerine yazılmıyor" bölümüne bakın). sk:install'i zaten kurulu bir uygulamaya dokunmak için yanlış araç yapan şey, yukarıdaki "Consumer tarafından değiştirilmiş bir published dosya varsayılan olarak atlanıyor" bölümünde anlatılan consumer-edit davranışıdır: --force verilmeden düzenlenmiş bir dosya tazelenmek yerine atlanıp raporlanıyor, --force ile ise doğrudan üzerine yazılıyor — ikisi de sk:update ya da kapsamı daraltılmış sk:publish --tag=<alan>'ın verdiği seçici, düzenlemeyi koruyan tazelemeyi vermiyor. Kurulu bir uygulamayı değiştirmek için bunları kullanın.

DATA_ENCRYPTION_CIPHER ile app.cipher eşleşmek zorunda — zorunlu kılındı

Okuma zincirinin tamamı (DATA_ENCRYPTION_KEY, DATA_ENCRYPTION_PREVIOUS_KEYS[n], APP_PREVIOUS_KEYS[n], APP_KEY) tek bir cipher ile kullanılır; dolayısıyla app.cipher değerinden farklı bir DATA_ENCRYPTION_CIPHER, anahtarı listede olsa bile diğer cipher ile yazılmış satırları okunamaz bırakır. DataEncrypterFactory::cipher() artık geç gelen kapalı bir DecryptException yerine her iki değeri de adlandıran bir RuntimeException fırlatıyor. Değişkeni kaldırın ya da app.cipher ile aynı değere ayarlayın.

Şifreli ayar veya 2FA verisi barındıran bir veritabanında app.cipher değerini değiştirmek tek yönlü bir sınırdır: önceki-anahtar zinciri anahtar başına cipher taşımadığı için eski payload'lar okunamaz hale gelir ve ne encryption:health ne de encryption:rekey onları kurtarabilir. Değiştirmek zorundaysanız önce eski cipher altında rekey yapın, encryption:health ile doğrulayın, yedek alın ve ancak sonra geçin — bunu bir config düzenlemesi değil, migration olarak ele alın.

Aktivite kaydı morph genişletme migration'ı — yalnız ileri düzeltme

2026_06_20_000000_widen_activity_log_morphs_to_string, UUID kullanıcılar ile bigint Role/Permission id'lerinin aynı tabloyu paylaşabilmesi için activity_log.subject_id / causer_id kolonlarını char(36) genişliğine çeker. down() metodu her iki kolonu uuid tipine geri döndürür. MariaDB 10.7+ üzerinde bu native UUID tipidir; sayısal subject/causer id barındıran bir tabloyu geri almak hata verir ya da veriyi budar. MySQL'de iki tip çakıştığı için risk testte kolayca gözden kaçar.

Dolu bir activity_log üzerinde bu migration'ı geri almayın. Yeni bir migration ile ileri düzeltin ya da yükseltme öncesi alınan yedekten dönün.

Aktivite kaydı kimlik bilgisi redaksiyonu — migration öncesi yedek alın

Yeni aktivite satırları artık hassas alanları kaydetmiyor; ancak mevcut activity_log satırları hâlâ parola hash'leri, token'lar veya secret'lar içerebilir. Yeni data-only migration bu anahtarları hem attribute_changes hem properties JSON kolonundan recursive olarak kaldırır.

Migration paketin içinde (database/migrations/, kitin diğer şema dosyaları gibi otomatik yüklenir) gelir; dolayısıyla yalnızca composer update ile taşınır — almak için sk:install / sk:update gerekmez. İlk php artisan migrate çalıştırmasında devreye girer; aşağıdaki yedek bu yüzden isteğe bağlı değildir.

Bu redaksiyon GERİ DÖNDÜRÜLEMEZ. php artisan migrate çalıştırmadan önce veritabanı yedeği almak ZORUNLUDUR. Silinen kimlik bilgisi materyali yeniden oluşturulamayacağı için migration'ın down() metodu bilinçli olarak no-op'tur.

Yedeği aldıktan sonra normal migration'ı çalıştırın:

php artisan migrate

Migration, hassas anahtar ön filtresini kullanmak yerine tüm satırları tarar: MySQL'de JSON kolonu büyük/küçük harfe duyarlı karşılaştırıldığı için farklı yazılmış bir anahtar (Password) aksi hâlde atlanırdı. Çok büyük bir activity_log tablosunda bu adım bir süre çalışır; tablo kilidi almaz ve her 500 satırlık sayfa için tek bir kısa transaction commit eder.

php artisan sk:doctor çıktısına activity-log-secrets kontrolü eklendi; böylece migration'ı hiç çalıştırmamış bir kurulum sessiz kalmak yerine FAIL raporlar. Bu kontrol ikinci bir tam geçiş değil, sınırlı ve salt-okunur bir sondadır: birincil anahtara göre ilk 500 satırı okur — PostgreSQL dahil her sürücüde aynı sabit maliyet — ve kararı PHP'de verir; böylece farklı yazılmış bir anahtar collation'dan bağımsız olarak yakalanır. Bu pencerede durduğu için, büyük tablolarda bulgu bir alt sınır ("en az N") olarak raporlanır ve temiz sonuç taradığı pencereyi adlandırır. Tam sayıma ihtiyacınız olduğunda php artisan sk:redact-activity-secrets --dry-run --all kullanın. --all bayrağı önemlidir: onsuz komut MySQL, MariaDB ve SQLite'ta SQL tarafında bir anahtar-adı ön filtresine düşer ve farklı yazılmış bir anahtar bu filtreden kaçabilir.

Alttaki komut idempotent'tir; eski bir yedeği geri yükledikten sonra da ayrıca çalıştırılabilir:

php artisan sk:redact-activity-secrets --dry-run
php artisan sk:redact-activity-secrets
php artisan sk:redact-activity-secrets --chunk=500
php artisan sk:redact-activity-secrets --all
  • --dry-run yazma yapmadan değişecek satırları raporlar.
  • --chunk= her turda işlenen satır sayısını belirler (varsayılan 500, en fazla 5000).
  • --all, hassas anahtar ön filtresini kullanmak yerine tüm satırları tarar.

Komut bir JSON payload'ının decode edilemediğini bildirirse o payload değiştirilmeden bırakılır ve hâlâ kimlik bilgisi içerebilir. Yükseltmeyi tamamlanmış saymadan önce bildirilen her satırı elle inceleyip redact edin.

FileManager context ability'leri — BREAKING

Consumer tarafından kaydedilen FileManager context closure'ları artık yalnızca read, create, update veya delete değerlerinden birini alır. Kit artık hiçbir zaman write göndermez.

Dokümante edilmiş okuma-mutasyon ayrımı biçimini kullanan closure güvenli kalır:

'authorize' => fn (Model $actor, string $ability, Model $owner): bool =>
    $ability === 'read' ? $readCheck : $writeCheck,

Ancak ters legacy biçim tehlikelidir:

'authorize' => fn (Model $actor, string $ability, Model $owner): bool =>
    $ability === 'write' ? $writeCheck : $readCheck,

write artık gönderilmediği için her mutasyon bu closure'ın okuma dalına düşer. $readCheck, $writeCheck'ten daha genişse create, update ve delete istekleri sessizce fazla yetkilendirilebilir.

Consumer tarafından kaydedilen her closure'ı dört ability adını da açıkça eşleyecek biçimde yeniden yazın:

'authorize' => fn (Model $actor, string $ability, Model $owner): bool => match ($ability) {
    'read' => $readCheck,
    'create' => $createCheck,
    'update' => $updateCheck,
    'delete' => $deleteCheck,
    default => false,
},

Built-in global context artık bu ability'leri birebir files.read, files.create, files.update ve files.delete ile eşler. Bu nedenle yalnız files.create sahibi bir rol artık silme veya çöpü boşaltma erişimine, yalnız files.update sahibi bir rol ise okuma erişimine sahip değildir. Her role ihtiyaç duyduğu belirli files.* yetkilerini verin, ardından seed edilmiş yetkileri yeniden oluşturun:

php artisan sk:seed-permissions

Çözülemeyen route'larda fail-closed, mevcut kurulum için opt-in

Bu sürümde hiçbir şey kırılmıyor. CheckResourcePermission tarafından izni çözülemeyen bir route bugün hâlâ geçer — tıpkı önceki gibi; middleware artık ayrıca route'u adlandıran, throttle edilmiş bir uyarı logu basar; böylece boşluk sessiz kalmak yerine görünür olur. Şu anda başarılı olan hiçbir istek bu sürüm yüzünden başarısız olmaya başlamaz.

Kitin kendi route'ları paketin içinde (src/) düzeltildi: kitin gönderdiği her route artık kendi başına bir izne çözülüyor. Mevcut bir kurulum bu düzeltmeyi yalnızca composer update ile alır — route dosyası düzenlemesi yok, sk:update reconciliation'ı yok. Gerekçe, route'ların pakette yaşaması değil: route'lar stubs/routes/web/*-route.php içinde kayıtlı ve sk:install onları app'inize kopyaladı. src/ içinde yaşayan şey sözleşme: CheckResourcePermission içindeki, o dosyaların zaten kullandığı adlarla anahtarlanmış bir route-adı → izin haritası. Düzeltme bu yüzden düzenlemiş olabileceğiniz bir dosyaya dokunmadan geliyor.

Madalyonun diğer yüzü de bilinmeli: kitin route'larından birini kendi kopyanızda yeniden adlandırdıysanız harita artık onu tutmuyor; o route uyarıyla geçmeye geri döner ve bayrak çevrildikten sonra reddedilir. sk:doctor --only=unresolved-routes tam olarak bunları listeler.

Bir durum middleware katmanında bilinçli olarak kapatılmadı: roles.bulk ve users.bulk. Bu uçların gerektirdiği ability, route'un değil istek gövdesinde adı geçen aksiyonun bir özelliği; BulkActionDispatcher zaten her item'ı handler'ın kendi ability'siyle yetkilendiriyor (BulkDeleteUserAction users.delete istiyor). Route seviyesinde tek bir eşleme yalnızca fazla-reddedebilirdi — .delete, .update ve .read'in her biri farklı bir meşru rolü kırar, çünkü bu ability'ler permission-resources.php içinde birbirinden bağımsız. Bu yüzden paketin muaf listesine yazıldılar; bu aynı zamanda onları çözülemeyen ekseninden çıkarıyor, böylece bayrak çevrildiğinde bulk aksiyonları kırılamaz. Item bazlı yetkilendirme değişmedi ve asıl kapı olmayı sürdürüyor.

Varsayılan değişmeden önce izlenecek sıralı düzeltme yolu:

  1. php artisan sk:doctor --only=unresolved-routes çalıştırarak kendi app'inizde hâlâ uyarıyla geçen her route'u listeleyin.
  2. Listelenen her route'u şu yollardan biriyle düzeltin:
    • Action segmenti middleware'in ability haritasında olan bir <resource>.<action> route adı verin; böylece izin otomatik çözülür.
    • Açık bir izin argümanıyla gate edin, örn. check.permission:reports.read.
    • Route bilinçli olarak izinsiz kalacaksa (public bir webhook, health check, …) starter-kit.permissions.unrestricted_routes altında tanımlayın (dar Str::is desenleri — geniş bir desen sonradan eklenen route'ları da sessizce muaf tuttuğu için, ağaç yerine tek tek endpoint listelemeyi tercih edin).
  3. 1-2. adımlar bittiğinde staging ortamında STARTER_KIT_ALLOW_UNRESOLVED_ROUTES=false (ya da starter-kit.permissions.allow_unresolved = false) ayarlayıp güvendiğiniz hiçbir şeyin reddedilmediğini doğrulayın. Staging temiz çıktığında aynı değeri production'da da verin. Opt-in'in tamamı bu satır — başka hiçbir şeyin değişmesi gerekmiyor ve bunu sizin yerinize kimse yapmayacak.

Kitin config'ini publish ettiyseniz (php artisan sk:publish --tag=config), config/starter-kit.php kopyanız iki yeni anahtardan da önce oluşmuştur ve mergeConfigFrom yalnızca en üst seviyede birleştirir — paketin permissions dizisi sizinkinin içindeki boşlukları doldurmaz. Bir şey kırılmaz: allow_unresolved kod tarafında paket varsayılanına düşer, olmayan unrestricted_routes ise boş liste olarak okunur. Ancak 2. adımdaki üçüncü seçenek, publish edilmiş permissions dizinize 'unrestricted_routes' => [...] anahtarını kendiniz eklemeden hiçbir işe yaramaz. İki anahtarı da almak için kopyanızı vendor/lvntr/laravel-starter-kit/config/starter-kit.php ile karşılaştırın.

Hiçbir sürüm bunu sizin yerinize çevirmeyecek. STARTER_KIT_ALLOW_UNRESOLVED_ROUTES (config starter-kit.permissions.allow_unresolved), değeri kendisi vermeyen bir uygulama için varsayılan olarak true'dur ve bu varsayılan 13.x'in hiçbir yerinde değişmiyor. Uygulamanızın kendiliğinden reddetmeye başlayacağı planlı bir sürüm yok; bunu açan tek şey 3. adım ve zamanlamasına siz karar veriyorsunuz.

Öylece değiştirilmemesinin gerekçesi, değişimin erişim alanı. Config'i hiç publish etmemiş bir kurulum da, publish edilmiş kopyası bu anahtardan önce oluşmuş bir kurulum da paketin kendi sınıf sabitine düşer; o sabiti çevirmek, hiç kimse bir dosyaya dokunmadan, yalnızca composer update çalıştıran her uygulamada yetkilendirme davranışını değiştirirdi. Bu erişime sahip bir varsayılan, bir sürüm hattının içinde güvenle değiştirilebilecek bir varsayılan değildir; ileride tekrar ele alınırsa yeri kendi upgrade notuyla birlikte bir major sürümdür.

Yepyeni bir proje farklı ve zaten sıkı. sk:install, oluşturduğu .env dosyasına STARTER_KIT_ALLOW_UNRESOLVED_ROUTES=false yazıyor: sıfırdan kurulan bir uygulamada geçmişten devralınacak route yok, dolayısıyla fail-closed başlıyor ve ilk izinsiz route'u production'da değil geliştirme sırasında yakalanıyor. Bu yalnızca ilk kurulum için geçerli — mevcut bir uygulamada sk:install'ı yeniden çalıştırmak bu anahtarı eklemez; sk:update ve sk:upgrade da eklemez.

Değeri hangi yöne verirseniz verin, env değişkeni production'da geçerli kaçış kapısı olarak kalır — düzeltmeyi bitirmek için daha fazla zamana ihtiyacınız varsa true'ya geri alabilirsiniz; ancak çözülememiş her route, tanımı gereği, o hâlde kaldığı sürece izinsiz (ungated) demektir.

Sayfalar-arası toplu seçim artık desteklenmeyen filtrelerde fail-closed davranıyor

Bir sayfa, kit'in Users veya Roles tablosuna kendi filtresini ekliyorsa — datatable'ın tanımlamadığı özel bir filter[...] anahtarı — artık "tümünü seç"e tıklandığında bu filtre aktifken toplu işlem sessizce o filtreyi yok sayan bir küme üzerinde çalışmak yerine 422 (sk-bulk.unknown_filters) döner. Bu sürümden önce desteklenmeyen aktif bir filtre snapshot'tan düşürülüyordu ve çözülen küme tablonun gösterdiğinden daha geniş oluyordu — filtrenin gizlemesi gereken satırları da siliyor ya da onlar üzerinde işlem yapıyordu.

Boş bir değer de aktiftir. filter[status]= (boş ya da yalnızca boşluktan oluşan bir string) tablo tarafında WHERE status = '' olarak uygulanır — boş bir küme — bu yüzden toplu seçim tarafı artık onu "filtre yok" saymak yerine olduğu gibi geçiriyor: desteklenen bir anahtarda tablonun gösterdiği aynı (boş) kümeyi çözer, desteklenmeyen bir anahtarda ise aynı 422 ile reddedilir. Yalnızca null değer ya da boş dizi yok sayılır; bu, Spatie'nin AllowedFilter'ının atladığı şekillerle birebir aynıdır. Gönderilen SkDatatable URL'ye asla boş bir filtre yazmaz, dolayısıyla stok Users/Roles sayfaları etkilenmez; kendi filter_snapshot'ını üreten bir sayfa boş anahtarları göndermek yerine düşürmelidir.

422'yi, desteklenmeyen filtreyi backend'e ulaşmadan snapshot'tan çıkararak "düzeltmeyin" — bu, kümeyi eski, güvensiz davranışa geri genişletir. Bunun yerine query sınıfının gerçekten uyguladığı allow-list'i genişletin (UserBulkSelectionQuery::ALLOWED_FILTERS / Roles karşılığı) ki yeni filtre tablonun kullandığı aynı semantikle uygulansın, ya da o filtre aktifken sayfalar-arası seçimi devre dışı bırakıp satır bazlı seçime geri dönün.

Bu düzeltme vendor query sınıflarında yaşıyor (Lvntr\StarterKit\Domain\User\Queries\UserBulkSelectionQuery, Lvntr\StarterKit\Domain\Role\Queries\RoleBulkSelectionQuery). Bu sorgulardan birinin vendor namespace'i dışına çıkarılmış bir kopyası — make:sk-domain ile ya da elle — composer update ile bu düzeltmeyi almaz; kopyanızı vendor kaynağıyla yeniden diff'leyin. Aynı şekilde, php artisan sk:publish --tag=composables ile publish edilmiş bir useDatatableSelection.ts kopyası, siz yeniden publish edene ya da aşağıda anlatılan değişikliği elle taşıyana kadar eski gönderdiği id şeklini göndermeye devam eder.

BulkActionRequest sayfalar-arası modda artık ids istemiyor

app/Http/Requests/Admin/BulkActionRequest.php, select_all_filtered true olsa bile ids (min:1) istiyordu; bu, belgelenmiş payload'la (ids o durumda boş gelir) çelişiyordu — useDatatableSelection().executeBulkAction()'ı mevcut sayfada hiçbir şey seçili değilken doğrudan çağıran bir host 422 (sk-bulk.ids_required) alıyordu. Kural artık Rule::requiredIf(! select_all_filtered); gönderilen id'ler yine doğrulanır (array, max:500, opak string) ve sayfalar-arası küme seçim sorgusunun MAX_ITEMS sınırıyla bağlı kalır. Bu publish edilen bir stub'dır: değiştirilmemiş bir kopya php artisan sk:update ile yenilenir; düzenlediğiniz bir kopya siz değişikliği taşıyana kadar eski kuralı korur.

Toplu seçim id'leri opak string olarak gönderiliyor, sayısal dönüşüm yok

useDatatableSelection()'ın executeBulkAction()'ı artık seçili satır id'lerini göndermeden önce dönüştürmüyor. Backend ids.* doğrulaması zaten string|min:1|max:64 kabul ediyordu — yani UUID/ULID birincil anahtarlar zaten geçerliydi — ama sayısal görünümlü bir id daha önce bir dönüşüm adımından geçebiliyordu. Kendi toplu işlem endpoint'iniz ids'i katı bir integer cast ile ayrıştırıyorsa, idKey kolonunuzun kullandığı tam string tipini hâlâ kabul ettiğinden emin olun.

DatatableQueryBuilder::columns() payload şekillendirmesi fail-closed

columns() tanımlayan ve hiçbir tanımlı sütunla eşleşmeyen bir ?columns= istek parametresi alan bir backend artık her satırı yalnızca alwaysInclude() anahtarlarına indirger — artık tam satıra geri dönmez. columns parametresinin hiç bulunmaması bu davranıştan etkilenmez ve tam satır dönmeye devam eder. Frontend sütun anahtarı ile karşılık gelen backend columns() anahtarı bir tarafta yeniden adlandırılıp diğerinde kalmışsa, etkilenen hücreler artık tam payload'ın uyumsuzluğu maskelemesi yerine boş render edilir; güncelleme sonrası eksik hücre verisi görürseniz her iki tarafı da denetleyin.

definitions.lang daraltılıyor — migration reddedebilir

create_definitions_table, üç varsayılan string() (255 karakter, utf8mb4) sütun üzerinde unique(['key', 'value', 'lang']) tanımlıyordu — MySQL/MariaDB'nin 3072 byte'lık InnoDB anahtar sınırının 3060'ında, herhangi bir sütunun tek bir karakter genişlemesi bu sınırı kırardı. Yeni bir migration yalnızca lang'i 35 karaktere daraltıyor — 35, kitin herhangi bir yerde kabul ettiği en geniş locale değeri olan content_languages.code'dan alındı; böylece kitin kendi ekranlarından saklayabileceğiniz her etiket sığmaya devam ediyor ve aşağıdaki ret bu değerler için erişilemez kalıyor. key ve value yayımlanmış 255 genişliğini koruyor: tek başına lang indeksi 2180 byte'a indiriyor, yani sınırın ~892 byte altına; onları da daraltmak yalnızca mevcut şemanın kabul ettiği veriyi bloke ederdi.

Şemaya dokunmadan önce migration, mevcut her satırı ölçer (lang karakter uzunluğu, soft-delete edilenler dahil — bunlar hâlâ unique indeksi işgal eder) ve herhangi bir satır yeni sınırda karakter kaybedecekse — şemayı değiştirmeden — reddeder. Verinizde reddederse:

  1. Hatayı okuyun — sütunu, sınırı aşan satır sayısını ve bulunan en uzun değeri adlandırır.
  2. Sorunlu definitions satırlarını kısaltın veya silin (soft-delete edilenler dahil — deleted_at, bir satırı unique indeksten muaf tutmaz).
  3. php artisan migrate'i yeniden çalıştırın.

Migration doğrudan geri alınabilir (down() lang'i 255'e geri genişletir — bir genişletme asla kırpmaz, dolayısıyla ölçüme gerek duymaz). Her iki yön de unique indeksin var olduğunu doğrulayarak biter; böylece indeksi zaten eksik hâlde bu migration'a ulaşan bir tablo (yarıda kalmış önceki bir koşu) garantisi olmadan "migrate edildi" diye kaydedilmek yerine indeksi yeniden kurulur. Kit'in kendi ~34 tohumlanmış satırının çok ötesine büyümüş bir tabloda ALTER TABLE + indeks yeniden kurma işlemi süresince bir metadata kilidi tutar; büyük bir tablodaki başka herhangi bir ALTER kadar dikkatle planlayın.

media tablosu migration'ı artık bir rollback yoluna sahip — yıkmak yerine reddeden bir yol

create_media_table'ın bir down()'ı yoktu. Laravel'in migrator'ı bu çağrıyı method_exists ile koruduğu için php artisan migrate:rollback hata vermiyordu — tabloyu sessizce atlıyor, ama migration'ın kayıt defteri satırını yine de siliyordu; geriye uygulamanın artık kaydını tutmadığı bir media tablosu ve o tabloda patlayan bir yeniden migrate kalıyordu. Artık bir down() tanımlıyor: tablo boşsa düşürülüyor, içinde satır varken denenen bir rollback ise tabloyu adıyla anan bir hatayla duruyor. Aynı zincirdeki iki sonraki migration (add_folder_id_to_media_table, add_soft_deletes_to_media_table) da birebir aynı reddi taşıyor; çünkü bir batch en yeniden en eskiye doğru geri alınır: bu olmadan, create migration'ının koruması hiç devreye girmeden dolu bir tablodan folder_id ve deleted_at düşürülmüş olurdu.

Bu, mevcut tüketiciler için bir davranış değişikliği: bu migration'ın ait olduğu batch'i kapsayan bir migrate:rollback daha önce media'yı sessizce atlıyordu; media satırı olan bir kurulumda artık hata veriyor. Bu hata özelliğin kendisi. Dolu bir media'yı düşürmek satırları kaldırırdı, dosyaları değil: her satır yapılandırılmış bir disk üzerindeki bir blob'a işaret eder ve Spatie bu blob'u yalnızca modelin deleting event'i üzerinden siler — bir şema rollback'i Eloquent'i tamamen atlar, yani storage dizinleri bozulmadan kalırken onların tek indeksi yok edilir ve uygulamanın artık numaralandıramayacağı öksüz dosyalar geriye kalırdı.

Migration'ı bilerek geri almak için önce media'yı uygulama üzerinden silin — böylece blob'lar satırlarla birlikte gider — sonra rollback'i tekrar çalıştırın. Korumayı aşmak için tabloyu ham SQL ile boşaltmayın; bu, korumanın engellemek için var olduğu öksüzleşmeyi bire bir üretir.

Dosya yüklemeleri artık bir client-uzantı allow-list'i uyguluyor

FileManager ve avatar yüklemeleri artık yalnızca sniff edilen content-type eşleşmesiyle geçmiyor — client dosya adının uzantısı da kontrol ediliyor ve media-library.disallowed_extensions artık html, htm, xhtml, xht, svg, svgz, xml, xsl, xslt, js, mjs ve hta'yı adın yalnızca son değil her nokta segmentinde engelliyor (bu yüzden name.html.pdf, .pdf son uzantı olsa bile reddediliyor). Nedeni için CHANGELOG.tr.md'ye bakın. Burada hiçbir şey veritabanına veya .env'e dokunmuyor; iki şey bir operatörün dikkatini gerektiriyor.

Avatar request stub'ı consumer-sahipli. stubs/app/Http/Requests/UploadAvatarRequest.php, sk:install tarafından uygulamanıza kopyalandı ve kit zaten size teslim ettiği bir dosyaya geri erişemez. Mevcut bir kurulum, siz ya tazelenmiş stub'ı almak için php artisan sk:update çalıştırana ya da kendi kopyanızın rules() dizisine — mevcut 'mimes:jpg,jpeg,png,webp' satırının hemen ardına — 'extensions:jpg,jpeg,png,webp''i elle ekleyene kadar eski kurallarını (image, mimes:jpg,jpeg,png,webp, uzantı kontrolü yok) koruyor.

media-library.disallowed_extensions sertleştirmesi koşulsuz uygulanıyor — kapatacak bir bayrak yok. Kendi kopyanızı publish ettiğiniz an geri çekilen kitin media-library.php/activitylog.php override'larının aksine, bu merge her boot'ta koşulsuz çalışıyor. Uygulamanız yeni engellenen uzantılardan birini gerçekten kabul etmesi gerekiyorsa, kabul ettiğiniz MIME/uzantı çiftini bildirmek için alt sınıflanmış bir request'te mimeExtensionMap()'i override edin ve o uzantıyı kendi service provider'ınızda (kitinkinden sonra register edilmiş) media-library.disallowed_extensions'tan kendiniz çıkarın — bunu yapmanın bu sertleştirmenin kapattığı aktif-içerik riskini yeniden açtığını bilerek.

Rename uzantıyı korur; boyut tavanı hizalandı. PATCH files/{media} artık uzantısı saklanan dosyanınkinden farklı olan ya da yasaklı bir segment içeren (report.php.pdf) yeni adı 422 ile reddediyor. Kitin mimeExtensionMap()'i dışındaki kabul edilen MIME tipleri (PPTX, RAR, Markdown, …) uzantılarını Symfony'nin MIME veritabanından çözüyor; yukarıdaki alt sınıf override'ı yalnız ikisinin de tanımadığı bir MIME için gerekiyor. Media library'nin kendi media-library.max_file_size tavanı (varsayılan 10 MB) FileManager'ın max_size_mb ayarından bağımsızdır: bu tavanı aşan bir yükleme artık 500 yerine 422 ("The uploaded file is too large.") ile reddediliyor ve yenilenen app/Providers/SettingsServiceProvider.php stub'ı max_file_size'ı max_size_mb'den set ediyor — php artisan sk:update ile çekin ya da kendi kopyanızdaki mevcut file-manager.settings.max_size_mb yazımının yanına config(['media-library.max_file_size' => $maxSizeMb * 1024 * 1024]) ekleyin.

Segment bazlı engelleme (yukarıdaki name.html.pdf durumu) spatie/laravel-medialibrary 11.23.0 (2026-05-28) ile geldi; kitin composer.json'ı artık ^11.23 istiyor, dolayısıyla bu sürümü getiren composer update listeyi uygulayan bir build'i de çeker. Eski bir build'in hâlâ kurulu olduğu aralığı kitin kendi request seviyesindeki segment kontrolü kapatır. Şüphede kalırsanız composer show spatie/laravel-medialibrary ile kontrol edin.

Ayarlar cache anahtarı değişti — SettingService'i extend etmediyseniz işlem gerekmiyor

SettingService artık settings altında çözülmüş değerleri cache'lemek yerine settings:v2 altında ham (ciphertext) satırları cache'liyor; nedeni için CHANGELOG.tr.md'ye bakın. Herhangi bir deploy adımı gerekmiyor: eski settings anahtarı, yeni snapshot ilk kurulduğunda otomatik olarak düşürülüyor. Beklemek yerine kalıcı bir düz-metin snapshot'ı hemen düşürmek isterseniz php artisan cache:forget settings'i elle çalıştırmak zararsızdır.

Eski cache davranışını aşmak için SettingService'i extend ettiyseniz, o subclass'ı kaldırın. Bu sürümden önce write path'ler cache snapshot'ını düşürmüyordu ve bazı kurulumlar bunu, cache anahtarını yeniden tanımlayan kendi alt sınıflarıyla yamamıştı — tipik olarak private const CACHE_KEY = 'settings';. SettingService::CACHE_KEY artık public bir sabit ve PHP bir alt sınıfın sabit görünürlüğünü daraltmasına izin vermiyor; dolayısıyla böyle bir sınıf composer update sırasında autoload aşamasında fatal veriyor:

Access level to App\...\YourSettingService::CACHE_KEY must be public
(as in class Lvntr\StarterKit\Domain\Setting\SettingService)

Subclass zaten gereksiz: artık her write path DB::afterCommit() üzerinden Cache::forget() çağırıyor, yani snapshot en dıştaki commit'te düşüyor. Alt sınıfı silin, container binding'inizi (ve varsa class_alias'ı) Lvntr\StarterKit\Domain\Setting\SettingService'e geri döndürün, ardından composer dump-autoload && php artisan optimize:clear çalıştırın. Görünürlüğü düzeltip subclass'ı korumak da seçenek değil — 'settings' anahtarı düz-metin cache'leyen v1 davranışını geri getirir. SettingService'i hiç extend etmemiş bir tüketici etkilenmez; kit stubs/ altında böyle bir alt sınıf göndermiyor.

POST /api/v1/auth/login artık kendi hesap-başına rate limitine sahip

stubs/routes/api/public-api.php'deki login rotası throttle:5,1'den yeni bir isimli limiter'a taşındı: api-login (paketin kendisinde, Lvntr\StarterKit\StarterKitServiceProvider'de kayıtlı): Limit::perMinute(5)->by('ip:...') artı Limit::perMinute(3)->by('email:...'). Bu sürümden önce endpoint'in hiçbir hesap-başına tavanı yoktu — bir saldırgan tek bir hesap için denemelerini birçok IP'ye yayarak hiçbir şeye takılmıyordu. register ve two-factor-challenge değişmedi, hâlâ throttle:5,1: register'ın hesap var olmadan önce üzerine anahtarlanacak bir hesap kimliği yok, ve 2FA challenge tek kullanımlık ve kısa ömürlü sunucu-tarafından-verilen bir challenge id'sine anahtarlanmış durumda, dolayısıyla her ikisinde de saldırganın oynayabileceği tek eksen IP tavanı.

Bir eksen bilinçli olarak gevşedi. İsimsiz bir throttle:5,1 yalnızca alan adı ve IP üzerine anahtarlanır — rota bileşeni yoktur — dolayısıyla login, register ve two-factor-challenge daha önce IP başına tek bir 5/dakika kovasını paylaşıyordu. login artık kendi ad-alanlı kovasına sahip, yani üç endpoint birlikte IP başına dakikada 5 yerine 10 isteğe izin veriyor. Login rotasının kendisi IP başına 5/dakikada değişmedi (ve artık ek olarak e-posta başına 3/dakika ile sınırlı); /api/v1 grubunun tamamı hâlâ kendi throttle:api 60/dakika sınırına tabi.

api-login, web login limiter'ının yeniden kullanımı değil, kasıtlı olarak ayrı bir limiter: auth.login_throttle ayarı yalnızca Fortify'ın kendi rota kaydının okuduğu config değerini yeniden yazıyor, dolayısıyla hiçbir admin toggle'ı API'nin tavanını gevşetemez, ve API web limiter'ının daha gevşek 10/dakika-IP-başına sınırını miras almıyor.

Limiter'ın kendisi elle taşınmayı gerektirmiyor. Kaydı paketin içinde, Lvntr\StarterKit\StarterKitServiceProvider::configureRateLimiting()'de yapılıyor; publish edilen FortifyServiceProvider'da değil — bilinçli olarak. sk:update publish edilmiş dosyaları birbirinden bağımsız yeniler; dolayısıyla provider'ını özelleştirmiş (hash uyuşmadığı için korunur) ama rota dosyasına dokunmamış (yenilenir) bir kurulumda, rota hiç kaydedilmemiş bir limiter'ı adıyla çağırır duruma düşerdi — Laravel buna MissingRateLimiterException ile yanıt verir, yani her API login isteği 500 döner. Limiter'ı vendor katmanından dağıtmak bu arıza biçimini ortadan kaldırıyor. Kendi api-login limiter'ınızı zaten eklediyseniz o kazanmaya devam eder: sizin provider'ınız paketinkinden sonra boot olur.

Elle taşınması gerekebilecek tek şey var. routes/api/public-api.php publish edilmiş bir stub'dır ve dağıtılan hash ile eşleşmeyen diskteki bir kopya sk:update tarafından üzerine yazılmaz, atlanır. Bu dosyayı özelleştirdiyseniz login rotasının middleware'ini throttle:5,1'den throttle:api-login'e kendiniz değiştirin. Değiştirene kadar endpoint eski IP-yalnızca tavanını korur ve hesap-başına bir tavan kazanmaz — hata vermez, sadece olduğu gibi kalır.

Yeni starter-kit.security.csp_nonce bayrağı — opt-in, kör açılırsa publish edilmemiş bir Blade'i kırar

Yeni bir config anahtarı, starter-kit.security.csp_nonce (env STARTER_KIT_CSP_NONCE), SecurityHeaders middleware'inin script-src direktifini 'unsafe-inline''dan yanıt render edilmeden önce Vite::useCspNonce() ile üretilen istek-başına bir 'nonce-<random>''a çeviriyor. Nonce mevcut olduğunda tarayıcı 'unsafe-inline''ı tamamen görmezden gelmeye başlıyor — bu, bayrağın gerçek bir açığı kapatmasını sağlayan şey (enjekte edilmiş bir inline <script> artık çalışmıyor) ama aynı zamanda dikkatsizce açmayı tehlikeli kılan şey de bu.

Kasıtlı olarak false varsayılıyor. Kit tam olarak bir inline script gönderiyor — resources/views/app.blade.php'deki FOUC-önleyici tema script'i — ve bu script nonce="{{ Vite::cspNonce() }}" özniteliğini yalnız yeni publish edilmiş dosya sürümünde taşıyor. app.blade.php'si zaten publish edilmiş ve bu özniteliği barındırmayan bir uygulama, bayrak açıldığı an bu script'i sessizce kaybediyor: hata yok, konsolda göze çarpan bir engellenmiş istek yok, sadece her sayfa yüklemesinde yanlış temada açılan ve gözle görülür şekilde flaşlayan bir panel.

Kimin hangi değeri aldığı: sk:install, STARTER_KIT_CSP_NONCE=true'yu yalnız ilk kurulumda .env'e ekliyor (FIRST_INSTALL_ONLY_ENV_KEYS), çünkü yepyeni bir projenin publish edilmiş Blade'i özniteliği zaten taşıyor. sk:install'i mevcut bir uygulamada yeniden çalıştırmak anahtarı eklemiyor, ve sk:update / sk:upgrade .env'e hiç yazmıyor — dolayısıyla mevcut hiçbir kurulum bir güncellemeyle çevrilmiyor. Hiçbir şey ayarlamayan bir uygulama false'ta kalıyor.

Mevcut bir kurulumda açmak için, sırayı şu şekilde takip edin — ters çevrilirse uygulama iki adım arasında kırılır:

  1. Publish edilmiş resources/views/app.blade.php'nizdeki inline <script> etiketine nonce="{{ Vite::cspNonce() }}" ekleyin (hangi etiket olduğundan emin değilseniz vendor/lvntr/laravel-starter-kit'in stub'ıyla diff alın).
  2. STARTER_KIT_CSP_NONCE=true'yu ayarlayın (config publish ettiyseniz starter-kit.security.csp_nonce => true).

Bayrak kapalıyken o script etiketindeki nonce="..." özniteliği boş render edilir ve etkisizdir — nonce kaynağı olmayan bir CSP hâlâ 'unsafe-inline''ı onaylıyor — dolayısıyla özniteliği önceden eklemek her zaman güvenlidir. style-src 'unsafe-inline' her iki durumda da etkilenmiyor; PrimeVue çalışma zamanında inline stiller yazıyor ve nonce'lanamıyor.

API dökümantasyon yüzeyi api-dock'a taşındı — eski /docs/api URL'i gitti

Etkilenen: her kurulum; kitin kendi API dökümantasyon okuma yüzeyi değişti. Etkilenmeyen: JSON API'nin kendisi (/api/v1/...) — yalnızca OpenAPI belgesini okumaya gittiğiniz yer değişti.

Kit artık yeni bir pakete bağımlı: lvntr/api-dock — ve 13.7.0'dan itibaren dökümantasyon yüzeyi Scramble'ın kendi paketlenmiş arayüzü değil, bu. api-dock, kitin route attribute'larından zaten ürettiği aynı default Scramble API'sini belgeliyor; bir controller veya route'u nasıl annotate ettiğiniz konusunda hiçbir şey değişmiyor. Değişen şey, onu okumaya nereye gittiğiniz ve kimin erişebileceği: Scramble'ın varsayılan rotaları artık bilerek devre dışı bırakıldı (Scramble::ignoreDefaultRoutes()), yani /docs/api ve /docs/api.json güncelleyen her uygulamada 404 dönüyor — yönlendirme yok, geçiş süresi yok. Panel artık /api-dock adresinde yaşıyor (ham OpenAPI belgesi /api-dock/spec'te), ve eski Scramble rotasının aksine, seed edilen api-docs.read izninin arkasında korunuyor — API Routes ekranının zaten kullandığı aynı izin. Eski URL'i yer imlerine eklediyseniz, dahili dökümantasyondan link verdiyseniz veya bir CI/tooling adımını ona işaret ettiyseniz, /api-dock'a güncelleyin.

Eski yüzey, ancak yenisi gerçekten kuruluysa emekliye ayrılıyor. Scramble::ignoreDefaultRoutes() çağrısı class_exists(Lvntr\ApiDock\ApiDockServiceProvider::class) ile korunuyor; yani bu sürümün kaynağına sahip olup yeni Composer bağımlılığını çözmemiş bir uygulama — dev-main izleyen bir path veya VCS repository'si, eski bir lock'tan geri yüklenmiş bir vendor/ dizini, api-dock'u dışarıda bırakan bir kısıt — iki yüzeyi birden kaybetmek yerine /docs/api'yi çalışır halde tutuyor. Bu pencerenin belirtisi tanınabilir: /docs/api hâlâ cevap veriyor, /api-dock 404 dönüyor ve API Routes ekranında panel butonu görünmüyor (buton yalnız api-dock.docs rotası kayıtlıysa render ediliyor). Çözüm tek bir composer update lvntr/laravel-starter-kit — lvntr/api-dock'u da beraberinde kurar. php artisan sk:update bunu yapamaz: yalnız scaffold dosyalarını kopyalar, hiçbir zaman bir Composer paketi kurmaz.

Kontrol edilecekler:

  • sk:update, config/api-dock.php'yi ve panelin derlenmiş asset'lerini (--tag=api-dock-assets) başka bir config/asset tag'i yayınladığı gibi yayınlıyor — normal bir sk:update'te elle çalıştırılacak ekstra bir şey yok.
  • Yetkilendirme bu publish adımını beklemiyor. api-dock'un kendi paket varsayılanları paneli, /api-dock/spec belgesini ve try-it proxy'sini yalnız ['web'] arkasında sunuyor; bu yüzden composer update çalıştırıp orada durursanız kit api-dock'u anonim bırakmak yerine tamamen kapatıyor. Uygulamanızda App\Http\Middleware\CheckApiDocsAccess var olduğu anda kit ['web', 'auth', CheckApiDocsAccess::class] yığınını kendisi kuruyor. Her iki davranış da yalnızca api-dock.middleware hâlâ dokunulmamış ['web'] varsayılanındayken geçerli — config'i publish edip kendi middleware yığınınızı yazarsanız kit tamamen devreden çıkıyor.
  • Daha önce php artisan vendor:publish --tag=scramble-config çalıştırdıysanız ve kendi config/scramble.php'niz commit'liyse, bunu açıp Scramble'ın varsayılan docs rotasını kendi başına yeniden kaydetmediğinden emin olun (örn. Scramble::routes() çağırarak veya ignoreDefaultRoutes()'u temizleyerek) — bu değişiklikten önce publish edilmiş bir config bunu kendi başına yapamaz, ama elle düzenlenmiş biri yapabilir ve api-dock ile aynı belge için rekabet eder.
  • Dört yeni api-dock:* Artisan komutu (export, diff, sync, agent-guide) ve AI odaklı llms.txt/MCP export'ları opt-in araçlardır, yükseltme yolunun parçası değildir — her birinin ne yaptığı için bkz. docs/api.tr.md ve config/api-dock.php.

Daha önce evi olmayan sabit ops notları

Daha önceki sürümlerde zaten gönderilmiş ama belgelenmiş bir yeri olmayan birkaç davranış var; bundan sonra bulunabilir olması için burada not ediyoruz:

  • STARTER_KIT_ALLOW_UNRESOLVED_ROUTES=false (config starter-kit.permissions.allow_unresolved), yukarıda "Çözümlenmemiş-rota fail-closed'ı mevcut bir kurulum için opt-in" altında anlatılan CheckResourcePermission için fail-closed ayarıdır. Mevcut bir kurulum için önce php artisan sk:doctor --only=unresolved-routes çalıştırıp hâlâ gerçek bir izin yerine bir uyarıyla çözülen her rotayı listeleyin, her birini düzeltin, sonra bayrağı çevirin. Yeni bir kurulum zaten false olarak gelir.
  • Passport scope zorlaması opt-in'dir. config/starter-kit.php'nin passport.scopes / passport.default_scopes'u bir katalog tanımlıyor, ama kitteki hiçbir şey sizin adınıza bir rotaya middleware('scope:...') eklemiyor — default_scopes'u boş bırakmak Passport'un örtük * scope'unu koruyor, dolayısıyla mevcut API istemcileri çalışmaya devam ediyor. Kısıtlamak istediğiniz her rotaya scope: middleware'ini kendiniz ekleyin.
  • ?type= alt-kaynak sorgu kapsamlandırması consumer'ın sorumluluğundadır. CheckResourcePermission, bir ?type= query parametresini tanıyor ve elde edilen <resource>:<type>.<ability> izni mevcutsa (örn. /admin/users?type=student → users:student.read), üst kaynağın izni yerine onu kontrol ediyor. Middleware yalnız çözdüğü izni kontrol ediyor — yanıtı aynı type değerine göre gerçekten filtrelemek controller'ınızın/query'nizin işi; middleware bunu sizin yerinize yapmıyor, ve izin kontrolü geçerken ?type='i görmezden gelen bir controller, bir rolün görmesine izin verilen şeyle endpoint'in döndürdüğü şey arasında bir uyumsuzluk yaratır.
  • EnsureUserIsActive tasarım gereği fail-open'dır. Yalnız starter-kit.security.active_status_denied'da (varsayılan ['inactive', 'banned']) açıkça listelenen bir status değerini engelliyor; kimliği doğrulanmış kullanıcısı olmayan bir guard, çözülemeyen bir guard, status özniteliği olmayan bir kullanıcı modeli, string olmayan bir status, ya da deny-list'te olmayan herhangi bir değer değişmeden geçiyor. Açıkça listelemediğiniz bir status'u engellemesini beklemeyin.

v13.6.8 → v13.6.9

CheckResourcePermission artık staging/demo'da fail-closed (davranış değişikliği)

Bu bir runtime (src/) değişikliğidir; yalnız composer update ile taşınır — publish edilmiş dosya değişmez, sk:update gerekmez. Buraya, production dışı host'larda bilinçli, güvenlik amaçlı bir davranış değişikliği olduğu için eklenmiştir.

Önce: middleware bir route'u DB'de seed'lenmemiş bir izne çözdüğünde, production dışındaki her ortamda (staging, uat, demo, testing) isteği bir uyarı logu ile geçiriyordu — yalnız production reddediyordu. Böylece public bir staging/demo host'u, izin satırı unutulmuş bir endpoint'i sessizce açığa çıkarabiliyordu.

Sonra: seed'lenmemiş bir izin, local dışındaki her ortamda reddedilir. local yine uyarıp geçirir; böylece henüz seed'lenmemiş bir izin günlük geliştirmeyi bloklamaz.

Fark edebilecekleriniz: public bir staging / uat / demo dağıtımında, izni (php artisan sk:seed-permissions ile) seed'lenmemiş bir route artık sessizce geçmek yerine 403 döner. Çözüm izni seed'lemektir — ki production zaten bunu gerektiriyordu.

Opt-out (eski davranışa dönüş): production dışı ortamlarda eski "geçir" davranışını bilinçli olarak istiyorsanız .env'e ekleyin:

STARTER_KIT_ALLOW_UNMAPPED_PERMISSIONS=true

(veya config(['starter-kit.permissions.allow_unmapped' => true])). Bu bayrak ne olursa olsun production her zaman reddeder; local her zaman geçirir.

Bu değişiklikte ayrıca: middleware'in seed'lenmiş izin sorgusu artık Octane-güvenli — izin adı kümesini tüm worker ömrü yerine kısa TTL (60sn) ile cache'ler ve sk:seed-permissions seed sonrası bu cache'i hemen temizler; böylece yeni seed'lenen izin, bayat bir worker geri dönüşene kadar beklemek yerine anında etkili olur.

Cache-store bağımlılığı: seed'lenmiş izin kontrolü artık bir container-instance binding yerine Cache::remember() (uygulamanızın yapılandırılmış varsayılan cache store'u) üzerinden çalışıyor. Networked bir cache store (Redis, Memcached, …) kullanıyorsanız, izin kontrolü yolu artık cache-miss durumunda o store'a dokunuyor — cache trafiğini izliyorsanız veya paylaşımlı/clustered bir store kullanıyorsanız bilmekte fayda var. file veya array cache driver'ındaki projelerde davranış farkı yok.


v13.6.7 → v13.6.8

Özet

Birkaç publish edilmiş stub dosyasına dokunan bir kalite/UX turu. Başlıca değişiklik bir güvenlik düzeltmesi: auth.login_throttle = '0' artık web login rate limiter'ını tamamen devre dışı bırakmıyor — bunun yerine bilinçli olarak gevşek bir taban limiter'a geçiyor. Bu sürümdeki diğer her şey (audit-log genişletmesi, sk:install/sk:doctor/sk:eject DX iyileştirmeleri, form/datatable erişilebilirliği) src/ altında (vendor runtime) yaşıyor ve yalnızca composer update yeterli — tam liste için CHANGELOG.md'ye bakın. Aşağıdaki adımları bir kez çalıştırın; sonraki bölümler referans detaydır.

composer update lvntr/laravel-starter-kit
php artisan sk:update   # güncellenmiş SettingsServiceProvider/FortifyServiceProvider, eslint.config.js, vitest.config.ts, Definition model, datatable.css'i teslim eder
npm run build

login_throttle = '0' artık web login limiter'ını tamamen devre dışı bırakmıyor

Öncesi: Ayarlar → Güvenlik'te auth.login_throttle değerini '0' yapmak Fortify'nin login rate limiter'ını tamamen null'a çekiyordu — bu tek ayarda 0 değeri web login'in sınırsız denemeyi kabul etmesine yol açıyordu.

Sonrası: stubs/app/Providers/SettingsServiceProvider.php artık sert login limiter'ını null'a çekmek yerine yeni bir login-relaxed limiter'a çeviriyor (tanımı stubs/app/Providers/FortifyServiceProvider.php'de). Web login bir yönetici tarafından gevşetilebilir, ama asla tam limitsiz kalmaz. API auth route'ları her iki durumda da etkilenmez — kendi sabit throttle:5,1 middleware'ini taşırlar.

SettingsServiceProvider.php veya FortifyServiceProvider.php'yi özelleştirmediyseniz: sk:update her iki değişikliği de otomatik teslim eder; yukarıdaki standart yükseltme adımlarının dışında bir işlem gerekmez.

Her ikisini de özelleştirdiyseniz: sk:update sizin versiyonunuzu korur ve hash farkını raporlar. İki değişikliği elle uygulayın:

  • FortifyServiceProvider::boot()'a bir login-relaxed RateLimiter::for(...) tanımı ekleyin (kesin limitler için vendor stub'a bakın).
  • SettingsServiceProvider'da, auth.login_throttle === '0' iken config(['fortify.limiters.login' => null]) satırını config(['fortify.limiters.login' => 'login-relaxed']) olarak değiştirin.

Projenize özel bir sebepten tamamen limitsiz bir login limiter'ı isterseniz, kendi kopyanızda doğrudan config(['fortify.limiters.login' => null]) ayarlamaya devam edebilirsiniz — bu artık kit'in varsayılan davranışı değildir.

İlgili küçük değişiklikler

  • stubs/eslint.config.js ruleset'i yükseltildi — pluginVue flat config essential'dan strongly-recommended'a taşındı. sk:update yeni dosyayı teslim eder; özelleştirmediyseniz, güncelledikten sonraki ilk npm run lint çalıştırmasında yeni (önceden var olan) Vue stil uyarıları görebilirsiniz. Kendi hızınızda düzeltin ya da kuralı kendi kopyanızda warn'a sabitleyin.
  • Vitest config vite.config.ts'den ayrıştırıldı — inline test: {...} bloğu yeni bir stubs/vitest.config.ts'e taşındı. sk:update iki dosyayı birlikte teslim eder; vite.config.ts'i özelleştirdiyseniz yeni vitest.config.ts'i elle yanına ekleyin (varsayılan environment/globals değerleri için vendor stub'a bakın).
  • API iki faktör challenge'ları artık atomik olarak sahipleniliyor — app/Domain/Auth/Actions/TwoFactorChallengeAction.php artık Cache::pull() çağrısını sahiplenme olarak kullanmıyor. Cache::pull() her cache sürücüsünde ayrı bir get + forget'tir; bu yüzden tek bir challenge id'sinin eşzamanlı iki denemesi aynı kullanıcı id'sini okuyup iki access token üretebiliyordu ve route'taki throttle:5,1 bu yarışı daraltsa da sıraya sokmuyor. Action artık challenge'ı bir yardımcı anahtar üzerinden (api:2fa_challenge_claimed:{uuid}) Cache::add() ile sahipleniyor — store içinde atomik olan "yoksa ekle" işlemi — ve payload'ı yalnızca kazanan okuyor. Cache::lock() yerine Cache::add() bilinçli bir tercih: database cache sürücüsünde lock ayrı bir cache_locks tablosuna ihtiyaç duyar ve bu tabloyu oluşturmamış bir kurulum 2FA endpoint'inde sert bir hata alırdı. Yapılandırma değişikliği veya yeni tablo gerekmiyor; tek kullanımlık davranış da aynı kalıyor (yanlış kod challenge'ı yine tüketir). TwoFactorChallengeAction.php veya LoginUserAction.php dosyalarını özelleştirdiyseniz, sk:update kopyalarınızı korur ve hash farkı raporlar — değişikliği elle taşıyın: LoginUserAction, public bir TWO_FACTOR_CHALLENGE_TTL sabiti ile challengeClaimKey() yardımcısını kazandı ve TwoFactorChallengeAction::execute(), başka hiçbir şey okumadan önce Cache::add(LoginUserAction::challengeClaimKey($challenge), true, LoginUserAction::TWO_FACTOR_CHALLENGE_TTL) false döndüğünde null dönmelidir.
  • Definition modeli bir cache-flush observer'ı kazandı — app/Models/Definition.php artık her yazma yolunda (saved/deleted/restored/forceDeleted) definition cache'ini flush ediyor; bu, seeder dışında bir yolla Definition yazmanın ~1h TTL'ye kadar bayat cache bırakabildiği bir hatayı düzeltiyor. Eski (hatalı) bayatlığa güveniyorduysanız dışında görünür bir değişiklik yok.
  • Datatable inline arama-temizle / filtre-kaldırma markup'ı — stubs/resources/css/theme/main/components/datatable.css, klavye erişilebilirliği için altındaki icon-only <span>'i gerçek bir <button>'a çevirdi; CSS reset görsel olarak birebir aynı tutuyor. Standart sk:update && npm run build dışında bir işlem gerekmez.

v13.5.11 → v13.6.0

Özet

13.6.0, v13.5.11'den (son yayınlanan sürüm) bu yana publish edilmiş dosyalara dokunan tüm değişiklikleri tek bir geçişte toplar. Vendor-runtime migrasyonunu tamamlar — backend yardımcı sınıfları, middleware, üç üçüncü-parti config, 15 composable, TurnstileWidget.vue ve v-can / v-role izin direktif plugin'i artık vendor paketinden çalışır — ve yapılandırılmış tema/layout/CSS sistemini getirir: bir AppShell.vue kompozisyonu, themes/main/ slot ağacı (her CSS cascade katmanı override edilebilir bir slot) ve opt-in themes/custom/ override teması. Ayrıca Güvenlik Ayarları yeniden tasarımını getirir: Güvenlik sekmesi üç alt sekmeye ayrılır (Kimlik Doğrulama / Parola Politikası / Cloudflare Turnstile), altı yeni auth.* ayar anahtarı eklenir ve parola kuralları ile parola geçerlilik süresi EnsurePasswordNotExpired middleware'i aracılığıyla tam olarak uygulamaya alınır. Varsayılan build'de görsel değişiklik yoktur — varsayılan build (VITE_SK_THEME=main) güvenlik ayarlarına dokunmayan projeler için v13.5.11 ile byte-identical'dır. Aşağıdaki adımlarla geçişi tek seferde yapın; ardından gelen alan-bazlı bölümler referans detaydır (yalnızca projenize uyan "özelleştirdiyseniz…" notlarını uygulayın).

composer update lvntr/laravel-starter-kit
php artisan sk:update          # yeni stub'ları getirir: layout, CSS tema ağacı, resolver, .env.example + package.json güncellemeleri
php artisan migrate            # users tablosuna password_changed_at kolonu ekler
npm install
npm run build                  # panel birebir aynı görünmeli

Runtime tema geçişi — main ve aura

Ayarlar → Görünüm'de tema seçimi artık iki yerleşik kit teması (main ve aura) için anında uygulanır — derleme gerekmez. Her ikisi de her zaman bundle'a dahildir; aura, yeni useTheme composable'ının runtime'da <html> üzerine yazdığı data-sk-theme="aura" attribute'u ile etkinleşir.

Tema Nasıl etkinleşir Derleme gerekli mi?
main Varsayılan — data-sk-theme attribute'u yok Hayır
aura useTheme tarafından <html> üzerine data-sk-theme="aura" yazılır Hayır
Custom (consumer tarafından oluşturulan) .env'de VITE_SK_THEME=<isim> Evet

Mevcut kurulumlar

php artisan sk:update && npm run build komutlarını çalıştırın (standart v13.6.0 geçişi). Güncellenmiş AdminLayout.vue stub'ı, mevcut useDarkMode() ve useAccentColor() çağrılarının yanına useTheme() çağrısı ekler. Build sonrasında Ayarlar → Görünüm'de main ile aura arasında geçiş yapmak anında gerçekleşir.

resources/css/theme/<isim>/ altında oluşturduğunuz custom temalar eskisi gibi çalışmaya devam eder — build-zamanı slot resolver'ı değişmemiştir.

aura CSS theme-runtime/'a taşındı

Daha önce resources/css/theme/aura/ konumundaki aura CSS dosyaları resources/css/theme-runtime/aura/ konumuna taşındı. Tüm kurallar artık html[data-sk-theme='aura']'ya scope'ludur.

sk:update, yeni theme-runtime/aura/ ağacını getirir ve eski theme/aura/ dizinini kaldırır. theme.css giriş dosyası artık iki import içerir:

@import './_active.css';
@import '../theme-runtime/aura/aura.css';

theme/theme.css'i özelleştirdiyseniz: sk:update sonrasında ikinci import'u yeniden ekleyin. sk:update, theme.css için hash farkı raporlar; iki-import modelini kopyanıza elle uygulayın.

VITE_SK_THEME=aura kullanıyorsanız: bu değişkeni kaldırın — artık istenen etkiyi üretmemektedir. aura teması artık slot tabanlı bir build-zamanı teması değildir; yalnızca Ayarlar → Görünüm üzerinden runtime'da etkinleşir. VITE_SK_THEME=aura ayarıyla resolver 24 slots, 0 overrides üretir (aura slot ağacında değil) ve aura görsel stili yine de etkinleşir — ancak _active.css üzerinden değil, yalnızca runtime attribute üzerinden.

Görsel değişiklik yok

main (varsayılan) kullanan projelerde build sonrası görsel çıktı byte-identical'dır. VITE_SK_THEME=aura kullanan projeler için değişkeni kaldırın, yeniden derleyin ve Ayarlar → Görünüm üzerinden aura'ya geçin — sonuç görsel olarak aynıdır.

İlgili küçük değişiklikler

  • sk:doctor Theme Manifest kontrolü artık _active.css başlığındaki tema VITE_SK_THEME ile uyuşmadığında uyarı vermiyor. Runtime tema kaydı marker'ı rutin olarak main'e sıfırladığından bu karşılaştırma sistematik yanlış pozitif üretirdi. Eksik manifest hard-fail'i ve ../ traversal uyarısı yerinde duruyor.
  • .env.example artık VITE_SK_THEME göndermiyor. Değişken build-time resolver tarafından custom temalar için hâlâ tanınır (marker → VITE_SK_THEME → main) — custom tema kullanacaksanız .env dosyanıza kendiniz ekleyin.

Kurulum anında domain eject'i (User + Role)

Bu sürümden itibaren sk:install, ilk çalıştırmada User ve Role domain runtime'ını otomatik olarak app/Domain/User/ ve app/Domain/Role/ altına eject eder. Bu değişiklik yalnızca yeni kurulumları etkiler — mevcut kurulumlar etkilenmez.

Mevcut kurulumlar — işlem gerekmez

Eject adımı hash registry ile korunmaktadır: yalnızca storage/starter-kit/hashes.json henüz yoksa (yani ilk kurulumda) çalışır. Mevcut kurulumda registry zaten mevcut olduğundan adım tamamen atlanır. app/Domain/{User,Role}/Actions daha önce yapılan manuel bir eject nedeniyle zaten mevcutsa, o domain'in eject'i uyarıyla atlanır ve kurulumun geri kalanı normal şekilde devam eder.

Bu sürümdeki yeni kurulumlar

app/Domain/User/ ve app/Domain/Role/, backend sınıfları App\Domain\ namespace'iyle yeniden yazılmış olarak oluşturulur; DomainServiceProvider altı audit-event Event::listen binding'ini alır. Takas: bu dosyalar artık size ait — composer update bu dosyalara upstream değişiklik göndermez. Ayrıntılar için artisan-commands.tr.md içindeki sk:eject güncelleme-kaybı notuna bakın.

Kurulum sırasında devre dışı bırakmak için:

php artisan sk:install --without-eject

Otomatik eject'i geri alma

rm -rf app/Domain/User/ app/Domain/Role/
# User ve Role için enjekte edilen Event::listen satırlarını kaldırın:
# app/Providers/DomainServiceProvider.php
composer dump-autoload

StarterKitServiceProvider içindeki class_alias tanımları, App\Domain\User\* ve App\Domain\Role\* import'larını otomatik olarak tekrar vendor kopyalarına yönlendirir.


Davranış-modülü HTTP + Vue katmanları vendor'a taşındı (v13.6.0)

Beş davranış modülünün — Files, Logs, ActivityLogs, ApiRoutes, Settings — HTTP katmanı (controller'lar + FormRequest'ler) ve Vue admin sayfaları uygulamanızdan vendor paketine taşındı. Fresh install'da bu dosyalar artık app/'e kopyalanmıyor. Mevcut kurulumda sk:update bunları hash guard altında otomatik olarak migrate eder (aşağıya bakın).

Modül bazında ne değişti

Modül Uygulamanızdaydı Artık vendor-resident
Files (File Manager) resources/js/pages/Admin/Files/ (yalnızca Vue — backend zaten vendor'daydı) Vue sayfaları app.ts fallback aracılığıyla paketten sunulur
Logs app/Http/Controllers/Admin/LogController.php, app/Http/Requests/Admin/Log/, resources/js/pages/Admin/Logs/ controller + request'ler + Vue sayfaları
Activity Logs app/Http/Controllers/Admin/ActivityLogController.php, resources/js/pages/Admin/ActivityLogs/ controller + Vue sayfaları
API Routes app/Http/Controllers/Admin/ApiRouteController.php, resources/js/pages/Admin/ApiRoutes/ controller + Vue sayfaları
Settings app/Http/Controllers/Admin/SettingsController.php, app/Http/Requests/Admin/Settings/, resources/js/pages/Admin/Settings/ controller + request'ler + Vue sayfaları

Vendor çözümlemesi nasıl çalışır

  • Controller'lar + FormRequest'ler, StarterKitServiceProvider::backwardCompatAliasPlan() aracılığıyla çözülür: App\Http\Controllers\Admin\SettingsController (ve diğer dördü) Lvntr\StarterKit\Http\... karşılıklarına alias'lanır. Bir app/Http/Controllers/Admin/SettingsController.php dosyası var olduğu anda alias devre dışı kalır — böylece mevcut herhangi bir app kopyası hiçbir import değişikliği olmadan kazanmaya devam eder.
  • Vue sayfaları, app.ts vendor-fallback loader'ı aracılığıyla çözülür: import.meta.glob('@lvntr/pages/...'), yerel resources/js/pages/ glob'undan sonra kontrol edilir. Yerel bir dosya mevcut olduğunda app-önce glob her zaman kazanır.

app.ts vendor-sayfa fallback gerekliliği

Vue migrasyonu, resources/js/app.ts'in @lvntr/pages vendor glob'unu içermesine bağlıdır. sk:update, vendor-migrate edilmiş herhangi bir Vue dosyasını kaldırmadan önce bu marker'ı kontrol eder; yoksa Vue grupları bir uyarıyla yerinde bırakılır:

 WARN  app.ts does not contain the @lvntr/pages vendor fallback — Vue migration skipped.
       Run `php artisan sk:update` after updating app.ts to complete the migration.

Bu uyarıyı görürseniz, app.ts'inize vendor-sayfa resolver'ını ekleyin. Güncellenmiş stub sk:update tarafından sağlanır — hash-tracked değişikliği app.ts'e uygulayın ve sk:update'i tekrar çalıştırın.

Mevcut kurulumlar — sk:update ne yapar

sk:update, dosyaları modül grubu bazında, iki bağımsız katmanda (PHP ve Vue) migrate eder:

  • Değiştirilmemiş kopyalar (disk üzerindeki hash registry kaydıyla eşleşir): silinir. Vendor kopyası devralır — controller alias köprüsü aracılığıyla, Vue sayfaları app.ts fallback aracılığıyla.
  • Değiştirilmiş kopyalar (hash farklı ya da registry kaydı yok): yerinde tutulur, korunmuş olarak raporlanır. Özelleştirilmiş dosyanız vendor kopyasını kazanmaya devam eder.
  • Grup atomikliği: bir modülün PHP katmanındaki tek bir dosya bile değiştirilmişse, o modülün tüm PHP katmanı korunur (ör. özelleştirilmiş bir SettingsController.php, eşleşen app/Http/Requests/Admin/Settings/ dizinini de korur). PHP ve Vue katmanları bağımsız olarak değerlendirilir.

sk:update sonrasında çalıştırın:

npm run build

Migration, route değişikliği veya permission değişikliği gerekmez.

Üç yaygın senaryo

Senaryo A — değiştirilmemiş kurulum (standart durum)

composer update lvntr/laravel-starter-kit
php artisan sk:update   # hash-korumalı kaldırma — beş modülün tümü otomatik migrate olur
npm run build

sk:update, değiştirilmemiş app kopyalarını kaldırır ve vendor devralır. Başka işlem gerekmez.

Senaryo B — bir veya birden fazla modülde değiştirilmiş dosya(lar)

sk:update, korunan her modül grubunu raporlar:

 WARN  Vendor-migrated paths preserved (user-modified or untracked):
  • app/Http/Controllers/Admin/SettingsController.php (modified)
  • app/Http/Requests/Admin/Settings/ (preserved with controller)
  • resources/js/pages/Admin/Settings/ (modified)

Özelleştirilmiş dosyalarınız çalışmaya devam eder — siz migrate etmeyi seçene kadar işlem gerekmez. Özelleştirilmiş modülün tam sahipliğini açıkça almak için:

php artisan sk:eject Setting   # vendor controller + request'leri + Vue'yu App\ namespace'iyle uygulamanıza kopyalar

Senaryo C — v13.6.0+'tan fresh install

İşlem gerekmez. sk:install, beş modülün hiçbirini kopyalamaz. İlk günden itibaren vendor'dan çalışırlar.

Vendor-resident bir modülün tam sahipliğini alma

Bir modülün backend'ini (controller, FormRequest'ler) veya Vue sayfalarını vendor'a migrate olduktan sonra özelleştirmek için sk:eject kullanın:

php artisan sk:eject Logs             # backend + Vue sayfaları
php artisan sk:eject Logs --no-vue    # yalnızca backend
php artisan sk:eject Logs --dry-run   # önce önizleme
php artisan sk:eject Files            # yalnızca Vue sayfaları (Files backend'i her zaman vendor'da kalır)

Eject sonrasında sk:update, bu dosyaları consumer-owned olarak değerlendirir ve asla kaldırmaz. Tam sk:eject flag referansı ve güncelleme-kaybı takası için artisan-commands.tr.md'ye bakın.

Değişmeyen alanlar

Alan Durum
Route dosyaları (routes/web/*-route.php) Değişmez — uygulamanızda kalır
Route dosyalarınızdaki App\Http\Controllers\Admin\* import'ları Çalışmaya devam eder — alias köprüsü ya da ejected kopya bunları çözer
Permission key'leri, route isimleri Değişmez
config/permission-resources.php Değişmez — sanctuary, asla üzerine yazılmaz
User / Role / Dashboard / Auth / Profile modülleri Değişmez — tamamen app-owned kalır

Davranış-modülü HTTP katmanı vendor'a taşındı — Faz 2 (v13.6.0)

Faz 1 (yukarıda), Files / Logs / ActivityLogs / ApiRoutes / Settings controller'larını ve Vue sayfalarını vendor'a taşıdı. Faz 2, vendor Settings sekmelerini besleyen kalan controller'ları ve zaten vendor servislerini saran iki API/Service controller'ını taşıyarak tabloyu tamamlar. Vue ve migration'lar zaten vendor'daydı (Faz 1'de sağlandı); Faz 2, yalnızca PHP katmanı taşımasıdır.

Modül bazında ne değişti

Modül Uygulamanızdaydı Artık vendor-resident
API Clients app/Http/Controllers/Admin/ApiClientController.php, app/Http/Requests/Admin/ApiClient/, app/Http/Resources/Admin/ApiClient/ controller + request'ler + resource
API Tokens app/Http/Controllers/Admin/ApiTokenController.php, app/Http/Requests/Admin/ApiToken/, app/Http/Resources/Admin/ApiToken/ controller + request + resource
System Health app/Http/Controllers/Admin/SystemHealthController.php controller (domain / request / resource yok)
Definitions (API + Service) app/Http/Controllers/Api/DefinitionController.php, app/Http/Controllers/Service/DefinitionServiceController.php her iki controller (vendor DefinitionService zaten vendor'daydı)
Media upload/delete app/Http/Controllers/Api/MediaUploadController.php controller
Content Languages app/Domain/ContentLanguage/ (Actions/DTOs/Queries), app/Http/Controllers/Admin/ContentLanguageController.php, app/Http/Requests/Admin/ContentLanguage/, app/Http/Resources/Admin/ContentLanguage/ domain runtime + controller + request'ler + resource

Vendor çözümlemesi nasıl çalışır

Faz 1 ile aynıdır: taşınan her controller / FormRequest / Resource, StarterKitServiceProvider::backwardCompatAliasPlan() tarafından App\Http\... FQCN'inden Lvntr\StarterKit\Http\... karşılığına, bir file_exists guard'ı altında alias'lanır — bir app/Http/Controllers/Admin/ApiClientController.php (ya da başka herhangi biri) dosyası var olduğu anda alias kenara çekilir ve sizin kopyanız kazanır. App\Domain\ContentLanguage\... runtime sınıfları da aynı şekilde çözülür (Lvntr\StarterKit\Domain\ContentLanguage\...'a alias). Route dosyalarınız mevcut App\Http\Controllers\... import'larını değişmeden korur.

Model'ler app-owned kalır

App\Models\ContentLanguage, App\Models\Media ve App\Models\Definition vendor'a taşınmaz ve alias'lanmaz — bir model'i taşımak Laravel'in XPolicy keşfini ve route-model binding'ini kırardı. Vendor ContentLanguageController, MediaUploadController ve DefinitionController bu model'lere App\ FQCN'leriyle referans verir; ejected app/Domain/ContentLanguage runtime'ı App\Models\ContentLanguage referansını değişmeden korur. content_languages ve media migration'ları zaten vendor'dadır (Faz 4) — Faz 2'de migration değişikliği yoktur.

Mevcut kurulumlar — geçiş adımları

composer update lvntr/laravel-starter-kit
php artisan sk:update   # artık-vendor PHP kopyalarının hash-korumalı kaldırması

sk:update, Faz 2 PHP katmanının değiştirilmemiş app kopyalarını modül grubu bazında kaldırır (Faz 1 ile aynı grup-atomik kural: bir modülün PHP katmanındaki herhangi bir değiştirilmiş dosya tüm katmanı korur). Yalnızca Faz 2 için Vue yeniden build'i gerekmez — Vue Faz 1'de migrate olmuştu — ancak tam v13.6.0 geçişinden sonra npm run build çalıştırmak doğru tek adım olmaya devam eder.

Tam sahipliği alma

php artisan sk:eject ApiClient          # ApiClient + ApiToken controller'ları + request'leri + resource'ları
php artisan sk:eject ContentLanguage    # domain + controller + request + resource
php artisan sk:eject SystemHealth       # yalnızca controller
php artisan sk:eject Definitions        # Api + Service controller'ları (DefinitionService vendor'da kalır)
php artisan sk:eject MediaUpload        # yalnızca controller (routes/web.php içindeki media.destroy route'u)

Eject sonrasında sk:update, bu dosyaları consumer-owned olarak değerlendirir ve asla kaldırmaz. Tam eject domain tablosu ve güncelleme-kaybı takası için artisan-commands.tr.md'ye bakın.

Değişmeyen alanlar

Alan Durum
Route dosyaları (routes/web/*-route.php, routes/web.php, routes/api/service-route.php) Değişmez — uygulamanızda kalır; yalnızca controller use import'u vendor'ı gösterir
Permission key'leri, route isimleri Değişmez — route isimleri CheckResourcePermission'ı sürer; hiçbir şey yeniden adlandırılmadı
Passport client/token secret tek-sefer-gösterim Değişmez — ApiClientController / ApiTokenController logic'i byte-identical, yalnızca dosya konumu taşındı
App\Models\{ContentLanguage,Media,Definition} Asla taşınmaz, asla alias'lanmaz — app-owned
RoleServiceController Değişmez — Role/Setting scaffold ekranlarını besler, app-owned kalır
LocaleController, Api/UserController, Api/Auth/*, Dashboard / User / Role / Profile / Auth controller'ları Değişmez — scaffold, tamamen app-owned

Domain runtime katmanları vendor'a taşındı (Faz 6)

Beş domain modülünün runtime katmanı (Actions, DTOs, Queries, Events, Listeners ve Setting servisi), stubs/app/Domain/ yerine pakete (src/Domain/, PSR-4 Lvntr\StarterKit\Domain\) taşındı. Tüketici yüzeyi — Controller'lar, FormRequest'ler, Model'ler, Vue sayfaları, route dosyaları, Policy'ler ve config/settings.php — uygulamanızda kalmaya devam eder ve etkilenmez.

Etkilenen domain'ler: ApiClient, ApiRoute, Setting, User, Role.

Değişmeyen alanlar

Alan Durum
Controller / provider'larınızdaki App\Domain\<Module>\... import'ları Çalışmaya devam eder — class_alias bunları vendor namespace'ine şeffaf biçimde çözer
Mevcut app/Domain/{ApiClient,ApiRoute,Setting,User,Role}/ kopyaları Korunur, otomatik silinmez
Controller'lar, FormRequest'ler, Model'ler, Vue sayfaları, route dosyaları Değişmez — uygulamanızda kalır
App\Models\User, App\Models\Role, App\Models\Setting Asla vendor'a taşınmaz
Store/UpdateRoleRequest privilege-boundary (validated()) Değişmez — app-owned
config/permission-resources.php Değişmez — sanctuary (sk:update asla üzerine yazmaz)
config/settings.php Değişmez — sanctuary (sk:update asla üzerine yazmaz, bu sürümde eklendi)
Policy'ler (UserPolicy, RolePolicy, SettingPolicy, ApiClientPolicy, ApiTokenPolicy) Değişmez — app-owned
Postman / Apidog console command'ları Değişmez — app-owned
BulkDeleteUserAction, BulkDeleteRoleAction Değişmez — app-owned (app-owned App\Http\BulkActions\BulkDeleteAction override base sınıfını extend eder)
Permission key'leri, route isimleri, API zarfı Değişmez
RoleEnum (system_admin / admin / user sözleşmesi) Değişmez — app-owned

Yeni kurulumlar (v13.6.0+)

sk:install artık app/Domain/ApiClient/, app/Domain/ApiRoute/, app/Domain/Setting/, app/Domain/User/Actions/, app/Domain/User/DTOs/, app/Domain/User/Events/, app/Domain/User/Listeners/, app/Domain/User/Queries/, app/Domain/Role/Actions/, app/Domain/Role/DTOs/, app/Domain/Role/Events/, app/Domain/Role/Listeners/ veya app/Domain/Role/Queries/ dizinlerini app/'e kopyalamaz. Bu modüllerin runtime sınıfları doğrudan vendor/lvntr/laravel-starter-kit/src/Domain/'dan çalışır. Scaffold controller'larındaki App\Domain\<Module>\... import'ları class_alias aracılığıyla çözülür — üretilen kodda herhangi bir import değişikliği gerekmez.

Mevcut kurulumlar — geçiş adımları

composer update lvntr/laravel-starter-kit
php artisan sk:update

sk:update, uygulamanızda kalan vendor-resident app kopyalarını raporlar (yalnızca bilgilendirici — otomatik olarak asla silinmez):

 WARN  v13.5.0+: package runtime runs from vendor. The following files still exist in your app:

  • app/Domain/ApiClient/
  • app/Domain/ApiRoute/
  • app/Domain/Setting/
  • app/Domain/User/Actions/
  • app/Domain/User/DTOs/
  • app/Domain/User/Events/
  • app/Domain/User/Listeners/
  • app/Domain/User/Queries/
  • app/Domain/Role/Actions/
  • app/Domain/Role/DTOs/
  • app/Domain/Role/Events/
  • app/Domain/Role/Listeners/
  • app/Domain/Role/Queries/

  Deleting these files is optional; vendor copies take precedence.

Başka herhangi bir adım gerekmez. Uygulamanız değişmeden çalışmaya devam eder.

İsteğe bağlı temizlik — eski app kopyalarını reconcile etme

Bu adım tamamen isteğe bağlıdır ve daha sonra da yapılabilir.

Bu domain dosyalarının hiçbirini özelleştirmediyseniz, app kopyalarını silerek vendor versiyonlarının (class_alias aracılığıyla) devreye girmesini sağlayabilirsiniz:

# ApiClient + ApiRoute
rm -rf app/Domain/ApiClient/
rm -rf app/Domain/ApiRoute/

# Setting runtime (app/Models/Setting.php ve app/Policies/SettingPolicy.php'yi koruyun)
rm -rf app/Domain/Setting/

# User runtime (app/Domain/User/BulkActions/'ı koruyun)
rm -rf app/Domain/User/Actions/
rm -rf app/Domain/User/DTOs/
rm -rf app/Domain/User/Events/
rm -rf app/Domain/User/Listeners/
rm -rf app/Domain/User/Queries/

# Role runtime (app/Domain/Role/BulkActions/'ı koruyun)
rm -rf app/Domain/Role/Actions/
rm -rf app/Domain/Role/DTOs/
rm -rf app/Domain/Role/Events/
rm -rf app/Domain/Role/Listeners/
rm -rf app/Domain/Role/Queries/

Bir domain dosyasını özelleştirdiyseniz, app/Domain/<Module>/ dizinini veya ilgili alt dizini koruyun. class_alias guard'ı bunu algılar ve devre dışı kalır — özelleştirilmiş versiyonunuz vendor kopyasını kazanmaya devam eder. Değiştirilmemiş alt dizinleri silerken değiştirilmişleri saklayabilirsiniz; guard sınıf düzeyinde çalışır.

Kısmi reconcile örneği — özelleştirilmiş bir Action'ı koruyun, geri kalanları silin:

# User Actions'dan yalnızca özelleştirilmiş dosyayı bırakın
rm app/Domain/User/Actions/DeleteUserAction.php
rm app/Domain/User/Actions/UpdateUserAction.php
# Koru: app/Domain/User/Actions/CreateUserAction.php (özelleştirilmiş)

Domain bazında override yolları

Taşınan tüm runtime sınıfları overridable tier'dadır: app/Domain/<Module>/<Class>.php dosyası mevcutsa vendor kopyasını geçersiz kılar — alias guard otomatik olarak devre dışı kalır. Açık bir opt-in gerekmez.

Katman Override yolu
Action, DTO, Query, Event, Listener, Service App\Domain\<Module>\... namespace'iyle app/Domain/<Module>/<path>.php oluşturun — alias guard vendor versiyonunu otomatik olarak atlar
Controller, FormRequest, Resource, Policy Zaten app-owned — doğrudan düzenleyin
config/settings.php Zaten app-owned sanctuary — doğrudan düzenleyin
Vue sayfaları (resources/js/pages/Admin/*) Zaten app-owned — doğrudan düzenleyin
Runtime sınıfını yeniden yayınlama PHP runtime sınıfları için sk:publish <domain> tag'i yoktur. vendor/lvntr/laravel-starter-kit/src/Domain/<Module>/ konumundan app/Domain/<Module>/ konumuna App\Domain\<Module>\ namespace'iyle kopyalayın; alias guard gerisini halleder

User / Role audit log event wiring

UserCreated, UserUpdated, UserDeleted, RoleCreated, RoleUpdated ve RoleDeleted event'leri ile Log* listener'ları artık vendor-resident'tır. StarterKitServiceProvider, event→listener bağlamalarını doğrudan kaydeder; DomainServiceProvider'daki bu altı çift için kayıtlar artık gerekli değildir.

Fresh install: DomainServiceProvider, bu altı çift için artık Event::listen çağrısı içermez — vendor provider bunları işler.

Mevcut kurulum: app/Providers/DomainServiceProvider.php içinde hâlâ bu çiftler için Event::listen kayıtları varsa (ör. Event::listen(UserCreated::class, LogUserCreated::class)), app kopyalarını reconcile ettikten sonra bu kayıtları kaldırın. App kopyaları mevcutken kayıtların tutulması zararsızdır — alias guard vendor binding'i atlar ve app tarafındaki kayıt tam olarak bir kez çalışır. Tamamen vendor'a geçmek için app kopyalarını ve DomainServiceProvider kayıtlarını birlikte kaldırın.

config/settings.php never-update sanctuary'sine eklendi

config/settings.php artık NEVER_UPDATE_PATHS listesindedir. sk:update bu dosyanın üzerine asla yazmaz; eklediğiniz sensitive key'ler veya setting grupları korunur. Bu, v13.6.0'dan itibaren geçerlidir — elle bir işlem gerekmez.

Değişmeyen alanlar

Alan Durum
Auth / permission davranışı Değişmez — CheckResourcePermission, permission-resources.php, RoleEnum dokunulmadı
API secret — Passport plainSecret tek-seferlik kuralı Değişmez — secret-üreten yüzey app-owned ApiClientController ve ApiClientResource'da kalır
Setting şifreleme — SettingService sensitive_keys okuma / Crypt::encryptString yazma Değişmez — logic aynı; yalnızca dosya konumu taşındı
config/settings.php sensitive-keys whitelist Değişmez — app-owned ve artık sanctuary
Route dosyaları ve middleware tier'ları Değişmez — tüm route dosyaları app-owned; route registry değişmedi

Kit çevirileri vendor'a taşındı (Faz 5)

44 adet kit-özel çeviri dosyası (sk-*.php, iki locale) stubs/lang/ yerine paket içine (resources/lang/{en,tr}/sk-*.php) taşındı. Önceden derlenmiş JSON (resources/js/lang/php_en.json / php_tr.json) bunlarla birlikte dağıtılıyor ve frontend i18n setup'ı tarafından otomatik tüketiliyor. Çeviri dosyaları artık fresh install'da uygulamanıza toplu olarak kopyalanmıyor.

Çeviriler nasıl dağıtılıyor

Katman v13.6.0 öncesi v13.6.0+
Frontend ($t('sk-common.*')) app/lang/*.php Vite plugin tarafından derlenir Vendor JSON, build zamanında app/lang ile merge edilir — çakışmada app kazanır
PHP backend (__('sk-common.*')) sk:install'ın kopyaladığı app/lang/{locale}/sk-*.php StarterKitServiceProvider boot'ta vendor resources/lang/{locale}/sk-*.php'yi kaydeder

Merge önceliği

Frontend i18n setup'ı (resources/js/app.ts) artık iki kaynak yükler:

  1. Vendor JSON — vendor/lvntr/laravel-starter-kit/resources/js/lang/php_{locale}.json (uygulamanızda override edilmeyen her key için fallback)
  2. App JSON — Vite i18n plugin'inin app/lang/*.php'den ürettiği lang/php_{locale}.json (öncelikli — özelleştirmeleriniz her zaman kazanır)

Eksik çeviriler vendor varsayılanına düşer. Bir sk-* key'ini özelleştirmediyseniz görünür bir değişiklik olmaz; özelleştirdiyseniz kendi versiyonunuz gösterilmeye devam eder.

Yeni kurulumlar (v13.6.0+)

sk:install artık lang/{en,tr}/sk-*.php dosyalarını uygulamanıza kopyalamaz. Kit çevirileri vendor paketinden sunulur. lang/{en,tr}/validation.php hâlâ kopyalanır — bu, standart Laravel validation override yüzeyidir ve uygulamanızda kalmaya devam eder.

Ekstra bir adım gerekmez.

Mevcut kurulumlar — geçiş adımları

composer update lvntr/laravel-starter-kit
php artisan sk:update
npm run build

sk:update, uygulamanızda kalan lang/{locale}/sk-*.php kopyalarını raporlar (yalnızca bilgilendirici — otomatik olarak asla silinmez):

 WARN  These files are now vendor-resident. Your app copies are kept; vendor copies take precedence where no app copy exists.

  • lang/en/sk-activity-log.php
  • lang/en/sk-api-clients.php
  • ... (locale başına 22 dosya)

  Deleting these files is optional; if present, they continue to take precedence over the vendor default for any keys they define.

Uygulamanız güncellemeden sonra değişmeden çalışmaya devam eder. Frontend bundle'ında yeni vendor JSON kaynağını almak için npm run build komutunu çalıştırın.

İsteğe bağlı temizlik

Hiçbir sk-*.php çeviri dosyasını özelleştirmediyseniz, tamamen vendor varsayılanına bırakmak için app kopyalarını silebilirsiniz:

rm lang/en/sk-*.php
rm lang/tr/sk-*.php

Bir ya da birden fazla dosyayı özelleştirdiyseniz, bunları koruyun — ya da yalnızca değiştirdiğiniz dosyaları saklayın. app/lang/{locale}/sk-*.php içinde tanımlı her key, o key için vendor değerini geçersiz kılar. App kopyanızda bulunmayan key'ler vendor varsayılanına düşer.

Özelleştirme ve kaçış kapısı

Vendor çeviri dosyalarını tam özelleştirme için uygulamanıza yayınlamak üzere:

php artisan sk:publish lang

Bu komut, vendor resources/lang/ içeriğini lang/vendor/starter-kit/ konumuna kopyalar ve namespace'li starter-kit:: çevirilerini kullanıma açar. Frontend ve backend'in kullandığı namespace-siz sk-* key'leri için override'larınızı doğrudan lang/{locale}/sk-*.php içine koyun — merge bunları otomatik alır.

Değişmeyen alanlar

Alan Durum
Çeviri içeriği — tüm sk-* string'leri Değişmez — yalnızca dosya konumu taşınır
lang/{locale}/validation.php Değişmez — uygulamanızda kalır (Laravel framework override yüzeyi)
Permission key'leri, route isimleri, API zarfı Değişmez
Frontend $t('sk-*') çağrı yerleri Değişmez
Vendor runtime'daki PHP __('sk-*') çağrı yerleri Değişmez — StarterKitServiceProvider aracılığıyla vendor resources/lang/ üzerinden çözülür

Kit migration'ları vendor'a taşındı (Faz 4)

Altı kit-özel migration stubs/database/migrations/ yerine paket içine (database/migrations/, loadMigrationsFrom ile otomatik yüklenir) taşındı. Artık fresh install'da uygulamanıza kopyalanmıyorlar.

Taşınan migration'lar

Dosya Tablo
2026_03_08_205445_create_media_table.php media
2026_03_11_071628_create_activity_log_table.php activity_log
2026_03_12_001950_create_definitions_table.php definitions
2026_03_14_080933_create_settings_table.php settings
2026_04_13_100200_add_folder_id_to_media_table.php media (folder_id ekleme)
2026_05_02_094121_add_soft_deletes_to_media_table.php media (deleted_at ekleme)

Framework-default migration'lar (create_users_table, create_cache_table, create_jobs_table), Passport OAuth migration'ları ve Spatie permission migration'ı taşınmadı — stubs/database/migrations/ içinde kalmaya devam ediyor ve uygulamanıza kopyalanmaya devam ediyor.

Nasıl çalışır

config('starter-kit.run_migrations') değeri true (varsayılan) olduğunda paket, vendor-resident migration'ları loadMigrationsFrom aracılığıyla otomatik yükler. Laravel migration geçmişini dosya adıyla (basename) takip ettiğinden, uygulamanızda zaten çalışmış bir migration sessizce atlanır — çift koşma veya hata olmaz.

Yeni kurulumlar (v13.6.0+)

sk:install artık yukarıda listelenen altı migration'ı kopyalamaz. Bu migration'lar doğrudan vendor paketinden çalışır. Ekstra bir adım gerekmez.

Mevcut kurulumlar — geçiş adımları

composer update lvntr/laravel-starter-kit
php artisan sk:update
php artisan migrate

sk:update, yukarıda listelenen altı app kopyasını force-delete eder. Bu güvenlidir: her dosya adı zaten migrations tablonuzda kayıtlıdır ve yeniden koşmaz (Laravel'in basename bazlı deduplication'ı). php artisan migrate, başka bekleyen migration yoksa "Nothing to migrate" döner.

Otomatik yüklemeyi devre dışı bırakma (kaçış kapısı)

Uygulamanızda fiziksel bir kopyaya ihtiyaç duyuyorsanız — örneğin bir migration'ı çalışmadan önce değiştirmek ya da statik analiz aracını tatmin etmek için — vendor migration'larını yayınlayın ve otomatik yüklemeyi kapatın:

php artisan vendor:publish --tag=starter-kit-migrations
php artisan vendor:publish --tag=starter-kit-config

Ardından config/starter-kit.php içinde run_migrations değerini false yapın:

'run_migrations' => false,

Otomatik yükleme devre dışıyken paket loadMigrationsFrom'u hiç çağırmaz; yayınlanan kopyalar tek kaynak haline gelir. Bu bayrağı migration'ları yayınlamadan false yapmayın — aksi hâlde fresh install'da tablolar hiç oluşturulmaz.

Değişmeyen alanlar

Alan Durum
Migration geçmişi (migrations tablosu) Değişmez — basename'ler zaten kayıtlı
Şema — tablo, kolon, index Değişmez — yalnızca dosya taşıması
Framework-default, Passport ve Spatie migration'ları Değişmez — uygulamanızda kalmaya devam eder
Permission key'leri, route isimleri, API zarfı Değişmez

Domain runtime katmanları vendor'a taşındı (Faz 3)

Dört domain modülünün runtime katmanı (Actions, DTOs, Queries, Events, Listeners, Services), stubs/app/Domain/ yerine pakete (src/Domain/, PSR-4 Lvntr\StarterKit\Domain\) taşındı. Tüketici yüzeyi — Controller'lar, FormRequest'ler, Model'ler, Vue sayfaları ve route dosyaları — uygulamanızda kalmaya devam eder ve etkilenmez.

Etkilenen domain'ler: ActivityLog, Logs, Session, Media.

Değişmeyen alanlar

Alan Durum
Controller / provider'larınızdaki App\Domain\<Module>\... import'ları Çalışmaya devam eder — class_alias bunları vendor namespace'ine şeffaf biçimde çözer
Mevcut app/Domain/{ActivityLog,Logs,Session,Media}/ kopyaları Korunur, otomatik silinmez
Controller'lar, FormRequest'ler, Model'ler, Vue sayfaları, route'lar Değişmez — uygulamanızda kalır
App\Models\User Asla vendor'a taşınmaz
Kit migration'ları v13.6.0'da vendor'a taşındı (Faz 4) — yukarıya bakın
Permission key'leri, route isimleri, API zarfı Değişmez

Yeni kurulumlar (v13.6.0+)

sk:install artık app/Domain/ActivityLog/, app/Domain/Logs/, app/Domain/Session/ veya app/Domain/Media/'yı app/'e kopyalamaz. Bu modüllerin runtime sınıfları doğrudan vendor/lvntr/laravel-starter-kit/src/Domain/'dan çalışır. Scaffold controller'larındaki App\Domain\<Module>\... import'ları class_alias aracılığıyla çözülür — üretilen kodda herhangi bir import değişikliği gerekmez.

Mevcut kurulumlar — geçiş adımları

composer update lvntr/laravel-starter-kit
php artisan sk:update

sk:update, artık vendor-resident olan uygulama kopyalarını raporlar (yalnızca bilgilendirici — otomatik silinmez):

 WARN  v13.5.0+: package runtime runs from vendor. The following files still exist in your app:

  • app/Domain/ActivityLog/
  • app/Domain/Logs/
  • app/Domain/Session/
  • app/Domain/Media/

  Deleting these files is optional; vendor copies take precedence.
  See: docs/migrate-existing-project-to-vendor.md

Başka bir adım gerekmez. Uygulamanız değişmeden çalışmaya devam eder.

İsteğe bağlı temizlik — eski uygulama kopyalarını reconcile etme

Bu adım tamamen isteğe bağlıdır, sonraya bırakılabilir.

Bu domain dosyalarını hiç özelleştirmediyseniz, uygulama kopyalarını silin; class_alias aracılığıyla vendor versiyonları devreye girer:

rm -rf app/Domain/ActivityLog/
rm -rf app/Domain/Logs/
rm -rf app/Domain/Session/
rm -rf app/Domain/Media/

Bir domain dosyasını özelleştirdiyseniz, app/Domain/<Module>/ dizininizi koruyun. class_alias guard'ı bunu algılar ve kenara çekilir — özelleştirdiğiniz versiyon vendor kopyasının önünde çalışmayı sürdürür. Değiştirilmemiş dosyaları tek tek silebilir, değiştirilmiş olanları tutabilirsiniz; guard sınıf bazında çalışır.

Kısmi reconcile örneği — özelleştirilmiş bir Action'ı koruyup geri kalanı silme:

# Özelleştirdiğiniz dosya dışındaki her şeyi kaldırın
rm -rf app/Domain/Logs/DTOs/
rm -rf app/Domain/Logs/Events/
rm -rf app/Domain/Logs/Listeners/
rm -rf app/Domain/Logs/Queries/
rm -rf app/Domain/Logs/Services/
# Koru: app/Domain/Logs/Actions/DeleteLogFilesAction.php (özelleştirilmiş)

Session domain — Authenticatable decoupling

PurgeOtherSessionsAction::execute(), artık somut App\Models\User yerine Illuminate\Contracts\Auth\Authenticatable kabul eder. Metot yalnızca Authenticatable kontratının parçası olan getAuthPassword() ve getAuthIdentifier()'ı kullanır. App\Models\User örneği ileten çağrı yerleri etkilenmez — User, Authenticatable'ı uygular.

ProfileController::destroySessions() veya herhangi başka bir çağrı yerinde değişiklik gerekmez.

Logs domain — event listener kaydı

LogFilesDeleted → LogActivityForLogFilesDeleted listener'ı artık vendor StarterKitServiceProvider tarafından kaydedilir (hem event hem listener vendor-resident). Fresh kurulum scaffold'undaki app/Providers/DomainServiceProvider.php bu çifti artık kaydetmez: class_alias'lı bir App\...::class literali derleme zamanında alias adına çözülür ve bu, vendor event'inin çalışma-zamanı sınıfıyla asla eşleşmez — app tarafındaki kayıt "log dosyaları silindi" audit kaydını sessizce düşürürdü. Fresh kurulumda ek işlem gerekmez. Yükseltip kendi app/Domain/Logs/ kopyalarınızı korursanız, mevcut DomainServiceProvider kaydınız onlar için çalışmaya devam eder (vendor kaydı dormant kalır — çift-tetikleme yok); reconcile ederseniz (app kopyalarını silerseniz) vendor kaydı artık-vendor dispatch'i karşılar.


Build scriptleri vendor'a taşındı — consumer wiring güncellenmeli

scripts/sk-theme-build.mjs ve scripts/vite-plugin-sk-theme.mjs artık uygulamanıza kopyalanmıyor. Bu scriptler paket içinde taşınarak vendor/lvntr/laravel-starter-kit/resources/js/theme/ üzerinden çözülüyor — @lvntr/components ve kit composable'larının kullandığı mekanizmanın birebir aynısı.

Yeni kurulumlar (sk:install) etkilenmez — scriptler zaten kopyalanmıyordu.

Mevcut kurulumlar için aşağıdaki adımları manuel uygulamanız gerekir.

Geçiş adımları

  1. Paketi güncelleyin:

    composer update lvntr/laravel-starter-kit
    php artisan sk:update
    

    sk:update, uygulamanızdaki eski scripts/sk-theme-build.mjs ve scripts/vite-plugin-sk-theme.mjs kopyalarını otomatik siler. Bu dosyaları değiştirmediyseniz 3. adıma geçin.

  2. scripts/sk-theme-build.mjs veya scripts/vite-plugin-sk-theme.mjs'yi özelleştirdiyseniz — bu dosyalar artık vendor tarafından yönetilmektedir. Özelleştirmelerinizi başka bir konuma taşımanız gerekir (örn. vendor versiyonunu içe alıp genişleten proje-yerel bir sarmalayıcı script). Resmi bir override hook'u için kit maintainer'ı ile iletişime geçin.

  3. vite.config.ts'i güncelleyin — plugin import'unu eski yerel yoldan vendor yoluna çevirin:

    - import skTheme from './scripts/vite-plugin-sk-theme.mjs';
    + import skTheme from './vendor/lvntr/laravel-starter-kit/resources/js/theme/vite-plugin-sk-theme.mjs';
    

    vite.config.ts'i özelleştirdiyseniz sk:update bu dosyayı otomatik güncellemez. Değişikliği elle uygulayın.

  4. package.json script'lerini güncelleyin — theme:build script'i vendor yolunu göstermeli; dev ve build script'lerindeki açık node scripts/... önek'i artık gerekli değil (skTheme() Vite plugin'i tema üretimini Vite lifecycle içinde garanti ediyor):

    - "theme:build": "node scripts/sk-theme-build.mjs",
    - "dev": "node scripts/sk-theme-build.mjs && vite",
    - "build": "node scripts/sk-theme-build.mjs && vite build && vite build --ssr",
    + "theme:build": "node vendor/lvntr/laravel-starter-kit/resources/js/theme/sk-theme-build.mjs",
    + "dev": "vite",
    + "build": "vite build && vite build --ssr",
    

    skTheme() plugin'i _active.css'i Vite'ın transform pipeline'ı içinde (buildStart / configureServer hook'ları) üretiyor; bu nedenle normal dev ve build çalıştırmalarında açık && öneki artık gerekmez. Tam build yapmadan yalnızca _active.css'i yeniden üretmek için npm run theme:build kullanın.

  5. Yeniden derleyin:

    npm run build
    

Görsel değişiklik yok

Resolver mantığı değişmedi — yalnızca dosya konumu taşındı. Üretilen _active.css çıktısı aynıdır.


Tema dizin yapısı düzleştirildi — BREAKING

themes/ ara dizini hem CSS hem JS tema ağacından kaldırıldı. Bu yollara doğrudan referans veren consumer app'lerin manuel geçiş yapması gerekir.

Eskisi Yenisi
resources/css/theme/themes/main/ resources/css/theme/main/
resources/css/theme/themes/custom/ resources/css/theme/custom/
resources/js/theme/themes/ kaldırıldı — override artık resources/js/theme/custom/preset.ts

Boş placeholder klasörler artık gönderilmiyor.

Geçiş adımları

  1. php artisan sk:update çalıştırın — yeni main/ ağacını resources/css/theme/main/ altına getirir.

  2. Eski klasörleri MANUEL silin — sk:update bunları otomatik silmez:

    rm -rf resources/css/theme/themes/
    rm -rf resources/js/theme/themes/
    
  3. Temayı ve asset'leri yeniden derleyin:

    npm run theme:build && npm run build
    

VITE_SK_THEME=custom kullananlar

Override dosyalarınızı yeni konumlara taşıyın:

Eskisi Yenisi
resources/css/theme/themes/custom/ resources/css/theme/custom/
resources/js/theme/themes/custom/preset.ts resources/js/theme/custom/preset.ts

Taşıma sonrasında npm run theme:build && npm run build ile doğrulayın.

Varsayılan tema — görsel değişiklik yok

Varsayılan VITE_SK_THEME=main (veya değişken tanımlı değil) kullanan projeler için görsel değişiklik yoktur. Geçiş sonrası üretilen _active.css çıktısı aynıdır.


Backend vendor taşıması

Geriye dönük uyum iki yolla garanti edilir:

  • Tam taşınan PHP sınıfları (stub bırakılmayanlar) StarterKitServiceProvider'ın kaydettiği class_alias() ile çözülür — ilgili app/… dosyasını silmediğiniz sürece eski App\… import'larınız çalışmaya devam eder. Özelleştirdiğiniz bir kopyayı koruduğunuzda guard kenara çekilir, böylece sizin sürümünüz kazanmayı sürdürür.
  • Alias'lı PHP sınıfları (scaffold'da ince bir App\… alt sınıfı/trait'i kalanlar) tanıdık App\… import'unu geçerli tutar; gerçek uygulama vendor'dan çalışır.

Vendor'a ne taşındı

Backend (PHP) — tam taşıma (stub yok; class_alias ile çözülür)
Eskiden (App\) Şimdi (vendor)
App\Support\HtmlSanitizer Lvntr\StarterKit\Support\HtmlSanitizer
App\Support\TranslatableQueryHelpers Lvntr\StarterKit\Support\TranslatableQueryHelpers
App\Support\MediaPathGenerator Lvntr\StarterKit\Support\MediaPathGenerator
App\Support\Scramble\ApiResponseExtension Lvntr\StarterKit\Support\Scramble\ApiResponseExtension
App\Http\Middleware\AssignTraceId Lvntr\StarterKit\Http\Middleware\AssignTraceId
App\Http\Middleware\SetLocale Lvntr\StarterKit\Http\Middleware\SetLocale
App\Http\Middleware\ValidateTurnstile Lvntr\StarterKit\Http\Middleware\ValidateTurnstile

AssignTraceId, SetLocale ve ValidateTurnstile, bootstrap/app.php'nizden zaten çağrılan Lvntr\StarterKit\Bootstrap::middleware() tarafından bağlanır; bu yüzden bootstrap değişikliği gerekmez. Yalnızca HandleInertiaRequests scaffold'da kalır — app'e özgü Inertia paylaşılan verisini taşır.

Backend (PHP) — vendor + ince App\ shim (import yolu değişmez)
Sınıf Not
App\Http\Responses\DatatableQueryBuilder vendor builder'ın ince alt sınıfı
App\Rules\HttpsOrLocalhostUrl ince alt sınıf
App\Rules\TurnstileRule ince alt sınıf
Backend (PHP) — trait'ler (doğrudan vendor import, alias yok)

PHP trait'leri class_alias() ile çözülemez, bu yüzden trait'ler sınıfların aldığı şeffaf App\… fallback'ini almaz. Kit'in trait'leri doğrudan vendor namespace'inden import edilir:

Trait Import
HasTranslatableRules use Lvntr\StarterKit\Support\HasTranslatableRules;
HasActivityLogging (v13.5.0'dan beri) use Lvntr\StarterKit\Traits\HasActivityLogging;
HasMediaCollections (v13.5.0'dan beri) use Lvntr\StarterKit\Traits\HasMediaCollections;

Gönderilen model/request scaffold'u bunları zaten Lvntr\StarterKit\…'ten import eder. Projenizde bu trait'lerden birinin yerel kopyası hâlâ varsa (örn. eski sürümden kalma app/Support/HasTranslatableRules.php) ve onu silerseniz, önce App\… trait'ine referans veren her use ifadesini vendor namespace'ine çevirmelisiniz — geri düşülecek bir alias yoktur.

Üçüncü-parti config'ler

Üçüncü-parti config — artık publish edilmiyor

config/activitylog.php, config/inertia.php ve config/media-library.php artık app'inize kopyalanmaz. Kit yalnızca ihtiyaç duyduğu override'ları StarterKitServiceProvider::applyVendorConfigDefaults() ile runtime'da uygular:

  • media-library.path_generator → vendor MediaPathGenerator, ve media-library.media_model → App\Models\Media (model mevcutsa) — File Manager Çöp Kutusu / soft-delete için zorunlu.
  • activitylog.include_soft_deleted_subjects → true
  • inertia.ssr.enabled → env('INERTIA_SSR_ENABLED', false)

Her override, o config'in kendi kopyanızı publish ettiyseniz atlanır — publish, tam kontrol için kaçış yolu olarak kalır. İlgili paketin kendi publish tag'ini kullanın, örn. php artisan vendor:publish --tag=medialibrary-config.

Installer'ın daha önce publish edilmiş config/media-library.php içine App\Support\MediaPathGenerator'ı AST ile enjekte eden davranışı kaldırıldı; path generator artık runtime'da set ediliyor.

Frontend composable'ları

Frontend
  • 15 composable (useApi, useCan, … useUrlTab) vendor'dan çalışır; @/composables/<name> önce local sonra vendor çözülür. useAdminMenu.ts ve index.ts düzenlenebilir stub olarak kalır.
  • TurnstileWidget.vue artık @lvntr/components/ui/TurnstileWidget.vue üzerinden gelir.

Ne değişmez

Alan Durum
Taşınan sınıfların mevcut app/… kopyaları Korunur, silinmez
Kodunuzdaki App\… sınıf import'ları Çalışmaya devam eder (tam taşınanlar için class_alias; DatatableQueryBuilder / Rule'lar için ince shim)
App\… trait import'ları (HasTranslatableRules, HasActivityLogging, HasMediaCollections) Alias yok — vendor namespace'ini kullanın (yukarıdaki trait notuna bakın)
Daha önce publish ettiğiniz config/activitylog.php / inertia.php / media-library.php Korunur; sizin dosyanız kazanır (runtime override atlanır)
@/composables/<name> import yolları Değişmez
Route adları, permission anahtarları, API response zarfı Değişmez
Migration geçmişi "Nothing to migrate"

İsteğe bağlı temizlik

Bu adımlar tamamen isteğe bağlıdır ve sonra da yapılabilir.

Tam taşınan PHP sınıfları — hiç özelleştirmediyseniz, yetim kalan dosyaları silin ki vendor sürümleri (class_alias ile) devralsın:

rm -f app/Support/HtmlSanitizer.php \
      app/Support/TranslatableQueryHelpers.php \
      app/Support/MediaPathGenerator.php \
      app/Support/Scramble/ApiResponseExtension.php \
      app/Http/Middleware/AssignTraceId.php \
      app/Http/Middleware/SetLocale.php \
      app/Http/Middleware/ValidateTurnstile.php

Daha önce publish edilmiş config/media-library.php içinde path_generator değeri App\Support\MediaPathGenerator ise alias ile çalışmaya devam eder. Tamamen runtime varsayılanına geçmek için config/media-library.php'yi silin — kit o zaman vendor path generator'ı ve App\Models\Media'yı zorunlu kılar.

Shim'li PHP sınıfları (DatatableQueryBuilder, HttpsOrLocalhostUrl, TurnstileRule) — ince App\ alt sınıfını yerinde bırakın; desteklenen override noktası budur. Yalnızca import'larınızı doğrudan Lvntr\StarterKit\…'e geçirirseniz silin.

Trait'ler (HasTranslatableRules, HasActivityLogging, HasMediaCollections) — yerel kopyanız varsa, önce her use ifadesini vendor namespace'ine çevirin, sonra yerel dosyayı silin. Import güncellemesini atlamak sınıfı kırar; çünkü trait'lerin class_alias fallback'i yoktur.

Taşınan bir sınıfı özelleştirdiyseniz, app/… dosyanızı koruyun: class_alias guard'ı (tam taşınan sınıflar için) onu algılar ve kenara çekilir, böylece sizin sürümünüz kazanmayı sürdürür. Shim'li sınıflar için shim'in kendisini özelleştirin.

sk:update çıktısı

sk:update, app'inizde hâlâ duran taşınmış dosyaları raporlar (yalnızca bilgilendirme — asla otomatik silinmez):

v13.5.0+: package runtime runs from vendor. The following files still exist in your app:

  • app/Http/Middleware/AssignTraceId.php
  • app/Http/Middleware/SetLocale.php
  • app/Http/Middleware/ValidateTurnstile.php
  • app/Support/HtmlSanitizer.php
  • app/Support/TranslatableQueryHelpers.php
  • app/Support/MediaPathGenerator.php
  • app/Support/HasTranslatableRules.php
  • app/Support/Scramble/ApiResponseExtension.php
  • config/media-library.php
  • config/activitylog.php
  • config/inertia.php

(v13.5.0 vendor-resident dosyaları — app/Domain/FileManager/, app/Domain/Shared/ vb. — hâlâ duruyorsa listede ayrıca görünür.)

Yeni kurulum (v13.6.0+)

Sıfır bir sk:install artık bu yardımcı sınıfları, middleware'leri, trait'leri veya üç üçüncü-parti config'i app/ / config/'e kopyalamaz. Bunlar vendor/lvntr/laravel-starter-kit/src/ ile kit'in runtime config override'larından çalışır. Scaffold, üretilen domain kodunun tanıdık import'larını koruması için DatatableQueryBuilder ve iki validation Rule için ince App\ shim'lerini hâlâ gönderir; trait yardımcıları (HasTranslatableRules) doğrudan vendor namespace'inden import edilir.


Sayfa-geçiş yükleme overlay'i (opt-in)

Bu sürüm SkPageLoader'ı ships eder — @lvntr/components/ui/SkPageLoader.vue konumunda animasyonlu tam-ekran sayfa-geçiş loader'ı; usePageLoading composable'ı (anti-flicker gecikmeli Inertia router olayları) tarafından sürülür ve theme/main/components/page-loader.css ile temalanır. Hem CSS slot'u hem composable sk:update ile gelir; CSS üretilen _active.css'e zaten import edilir. Marka sözcüğü harf-harf animasyon yapar ve aktif accent + temayı izler (bkz. theme.tr.md — Accent renk sistemi).

Opt-in'dir — gelen scaffold onu mount etmez. v13.6.0 scaffold'undaki hiçbir layout <SkPageLoader/> render etmez; siz wire edene kadar loader uykuda kalır (CSS'i de etkisiz). Upgrade'de otomatik davranış değişikliği yoktur.

Açmak için bileşeni AdminLayout.vue'nun overlays slot'una, diğer global overlay'lerin yanına ekleyin:

import SkPageLoader from '@lvntr/components/ui/SkPageLoader.vue';

<template #overlays>
    <ConfirmDialogComponent />
    <ToastComponent />
    <AppDialog />
    <ImageLightbox />
    <SkPageLoader :delay="250" />
</template>

Loader animasyonlu sözcüğü için sk-layout.loading çeviri anahtarını okur ve prefers-reduced-motion'ı dikkate alır. Kapatmak için satırı kaldırın.


Tema / CSS / layout reorganizasyonu

Admin panel CSS'i ve layout kabuğu yeniden düzenlendi. Görsel çıktı değişmez — varsayılan build (VITE_SK_THEME=main) v13.5.11 ile byte-identical'dır. Tüm layout ve bileşen class adları, token değerleri ve DOM yapısı korunur. Değişen yalnızca dosya düzenidir: monolitik bir _admin.scss ve dağınık _*.scss partial'larındaki stiller artık yapılandırılmış bir themes/main/ dizin ağacında yaşar; layout kabuğu ise yeniden kullanılabilir bir AppShell.vue + ince bir AdminLayout.vue kompozisyonuna bölünür.

Yeni opt-in tema-override sistemi (themes/custom/), herhangi bir CSS slot'unu build zamanında base temaya veya Vue bileşenlerine dokunmadan değiştirmenizi sağlar.

Taşınan dosyalar (CSS)

Kaldırılan Yerine gelen
resources/css/theme/_admin.scss themes/main/layout/{shell,sidebar,header,page-header,footer}.css
resources/css/theme/_datatable.scss themes/main/components/datatable.css
resources/css/theme/_formbuilder.scss themes/main/components/formbuilder.css
resources/css/theme/_dialog.scss themes/main/components/dialog.css
resources/css/theme/_toast.scss themes/main/components/toast.css
resources/css/theme/_tag.scss themes/main/components/tag.css
resources/css/theme/_card.scss themes/main/components/card.css
resources/css/theme/_editor.scss themes/main/components/editor.css
resources/css/theme/_tabs.scss themes/main/components/tabs.css
resources/css/theme/_menus.scss themes/main/components/menus.css
resources/css/theme/_navigation.scss themes/main/components/navigation.css
resources/css/theme/_confirm.scss themes/main/components/confirm.css
resources/css/theme/_primevue.scss themes/main/components/primevue.css
_base.scss'teki :root / .dark blokları themes/main/tokens.css
theme.css (açık slot import'ları) theme.css (tek @import './_active.css')

Taşınan dosyalar (layout)

resources/js/layouts/AdminLayout.vue artık yeni resources/js/layouts/AppShell.vue etrafında ince bir kompozisyondur. Dış prop/slot kontratı (title, subtitle, backUrl, default, page-actions) birebir aynıdır — hiçbir sayfanın import'unu veya template'ini değiştirmesi gerekmez.

Yeni dosyalar

Dosya Amaç
resources/js/layouts/AppShell.vue Yeniden kullanılabilir yapısal kabuk (sidebar durumu, responsive margin'ler, adlandırılmış bölgeler)
resources/css/theme/themes/main/ Dahili ana tema (tüm CSS slot'ları için kaynak)
resources/css/theme/themes/custom/ Boş CSS override tema iskeleti (bkz. o dizindeki themes/custom/README.md)
scripts/sk-theme-build.mjs CSS tema resolver'ı — _active.css'i üretir; dev ve build tarafından açıkça çağrılır
resources/js/theme/themes/custom/ Boş PrimeVue preset override iskeleti — .gitkeep ve override reçetesini açıklayan README.md ile birlikte gelir
scripts/vite-plugin-sk-theme.mjs Vite plugin'i — Vite'ın lifecycle'ı içinde _active.css'i üretir ve @/theme/preset import'unu build zamanında aktif temanın preset'ine çözümler

Üretilen artefakt

resources/css/theme/_active.css, scripts/sk-theme-build.mjs tarafından üretilir:

  • .gitignore'da listelenmiştir — asla commit edilmez.
  • Her npm run dev ve npm run build'de yeniden üretilir — resolver her iki script'e açıkça zincirlenir (npm lifecycle hook kullanılmaz; bu nedenle ignore-scripts=true altında da doğru çalışır).
  • sk:update tarafından hash-takip edilmez.

.env.example — yeni anahtar

VITE_SK_THEME=main

Henüz yoksa bu satırı .env ve .env.example dosyalarınıza ekleyin. Varsayılan main'dir; değişkeni atlamak aynı etkiyi verir.

package.json — resolver dev ve build'e zincirlendi

Resolver'ın açıkça zincirin bir parçası olarak çalışması için dev, build ve theme:build script'leri güncellendi:

"theme:build": "node scripts/sk-theme-build.mjs",
"dev": "node scripts/sk-theme-build.mjs && vite",
"build": "node scripts/sk-theme-build.mjs && vite build && vite build --ssr",

Kendi package.json'ınızı yönetiyorsanız dev ve build'i bu kalıba göre güncelleyin. Resolver açık bir && adımı olmalıdır — predev / prebuild lifecycle hook'larını kullanmayın; npm, ignore-scripts=true ayarı aktifken bu hook'ları sessizce atlar (consumer projelerinde ve CI'da yaygın bir güvenlik ayarı), bu da _active.css'in oluşturulmamasına ve build'in başarısız olmasına neden olur. npm run theme:build ise tam build yapmadan dosyayı isteğe bağlı oluşturmak için kullanılabilir.

_admin.scss veya herhangi bir _*.scss partial'ını özelleştirdiyseniz

  1. php artisan sk:update çalıştırın — taşınan dosyalar için hash farkı raporlanır.
  2. Özelleştirmelerinizi yukarıdaki tabloya göre ilgili themes/main/ dosyasına kopyalayın.
  3. Değişiklikleriniz kapsamlıysa themes/custom/'a almayı düşünün (bkz. docs/theme.tr.md — özel tema reçetesi).
  4. Doğrulamak için npm run build çalıştırın.

AdminLayout.vue'yu özelleştirdiyseniz

sk:update hash farkı raporlar. Yeni dosya AppShell etrafında ince bir kompozisyondur. Özelleştirmelerinizi yeni sürüme uygulayın — dış kontrat (prop'lar, slot'lar) değişmediğinden sayfa düzeyindeki template'lerin düzenlenmesi gerekmez.

PrimeVue preset — geçiş adımı gerekmez

PrimeVue preset resolver tamamen eklemeli ve geriye dönük uyumludur:

  • resources/js/theme/preset.ts yerinde kalır — kit onu asla taşımaz.
  • app.ts, @/theme/preset'i değişiklik olmadan import etmeye devam eder.
  • VITE_SK_THEME=main (veya değişken tanımlı değilse), build @/theme/preset'i taban preset.ts'e çözümler — önceki sürümle byte-identical davranış.
  • themes/custom/preset.ts override'ı yalnızca VITE_SK_THEME=custom ve dosyayı oluşturduğunuzda devreye girer. Dosya yoksa tabana düşer.

preset.ts'i özelleştirmiş mevcut consumer'lar özelleştirilmiş dosyalarını kullanmaya devam eder. Resolver buna müdahale etmez. Custom temaya kendi PrimeVue paletini vermek için bkz. docs/theme.tr.md — PrimeVue preset katmanı.

Görsel değişiklik yok

Bu yeniden düzenleme yalnızca yapısaldır. VITE_SK_THEME=main (varsayılan) ile üretilen _active.css, önceki theme.css manifestiyle birebir aynı CSS kurallarını aynı sırayla import eder. Token değerleri (aydınlık ve karanlık), class adları ve DOM yapısı aynıdır.

Daha önce resolver'ın dışında sabit import olan dört CSS dosyası (fonts.css, _base.scss, _auth.scss, utilities.css) artık themes/main/ altında yaşıyor ve resolver tarafından normal slot'lar gibi emit ediliyor. Cascade sırası değişmedi. Varsayılan build v13.5.11 ile byte-identical'dır.

Geçiş adımı gerekmez. Manuel adımlar yalnızca taşınan dosyaları elle düzenlediyseniz gereklidir.

Taşınan dosyalar

Kaldırılan Yerine gelen
resources/css/theme/fonts.css themes/main/fonts.css
resources/css/theme/_base.scss themes/main/_base.scss
resources/css/theme/_auth.scss themes/main/_auth.scss
resources/css/theme/utilities.css themes/main/utilities.css

Değişen dosyalar (import temizliği)

Dosya Değişiklik
theme/theme.css Sabit _auth.scss import'u kaldırıldı — resolver emit ediyor. Artık yalnızca @import './_active.css' içeriyor.
app.css Sabit utilities.css tail import'u kaldırıldı — resolver son slot olarak emit ediyor.

fonts.css, _base.scss, _auth.scss veya utilities.css'i özelleştirdiyseniz

  1. php artisan sk:update çalıştırın — taşınan dosyalar için hash farkı raporlanır.
  2. Özelleştirmelerinizi yukarıdaki tabloya göre ilgili themes/main/ dosyasına kopyalayın.
  3. Değişiklikleriniz temaya özgüyse themes/custom/ altına almayı düşünün (örn. themes/custom/fonts.css). Bkz. docs/theme.tr.md — Tam slot referansı.
  4. Doğrulamak için npm run build çalıştırın.

Öksüz kalan dosyalar (silinebilir)

sk:update yeni themes/main/ dosyalarını ekler ama diskte zaten bulunan eski düz-yol kopyalarını silmez. Yükseltmeden sonra şu dört dosya artık hiçbir yerden import edilmez ve ağacı temiz tutmak için silinebilir — yerinde bırakmak zararsızdır (hiçbir şey import etmez):

  • resources/css/theme/fonts.css
  • resources/css/theme/_base.scss
  • resources/css/theme/_auth.scss
  • resources/css/theme/utilities.css

Permission direktif plugin'i → vendor

v-can / v-role izin direktif plugin'i (resources/js/plugins/permission.ts) artık varsayılan olarak vendor paketinden çözülüyor; kit composable'larının çalışma şeklini aynalıyor. app.ts'teki @/plugins/permission import'u değişmedi; yerel dosya yoksa vendor kopyasına düşer. Davranış değişikliği yok — direktifler aynı.

Geçiş gerekmez. Mevcut projeler yerel resources/js/plugins/permission.ts'lerini korur; bu kopya vendor sürümünü gölgelemeye devam eder, yani hiçbir şey kırılmaz.

Ne değişti

Dosya Değişiklik
resources/js/plugins/permission.ts Artık vendor'dan sağlanıyor. Ölü useCan() export'u kaldırıldı (@/composables/useCan kullanın); yalnızca PermissionPlugin (v-can / v-role) shipping.
vite.config.ts Yeni @/plugins/* resolver'ı — önce yerel kopya, sonra vendor fallback — @/composables/*'ı aynalar.
tsconfig.json Yeni @/plugins/* path eşlemesi.

Yerel kopyanız artık isteğe bağlı

sk:update resources/js/plugins/permission.ts'i artık stub olarak göndermez, ama projenizdeki mevcut kopyayı da silmez. O yerel kopya çalışmaya devam eder — vendor sürümünü gölgeler. Şunları yapabilirsiniz:

  • Vendor yönetimli sürümü almak için silin (hiç düzenlemediyseniz önerilir): rm resources/js/plugins/permission.ts.
  • Kendi kopyanıza sabitlenmek veya direktifleri özelleştirmek için tutun.

Sonradan düzenlenebilir bir kopya oluşturmak için php artisan sk:publish --tag=plugins çalıştırın — vendor sürümünü tekrar gölgeler.

Davranış değişikliği yok

Direktifler aynı; yalnızca çözümleri taşındı. v-can / v-role tıpkı önceki gibi davranır ve app.ts'in düzenlenmesi gerekmez.


Güvenlik Ayarları yeniden tasarımı — parola politikası uygulaması

Bu sürüm Ayarlar → Güvenlik sekmesini yeniden tasarlar ve parola kuralları ile parola geçerlilik süresinin sunucu tarafında tam olarak uygulanmasını sağlar.

Yeni migration — users.password_changed_at

users tablosuna nullable bir timestamp kolonu eklenir. Migration sırasında mevcut satırlar now() ile geri dolduğundan, deploy sonrasında hiçbir kullanıcı aniden süresi dolmuş kabul edilmez.

php artisan migrate

Yeni auth.* ayar anahtarları

auth grubuna altı yeni anahtar eklenir. Tümünün geri uyumlu fallback'leri vardır; dolayısıyla mevcut kurulumlar seeder çalıştırmadan yükseltme yaptığında etkilenmez.

Anahtar Runtime fallback (anahtar DB'de yokken) Seeder (yeni kurulum)
auth.login_throttle '1' (throttle zaten aktif) '1'
auth.password_min_length 10 '10'
auth.password_expiry_days 0 (sınırsız) '0'
auth.password_require_mixed_case '1' '1'
auth.password_require_numbers '1' '1'
auth.password_require_symbols '1' '1'

Mevcut kurulumlar: sk:update, güncellenmiş _03_SettingSeeder.php dosyasını teslim eder. Seeding isteğe bağlıdır; seeding çalıştırılmadan yukarıdaki runtime fallback'ler kullanılır — davranış değişmez (fallback'ler özellik öncesi sertleştirilmiş baseline'a eşittir).

Yeni kurulumlar: önerilen default'ları uygulamak için seeder'ı sk:install kapsamında çalıştırın:

php artisan db:seed --class=_03_SettingSeeder

Giriş denemesi limiti toggle'ı

auth.login_throttle = '0' runtime'da Fortify giriş rate limiter'ını devre dışı bırakır. Default değer '1''dir (throttle aktif). Throttle'ı kapatmak bilinçli bir güvenlik düşürümüdür; bu ayar yalnızca yöneticilere açıktır.

Parola politikası uygulaması

Parola politikası ayarları yapılandırıldığında PasswordValidationRules trait, bunları her yeni parolaya uygular — kayıt, parola sıfırlama, parola onayı ve profil güncellemesi. Kurallar yalnızca yeni gönderilen parolalara uygulanır; mevcut saklı parolalar geçersiz olmaz.

Ayar Aktifken etkisi
password_min_length Password::min(n) uygular
password_require_mixed_case ->mixedCase() uygular
password_require_numbers ->numbers() uygular
password_require_symbols ->symbols() uygular

Politika yapılandırılmadığında (tüm fallback'ler) davranış, önceki Password::default() kurulumuna eşdeğerdir.

Parola geçerlilik süresi middleware'i (EnsurePasswordNotExpired)

auth.password_expiry_days > 0 olduğunda, password_changed_at değeri yapılandırılan gün sayısından daha eski olan kimlik doğrulanmış kullanıcılar, parolalarını güncelleyene kadar adanmış, guest tarzı bir parola-süresi-doldu ekranına (Auth/PasswordExpired.vue, password.expired rotası) yönlendirilir. Ekran giriş / parola sıfırlama layout'unu yansıtır — sidebar veya panel çerçevesi yoktur — ve parola tekrar güncel olduğunda kullanıcıyı dashboard'a geri gönderir.

Muaf rotalar (redirect döngüsü oluşamaz):

  • parola-süresi-doldu sayfası (password.expired, redirect hedefi)
  • çıkış
  • iki faktör challenge
  • Fortify parola uç noktaları

password_changed_at = null muaf tutulur (migration geri doldurmadan sonra pratikte oluşmaz).

Middleware, stub'ın routes/web.php dosyasındaki kimlik doğrulamalı panel route grubu aracılığıyla web + auth middleware grubuna kaydedilir. routes/web.php dosyasını özelleştirdiyseniz (yani sk:update bu dosyaya dokunmuyorsa), EnsurePasswordNotExpired middleware'ini auth grubuna manuel olarak eklemeniz gerekir:

use App\Http\Middleware\EnsurePasswordNotExpired;

Route::middleware(['auth', 'verified', EnsurePasswordNotExpired::class])->group(function () {
    // kimlik doğrulamalı rotalarınız
});

SecurityTab özelleştirmesi

resources/js/pages/Admin/Settings/components/SecurityTab.vue dosyasını özelleştirdiyseniz, sk:update --dry-run çalıştırarak diff'i görün ve değişikliklerinizi yeni üç alt sekme yapısıyla birleştirin. Güncelleme hash uyuşmazlığı olarak işaretlenecektir — manuel olarak uygulayın.

Değişmeyen alanlar

Alan Durum
Mevcut auth.* ayar anahtarları (registration, email_verification, password_reset, two_factor) Değişmez
UpdateAuthSettingsRequest — eski dört alanlı POST Kabul edilmeye devam eder; yeni alanlar sometimes
Giriş throttle default'u Aktif kalmaya devam eder ('1') — yükseltme sonrası davranış değişmez
Mevcut kullanıcıların parolaları Politika değişikliğiyle geçersiz olmaz
API yanıt zarfı Değişmez

v13.5.0 → v13.5.3

Özet

Bu sürümde sk:doctor / System Health paneli, File Manager için İmzalı Paylaşım Bağlantıları (Signed Share Link), Bulk Action API sertleştirmesi ve API İstemci UI eklendi. Yeni migration, config key ve permission adımları zorunludur.

Yükseltme Adımları

1. Paketi güncelleyin:

composer update lvntr/laravel-starter-kit

2. Yeni migration'ları yayınlayın ve çalıştırın:

php artisan vendor:publish --tag=starter-kit-migrations
php artisan migrate

Yeni migration: file_manager_share_revocations tablosu (İmzalı Paylaşım Bağlantısı iptali için zorunlu).

3. File Manager config'ini güncelleyin (yeni share.* key'leri):

php artisan vendor:publish --tag=starter-kit-config --force

config/file-manager.php dosyasına şu key'ler eklenir: share.enabled, share.default_ttl_hours, share.max_ttl_hours, share.allow_revoke. Mevcut key'ler etkilenmez.

4. Yeni stub'ları yayınlayın (DİKKAT: özelleştirilmiş stub'lar override edilir, önce diff alın):

php artisan vendor:publish --tag=starter-kit-stubs --force

5. Yeni izinleri seed'leyin ve cache'i temizleyin:

php artisan db:seed --class=PermissionResourcesSeeder
php artisan permission:cache-reset

Yeni izinler: system.health.view, share-media, revoke-share-media, api-clients.create, api-clients.read, api-clients.update, api-clients.delete, api-tokens.create, api-tokens.read, api-tokens.delete

Davranış Değişiklikleri

  • Passport istemci UI — confidential=false olan authorization-code client'ları artık UI üzerinden oluşturulamaz. Mevcut DB kayıtları etkilenmez.
  • Personal Access Token mint — user_id body alanı kaldırıldı. Başkası adına PAT oluşturmak için artisan komutunu kullanın.
  • AppServiceProvider stub — varsa duplicate Passport scope / Gate::before bloğunu kaldırın; StarterKitServiceProvider bunları kaydetmeye devam eder.
  • BulkActionRequest — ID'ler artık string|min:1|max:64 kuralıyla doğrulanıyor. Mevcut integer-only bulk action'lar etkilenmez.

v13.4.x → v13.5.0

Özet

Bu sürümde paket runtime vendor'a taşındı. app/ dizinindeki mevcut dosyalarınız değişmez; olduğu gibi çalışmaya devam eder. composer update tek zorunlu adımdır.

Yükseltme Adımları

composer update lvntr/laravel-starter-kit
php artisan migrate

php artisan migrate komutu "Nothing to migrate" döner çünkü mevcut migration history'niz bu sürümün vendor migration dosyalarıyla birebir eşleşiyor.

Opsiyonel adımlar

# Wayfinder typed route dosyalarını yenile (diff beklenmez)
php artisan wayfinder:generate

# Stub güncellemelerini kontrol et (hash değişmişse bildirir, zorlamaz)
php artisan sk:update --dry-run

Ne Değişmez

Alan Durum
app/Domain/FileManager/ dosyaları Korunur, silinmez
app/Domain/Shared/ dosyaları Korunur, silinmez
app/Traits/HasActivityLogging.php Korunur
app/Traits/HasMediaCollections.php Korunur
app/Helpers/sk-helpers.php Korunur, fonksiyonlarınız baskın kalır
app/Http/Responses/ApiResponse.php Korunur
app/Http/Middleware/CheckResourcePermission.php Korunur
Route isimleri (file-manager.*) 19 route adı AYNEN
Migration history "Nothing to migrate"
Config key'leri (starter-kit.*, file-manager.*) Mevcut key'ler korundu
Frontend @lvntr alias Dokunulmadı
Permission key'leri (files.read, files.update, vb.) AYNEN
API response envelope (success, status, message, data) AYNEN

Opsiyonel Cleanup

Backend dosyaları (vendor'a taşıma)

app/Domain/FileManager/, app/Domain/Shared/ gibi dosyalar artık vendor'dan da çalışıyor. Eğer bu dosyaları uygulamanızdan kaldırıp vendor versiyonunu kullanmak istiyorsanız adım adım rehber için bakın:

docs_project/migrate-existing-project-to-vendor.tr.md (uygulama worktree'sinde)

Bu adım tamamen isteğe bağlıdır ve hemen yapılması gerekmez.

Frontend (vendor symlink'e geçiş)

App'te resources/js/components/Lvntr-Starter-Kit/ klasörü hâlâ duruyorsa ve özel customization yoksa, vendor frontend'ine geçebilirsiniz:

  1. Vite alias — vite.config.ts içinde @lvntr/components alias'ını vendor path'e yönlendirin:

    '@lvntr/components': path.resolve(__dirname, 'vendor/lvntr/laravel-starter-kit/resources/js/components/Lvntr-Starter-Kit'),
    

    Components({ dirs }) plugin array'ine vendor path ekleyin:

    dirs: [
      'resources/js/components',
      'vendor/lvntr/laravel-starter-kit/resources/js/components/Lvntr-Starter-Kit',
    ],
    

    preserveSymlinks: true olduğundan emin olun.

  2. App kopyasını silin:

    rm -rf resources/js/components/Lvntr-Starter-Kit
    
  3. Build smoke:

    npm run build
    

    Exit 0 olmalı.

Customize edilmiş component'iniz varsa silmeyin; kendi resources/js/components/<X> altında app-specific bileşenlerinizi tutarken vendor lib'i import edebilirsiniz.

sk:sync deprecation

php artisan sk:sync deprecated oldu. Composer path repository (symlink) workflow'u kullananlar için gerekmiyordu zaten. --force ile eski davranış korunur ama önerilmez.

sk:update Çıktısı

Bu sürümden itibaren sk:update, vendor'a taşınan runtime dosyalar için kopyalama yapmaz. Çıktıda şuna benzer bir bilgi mesajı görürsünüz:

[INFO] v13.5.0+: Aşağıdaki dosyalar artık vendor'da çalışıyor.
       Silmek opsiyonel:
         app/Domain/FileManager/
         app/Domain/Shared/{Actions,Contracts,DTOs,Pipelines}
         app/Traits/HasActivityLogging.php
         app/Traits/HasMediaCollections.php
         app/Helpers/sk-helpers.php
         app/Http/Responses/ApiResponse.php
         app/Http/Middleware/CheckResourcePermission.php
         app/Http/Middleware/SecurityHeaders.php
         app/Exceptions/ApiException.php
         app/Exceptions/ApiExceptionHandler.php
         app/Http/Controllers/FileManagerController.php
         app/Http/Requests/FileManager/
         app/Console/Commands/PurgeFileManagerTrash.php

Hash takipli stub'lar (auth/layout/user/rol/ayar/config) için mevcut diff/bildirim davranışı korundu.

Yeni Install (v13.5.0+)

Yeni bir projede php artisan sk:install koştuğunuzda app/Domain/FileManager/, app/Domain/Shared/, app/Traits/, app/Helpers/sk-helpers.php, app/Http/Responses/ApiResponse.php, app/Http/Middleware/CheckResourcePermission.php dosyaları artık app/ dizinine kopyalanmıyor. Bu modüller doğrudan vendor/lvntr/laravel-starter-kit/src/ altından çalışıyor.

Uygulamaya publish edilen dosyalar: auth/layout Vue bileşenleri, User/Role/Setting domain iskeleti, config dosyaları, tek satır route stub'ları.


v13.4.8 → v13.4.9

Bkz. CHANGELOG.md.

Kısa geçiş:

composer update lvntr/laravel-starter-kit
php artisan sk:update
php artisan migrate
npm install
npm run build

v13.4.x → v13.4.10

Bkz. CHANGELOG.md.

composer update lvntr/laravel-starter-kit
php artisan sk:update
php artisan migrate
npm install
npm run build

13.4.0 → 13.4.1 — API response sertleştirme + Postman/Apidog sync + OAuth UUID fix

Özet: Bu patch, baştan sona elden geçirilen API response zarfını (trace-id pipeline, merkezi exception handler, leak kapatan controller patch'leri) iki yeni API client entegrasyonu (Postman + Apidog sync) ve iki adet kurulum fix'i (OAuth migration'ları UUID-uyumlu, site:install artık Passport personal access client'ı otomatik oluşturuyor) ile birleştiriyor. Çoğu değişiklik additive (yeni alan/header, yeni admin butonları) ama üç adet API-response davranışsal breaking noktası UI toast metinlerini veya katı client schema'larını etkileyebilir: abort() raw mesaj whitelist'i + ModelNotFoundException mesaj formatı + Api/AuthController ham User → UserResource geçişi.

0. Kimi etkiler?

Kullanıcı Ne yapmalı
Paketi yeni kuranlar (composer create-project + sk:install) Hiçbir şey — stubs zaten 13.4.1 sürümünde.
sk:update düzenli çalıştıranlar composer update + php artisan sk:update. ApiResponse, ApiExceptionHandler, AssignTraceId, sk-helpers.php otomatik taşınır; controller'lar manuel (Adım 4).
Custom controller'lara sahip olanlar Adım 4'teki patch'leri elle uygulayın — özellikle catch (LogicException $e) → throw ApiException::... pattern dönüşümü.
Sadece paket src/ kullananlar (publish yapmadı) composer update lvntr/laravel-starter-kit yeter; Bootstrap otomatik register ediyor.
Kendi app/Http/Middleware/AssignTraceId.php yazmış olanlar Sınıf adı çakışır; paket stub'ını tercih edin veya kendi class'ınızı yeniden adlandırın.

1. Upgrade öncesi hazırlık

  1. Branch + yedek: git checkout -b upgrade/v13.4.1 && git push
  2. Frontend/mobile takım: API response formatındaki additive alanları (trace_id body, X-Request-ID header, X-Correlation-ID echo, 429 Retry-After) onlara haber verin — strict şema kullananlar eklesinler.
  3. QA: Hata mesajları UI'da toast olarak gösteriliyorsa, Adım 2'deki davranışsal breaking'leri kısa bir QA pass'inden geçirin (abort() mesajları, model-not-found mesaj formatı, auth me/login response şekli).
  4. Ortam kontrolü: composer test + npm run build mevcut sürümde geçiyor mu?

2. Davranışsal breaking değişiklikler

Status kodları değişmedi; zarf alan listesi değişmedi; sadece message metni ve auth payload içindeki data.user alan listesi etkilenebilir.

2.1 abort($code, 'custom message') artık mesajı dışa sızdırmıyor

- // Eskiden: body.message = "SQL error: table users missing col xyz"
- abort(400, 'SQL error: table users missing col xyz');
+ // Artık: body.message = "Bad request."  (iç detay düşer)
+ abort(400, 'SQL error: ...');   // Bu mesaj artık client'a gitmez.

Neden: HttpExceptionInterface dalı artık $e->getMessage() yerine sabit defaultMessageForStatus() kullanıyor (K3). İç mesajlar APP_DEBUG=true iken debug.message alanına düşer.

Geçiş yolu: Client'a mesaj göstermek istediğiniz kontrollü durumlarda:

// Eski
abort(400, 'Invalid coupon code.');

// Yeni (handler'dan geçer, trace_id + correlation headers eklenir)
throw \App\Exceptions\ApiException::badRequest('Invalid coupon code.');

2.2 ModelNotFoundException mesajı model adını içeriyor

- body.message: "The requested resource was not found."
+ body.message: "User not found."          // veya Role, Product, …

Neden: ApiExceptionHandler::modelNotFoundMessage artık class_basename($e->getModel()) ile resolve ediyor (K4 — önceki AGENTS.md vaadini karşılıyor). Güvenlik etkisi yok — model sınıf adı zaten URL'den tahmin edilebilir.

Geçiş yolu: Frontend'de message string'ine karşı eşleşme yapan kod varsa regex'i gevşetin (/(not found|bulunamadı)/i gibi) veya status kodu (404) üzerinden dallanın.

2.3 Api/AuthController ham User → UserResource

  POST /api/v1/auth/login (default kind)
  POST /api/v1/auth/register (no-verification path)
  POST /api/v1/auth/two-factor-challenge
  GET  /api/v1/auth/me

- data.user: {
-     id: 1, first_name: "...", email: "...",
-     status: "active", email_verified_at: "...",
-     two_factor_confirmed_at: null,
-     avatar_url: "...", created_at: "...", updated_at: "..."
- }
+ data.user: <UserResource::toArray() çıktısı, app/Http/Resources/Admin/User/UserResource.php>

Neden: Ham Eloquent serializasyonu $hidden'a güvenmek zorundaydı; gelecekte eklenen hassas bir alan unutulsa sessizce sızardı. UserResource artık kontrat — hangi alan client'a gidiyor açıkça yazılı.

Geçiş yolu: UserResource'un döndürdüğü alan listesini kontrol edin (app/Http/Resources/Admin/User/UserResource.php). Ham model'in vardı ama Resource'ta olmayan alana bağımlıysanız, Resource'a ekleyin veya kendi AuthUserResource yazıp AuthController'da kullanın.

3. Paket güncellemesi

composer update lvntr/laravel-starter-kit --with-all-dependencies
php artisan sk:update              # Otomatik: ApiResponse + ApiExceptionHandler + sk-helpers + AssignTraceId
npm install                         # Değişmedi ama alışkanlık

sk:update çıktısı şu dosyaları otomatik günceller:

  • app/Http/Responses/ApiResponse.php
  • app/Exceptions/ApiExceptionHandler.php
  • app/Helpers/sk-helpers.php
  • app/Http/Middleware/AssignTraceId.php (yeni — dosya yoksa oluşturulur)
  • app/Http/Middleware/SecurityHeaders.php (dokunulmadı ama listede)

Önemli: AssignTraceId.php dosyası sk:update sonrası mevcut değilse, paket Bootstrap::middleware() App\Http\Middleware\AssignTraceId sınıfına referans veriyor ve ilk API request'te ClassNotFoundException atar. sk:update başarılı olduysa sorun yok; emin olmak için: ls app/Http/Middleware/AssignTraceId.php.

4. Manuel controller patch'leri (publish edilmiş custom'lar için)

sk:update controller'ları otomatik güncellemez — birçok projede custom metodlar eklenmiş oluyor. Aşağıdaki 11 leak pattern'ini elle temizleyin. Pattern evrensel:

- catch (LogicException $e) {
-     return to_api(null, $e->getMessage(), 422);
- }
+ catch (LogicException $e) {
+     throw \App\Exceptions\ApiException::unprocessable($e->getMessage());
+ }

Etkilenen dosyalar:

Dosya Satır / metot
app/Http/Controllers/FileManagerController.php bulkDelete, createFolder, renameFolder, moveItem, deleteFolder, upload, deleteFile — 7 adet
app/Http/Controllers/Api/UserController.php destroy — to_api(null, 'Unauthenticated.', 401) → throw ApiException::unauthorized(); to_api(null, $e->getMessage(), 400) → throw ApiException::badRequest(...)
app/Http/Controllers/Api/Auth/AuthController.php login — to_api(null, 'Invalid email or password.', 401) → throw ApiException::unauthorized(...); twoFactorChallenge — aynısı "Invalid or expired two-factor code." için

Her controller'ın başına use App\Exceptions\ApiException; eklemeyi unutmayın. Son olarak destroy'dakine benzer yerlerde return to_api(status: 204); try bloğunun dışına taşınır (Adım 2'deki çıkış akışı değişimi):

- try {
-     $action->execute($user, (string) $performedById);
-     return to_api(status: 204);
- } catch (\LogicException $e) {
-     return to_api(null, $e->getMessage(), 400);
- }
+ try {
+     $action->execute($user, (string) $performedById);
+ } catch (\LogicException $e) {
+     throw ApiException::badRequest($e->getMessage());
+ }
+
+ return to_api(status: 204);

5. Api/AuthController UserResource geçişi (publish edilmişse)

Adım 2.3'te anlatılan davranış değişimini uygulamak için Api/Auth/AuthController.php patch'i:

 use App\Domain\Auth\Actions\TwoFactorChallengeAction;
 use App\Domain\Auth\DTOs\LoginDTO;
 use App\Domain\Auth\DTOs\RegisterDTO;
+use App\Exceptions\ApiException;
 use App\Http\Controllers\Controller;
 use App\Http\Requests\Api\Auth\LoginRequest;
 use App\Http\Requests\Api\Auth\RegisterRequest;
 use App\Http\Requests\Api\Auth\TwoFactorChallengeRequest;
+use App\Http\Resources\Admin\User\UserResource;
 use App\Http\Responses\ApiResponse;

 public function register(...): ApiResponse
 {
     $result = $action->execute(...);
+    $userPayload = new UserResource($result['user']->loadMissing('roles'));

     if ($result['requires_verification']) {
         return to_api(
-            ['user' => $result['user'], 'requires_verification' => true],
+            ['user' => $userPayload, 'requires_verification' => true],
             'Registration successful. ...',
             201,
         );
     }

-    return to_api($result, 'Registration successful.', 201);
+    return to_api(
+        ['user' => $userPayload, 'token' => $result['token'], 'requires_verification' => false],
+        'Registration successful.',
+        201,
+    );
 }

 // login default branch
-    default => to_api(
-        ['user' => $result['user'], 'token' => $result['token']],
-        'Login successful.',
-    ),
+    default => to_api(
+        [
+            'user' => new UserResource($result['user']->loadMissing('roles')),
+            'token' => $result['token'],
+        ],
+        'Login successful.',
+    ),

 // me
-    return to_api($request->user());
+    return to_api(new UserResource($request->user()->loadMissing('roles')));

 // twoFactorChallenge
-    return to_api($result, 'Login successful.');
+    return to_api(
+        [
+            'user' => new UserResource($result['user']->loadMissing('roles')),
+            'token' => $result['token'],
+        ],
+        'Login successful.',
+    );

6. MakeDomainCommand scaffold (publish edilmişse)

app/Console/Commands/MakeDomainCommand.php publish edilmişse, yeni scaffold template'i için iki nokta:

 use {$dtoNamespace}\\{$this->dn}DTO;
+use App\Exceptions\ApiException;
 use App\Http\Controllers\Controller;
 ...

 public function destroy({$this->dn} \${$v}, Delete{$this->dn}Action \$action): ApiResponse|JsonResponse
 {
     try {
         \$action->execute(\${$v});
-
-        return to_api(status: 204);
     } catch (\LogicException \$e) {
-        return to_api(null, \$e->getMessage(), 400);
+        throw ApiException::badRequest(\$e->getMessage());
     }
+
+    return to_api(status: 204);
 }

Testiniz tests/Feature/Console/MakeDomainCommandTest.php'de yeni scaffold'u doğruluyorsa assertion güncellenmeli:

 expect(file_get_contents(app_path("Http/Controllers/Api/{$domain}Controller.php")))
-    ->toContain('return to_api(null, $e->getMessage(), 400);');
+    ->toContain('throw ApiException::badRequest($e->getMessage());');

7. Kurulum zamanı fix'leri (OAuth + Postman ayarları + Passport personal client)

Bu üç adım, 13.4.1 öncesi kurulmuş tüm mevcut install'lar için geçerli. API response işinden bağımsız çalışır — controller publish etsen de etmesen de sk:update sonrası çalıştır.

7.1 OAuth migration'ları UUID-uyumlu hale getirildi

Üç Passport migration'ı artık foreignUuid / nullableUuidMorphs kullanıyor (önceden foreignId / nullableMorphs). Bu, starter kit'in users.id için gönderdiği char(36) primary key ile eşleşiyor. Patch uygulanmazsa Passport ilk access token insert denemesinde SQLSTATE 1265: Data truncated for column 'user_id' hatasıyla API login akışını bozar.

Taze kurulumlar bunu site:install sırasında otomatik alır. Mevcut install'lar için üç migration'ı canlı veri üzerinde yeniden çalıştır:

# 1. Üç migration'ı rollback et (veri kaybı yok — oauth_* tabloları
#    her token üretimde yeniden doluyor):
php artisan migrate:rollback --path=database/migrations/2026_03_04_205119_create_oauth_auth_codes_table.php
php artisan migrate:rollback --path=database/migrations/2026_03_04_205120_create_oauth_access_tokens_table.php
php artisan migrate:rollback --path=database/migrations/2026_03_04_205122_create_oauth_clients_table.php

# 2. Yeni şema ile yeniden çalıştır:
php artisan migrate

Rollback mümkün değilse (schema fork'unda zaten char(36) user_id satırları varsa), kolonu manuel olarak düzelt:

ALTER TABLE oauth_access_tokens MODIFY user_id CHAR(36) NULL;
ALTER TABLE oauth_auth_codes    MODIFY user_id CHAR(36) NOT NULL;
ALTER TABLE oauth_clients       MODIFY owner_id CHAR(36) NULL;

Bir login testiyle doğrula — Adım 9 (Regresyon testi).

7.2 Postman / Apidog kimlik bilgileri .env'den ayarlar tablosuna taşındı

Postman'i üç .env anahtarıyla bağlayan önceki versiyon kaldırıldı. Yapılandırma artık postman / apidog settings gruplarında duruyor ve api_key / access_token alanları config/settings.php → sensitive_keys aracılığıyla DB'de encrypted tutuluyor.

.env içinde POSTMAN_API_KEY, POSTMAN_WORKSPACE_ID veya POSTMAN_COLLECTION_ID varsa bir kerelik olarak ayarlar tablosuna taşı, sonra .env'den sil:

php artisan tinker --execute '
use App\Models\Setting;
Setting::setValue("postman.api_key", env("POSTMAN_API_KEY"));
Setting::setValue("postman.workspace_id", env("POSTMAN_WORKSPACE_ID"));
Setting::setValue("postman.collection_id", env("POSTMAN_COLLECTION_ID"));
echo "migrated";
'

Ardından her iki dosyadan (.env ve .env.example) üç anahtarı da sil. Admin UI'da Settings → API Clients → Postman saklanan değerleri gösterir (gizli alanlar maskeli); anahtarı ileride rotate etmek için buradan yönet. Apidog aynı şekilde Settings → API Clients → Apidog üzerinden yapılandırılır (Access Token + Project ID).

7.3 Passport personal access client (site:install içindeki yeni adım)

site:install artık passport:keys ile admin-user seed adımları arasında passport:client --personal --provider=users'ı otomatik çalıştırıyor. Mevcut install'ında personal access client yoksa (belirti: API login'de RuntimeException: Personal access client not found for 'users'), bir kerelik oluştur:

php artisan passport:client --personal --provider=users --name="$(php artisan config:show app.name)" --no-interaction

oauth_clients tablosuna revoked=0 olan tek bir satır düşer. API token üretimi anında çalışmaya başlar — uygulama yeniden başlatmaya gerek yok.

8. Yeni additive özellikler — kod değişikliği gerekmez

Bu özellikler otomatik devreye girer, client'a yeni alanlar/header'lar gelir. Frontend takımını bilgilendirin:

Özellik Nerede görünür
trace_id (UUID) Her JSON body (success ve error), ayrıca X-Request-ID header
X-Correlation-ID Client X-Request-ID gönderirse sanitize edilip echo'lanır
Retry-After 429 Too Many Requests response'ta
simplePaginate() desteği to_api(Model::simplePaginate(...)) artık type error vermeden çalışır; meta.has_more verir
"Postman'e Gönder" butonu API Rotaları sayfası → yapılandırma tamamsa OpenAPI spec'ini Postman'e push eder
"Apidog'a Gönder" butonu API Rotaları sayfası → yapılandırma tamamsa OpenAPI spec'ini Apidog'a push eder
Settings → API Clients tabı Postman + Apidog kimlik bilgileri; postman.api_key / apidog.access_token DB'de encrypted

9. Regresyon testi — opsiyonel ama tavsiye edilir

Paket tests/Feature/Api/ApiResponseTest.php içinde envelope şekli + exception mapping + trace_id + 204 + Retry-After + debug guard için 16 testlik bir kontrat dosyası shipliyor. App'inizde yoksa şuradan kopyalayın:

cp vendor/lvntr/laravel-starter-kit/tests/examples/ApiResponseTest.php \
   tests/Feature/Api/ApiResponseTest.php
php artisan test --compact --filter=ApiResponseTest

Beklenen: 16 test, 57 assertion geçer. Fail olursa test API middleware group'ta AssignTraceId'in aktif olup olmadığını kontrol edin.

10. Geri alma (rollback)

Sürüm geri çevrilirse:

git revert <upgrade-commit>
composer install
php artisan sk:update --force   # publish edilmiş dosyaları eski sürüme döndürür

AssignTraceId.php dosyası 13.4.x'te yoktu — rollback'ten sonra silin veya Bootstrap.php'nin eski sürümü sınıfı referans etmiyorsa bırakın (no-op).


13.3.x → 13.4.0 — Güvenlik hardening sprint'i

Özet: Üç-katlı paralel kod inceleme bulguları sonrası ~37 bulgu kapatıldı (HIGH: 13 → 1 manuel, MEDIUM: 14, LOW: 4). Değişikliklerin büyük kısmı güvenlik (auth bypass, brute-force, XSS, log injection) ve veri bütünlüğü (DB transaction eksiklikleri). Yeni kurulumlar bu düzeltmeleri otomatik alır; mevcut kurulumlar bu dokümandaki patch listesini uygulamalıdır.

0. Kimi etkiler?

Kullanıcı Ne yapmalı
Paketi yeni kuranlar (composer create-project + sk:install) Hiçbir şey — stubs zaten yeni sürümde.
Mevcut consumer app çalıştıranlar Bu dokümandaki Adım 1-8'i takip edin.
Sadece paket src/ kullananlar (publish yapmadı) composer update lvntr/laravel-starter-kit yeter.

1. Upgrade öncesi hazırlık

  1. Branch + yedek: git checkout -b upgrade/v13.4.0 && git push
  2. DB yedeği: Production için snapshot / dump.
  3. Ortam kontrolü: composer test + npm run build mevcut sürümde geçiyor mu?
  4. PR sürecine dahil edin: Bu güncellemenin büyük kısmı patch-stili; code review gerektirir.

2. Paket güncellemesi

composer update lvntr/laravel-starter-kit --with-all-dependencies
npm install

Bu adım Tier-1 değişiklikleri (paket src/ içi) otomatik taşır:

  • SecurityHeaders HSTS preload direktifi (src/Http/Middleware/SecurityHeaders.php)
  • MakeDomainCommand / stub iyileştirmeleri

Kalan tüm değişiklikler publish edilmiş dosyalarda olduğu için sizin app'inizdeki kopyayı güncellemeniz gerekiyor.


3. HIGH — Güvenlik ve veri bütünlüğü patch'leri

Bunları aynı sırada uygulayın. Her biri bağımsız olarak çalışır ama sıralı commit temiz bir history oluşturur.

3.1 (BE-H1) UserPolicy::delete + Api\UserController::destroy null guard

Dosya: app/Policies/UserPolicy.php

delete() metodundaki self-match dalını değiştirin:

     public function delete(User $actor, User $user): bool
     {
         if ($actor->is($user)) {
-            return true;
+            return false;
         }

         if (! $this->canManage($actor, $user)) {
             return false;
         }

         return $actor->can('users.delete');
     }

Dosya: app/Http/Controllers/Api/UserController.php

destroy metoduna null guard ekleyin:

     public function destroy(Request $request, User $user, DeleteUserAction $action): ApiResponse|JsonResponse
     {
         Gate::authorize('delete', $user);

+        $performedById = $request->user()?->id;
+        if ($performedById === null) {
+            return to_api(null, 'Unauthenticated.', 401);
+        }
+
         try {
-            $action->execute($user, (string) $request->user()?->id);
+            $action->execute($user, (string) $performedById);
             return to_api(status: 204);

Test: DELETE /api/v1/users/{kendi_id} 403 dönmeli (policy'de reddedildi), expired token ile 401 dönmeli.


3.2 (BE-H2) CreateRoleAction + UpdateRoleAction DB transaction

Dosya: app/Domain/Role/Actions/CreateRoleAction.php

 use App\Models\Role;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
 ...
     public function execute(RoleDTO $dto): Role
     {
-        $role = Role::create($dto->toArray());
-        $role->syncPermissions($dto->permissions);
+        $role = DB::transaction(function () use ($dto): Role {
+            $role = Role::create($dto->toArray());
+            $role->syncPermissions($dto->permissions);
+
+            return $role;
+        });

         RoleCreated::dispatch($role, Auth::id());
         return $role;
     }

Dosya: app/Domain/Role/Actions/UpdateRoleAction.php

 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
 ...
         $oldPermissions = $role->permissions->pluck('name')->sort()->values()->all();

-        $role->update($data);
-        $role->refresh();
-        $role->syncPermissions($dto->permissions);
+        $role = DB::transaction(function () use ($role, $data, $dto): Role {
+            $role->update($data);
+            $role->refresh();
+            $role->syncPermissions($dto->permissions);
+
+            return $role;
+        });

3.3 (BE-H3) UpdateAuthSettingsAction 2FA revoke transaction

Dosya: app/Domain/Setting/Actions/UpdateAuthSettingsAction.php

 use App\Models\User;
+use Illuminate\Support\Facades\DB;

 ...
     public function execute(AuthSettingsDTO $dto): void
     {
-        $wasTwoFactorEnabled = Setting::getValue('auth.two_factor', '1') === '1';
-        $isTwoFactorDisabled = $dto->twoFactor === '0';
-
-        Setting::setGroup('auth', $dto->toArray());
-
-        if ($wasTwoFactorEnabled && $isTwoFactorDisabled) {
-            $this->revokeAllTwoFactorAuth();
-        }
+        DB::transaction(function () use ($dto): void {
+            $wasTwoFactorEnabled = Setting::getValue('auth.two_factor', '1') === '1';
+            $isTwoFactorDisabled = $dto->twoFactor === '0';
+
+            Setting::setGroup('auth', $dto->toArray());
+
+            if ($wasTwoFactorEnabled && $isTwoFactorDisabled) {
+                $this->revokeAllTwoFactorAuth();
+            }
+        });
     }

3.4 (BE-H4) LogoutUserAction null-safe

Dosya: app/Domain/Auth/Actions/LogoutUserAction.php

     public function execute(User $user): void
     {
-        $user->token()->revoke();
+        $user->token()?->revoke();
     }

Tek karakter — ama production'da active token olmayan kullanıcı logout isteğinde 500 hatası üretiyor.


3.5 (BE-H5) FileManager N+1 düzeltmesi

Dosyalar: app/Domain/FileManager/Actions/BulkDeleteAction.php ve DeleteFolderAction.php.

Her iki dosyada collectDescendantIds metodunu değiştirin — owner scope'unda tek sorguyla parent_id haritasını çekip PHP tarafında BFS yapacak. Değişiklik hacmi büyük olduğu için tam yeni sürümleri vendor/lvntr/laravel-starter-kit/stubs/app/Domain/FileManager/Actions/BulkDeleteAction.php ve DeleteFolderAction.php dosyalarından kopyalayın.

Ana değişiklikler:

  • BulkDeleteAction'a buildChildrenMap(FileManagerContextDTO $context): array eklendi. collectDescendantIds($folder, $childrenByParent) bu haritayı parametre alır.
  • DeleteFolderAction::collectDescendantIds'e context parametresi eklendi; owner'a ait tüm klasör satırlarını tek sorguda çekip dolaşıyor.

50 seviyelik klasör ağacında 50 query → 1 query.


3.6 (BE-H6) SMTP encryption 'none' düzeltmesi

Dosya: app/Providers/SettingsServiceProvider.php

             if (array_key_exists('encryption', $mail)) {
-                config(['mail.mailers.smtp.encryption' => $mail['encryption']]);
+                // Laravel's SMTP mailer expects null (not the string "none") to send without TLS.
+                $encryption = $mail['encryption'] === 'none' ? null : $mail['encryption'];
+                config(['mail.mailers.smtp.encryption' => $encryption]);
             }

3.7 (GV-H2 + GV-H3) ApiExceptionHandler — message leak + X-Request-ID injection

Dosya: app/Exceptions/ApiExceptionHandler.php

İki değişiklik:

A) handle() metodunda trace ID üretimini değiştirin:

     private static function handle(Throwable $e, Request $request): JsonResponse
     {
-        // 1. Trace ID — use client-provided value or generate a new one
-        $traceId = $request->header('X-Request-ID', (string) Str::uuid());
+        // 1. Trace ID — always server-generated to prevent log / header injection.
+        //    Any client-supplied X-Request-ID is accepted as correlation metadata
+        //    only after being sanitised and length-capped.
+        $traceId = (string) Str::uuid();
+        $clientRequestId = self::sanitizeClientRequestId($request->header('X-Request-ID'));

         // 2. Status + Message mapping
         [$status, $message] = self::resolve($e);

         // 3. Logging — 500+ non-validation errors
         if ($status >= 500 && ! ($e instanceof ValidationException)) {
             Log::error("[API {$status}] {$message}", [
                 'trace_id' => $traceId,
+                'client_request_id' => $clientRequestId,
                 'exception' => get_class($e),
                 ...
             ]);
         }

B) resolve() metodundaki default dalını ve sınıfa yeni metodu ekleyin:

-            // Unexpected errors
             default => [
                 500,
-                config('app.debug') ? $e->getMessage() : 'A server error occurred.',
+                'A server error occurred.',
             ],
         };
     }

+    /**
+     * Accept a client-provided X-Request-ID only if it matches a safe charset
+     * (letters, digits, dash, underscore, dot) and is ≤ 128 chars long.
+     */
+    private static function sanitizeClientRequestId(mixed $value): ?string
+    {
+        if (! is_string($value) || $value === '') {
+            return null;
+        }
+
+        $trimmed = substr($value, 0, 128);
+
+        return preg_match('/^[A-Za-z0-9._-]+$/', $trimmed) === 1 ? $trimmed : null;
+    }

3.8 (FE-H1) Axios CSRF defaults

Dosya: resources/js/app.ts

Dosyanın en üstüne, import'ların hemen ardına ekleyin:

 import '../css/app.css';
 import 'primeicons/primeicons.css';
 import { createInertiaApp, usePage } from '@inertiajs/vue3';
+import axios from 'axios';
 import { i18nVue } from 'laravel-vue-i18n';
 ...
 import { PermissionPlugin } from '@/plugins/permission';

+// Axios defaults — send session + XSRF cookies on every request so Fortify
+// endpoints that rely on the web session (2FA, sessions, password-confirm)
+// stay CSRF-protected. XSRF cookie/header names match Laravel's defaults.
+axios.defaults.withCredentials = true;
+axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
+axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
+axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
+axios.defaults.headers.common['Accept'] = 'application/json';

3.9 (FE-H2) TwoFactorTab QR SVG XSS fix

Dosya: resources/js/pages/Profile/components/TwoFactorTab.vue (ya da legacy yol pages/Profile/TwoFactorTab.vue)

A) <script setup> içinde — qrCodeSvg ref'inin altına ekleyin:

     const qrCodeSvg = ref('');
     const setupKey = ref('');
     const recoveryCodes = ref<string[]>([]);
     const showRecoveryCodes = ref(false);

+    /**
+     * Render the Fortify QR SVG through an <img src="data:..."> element
+     * rather than v-html. An <img> sandbox neutralises any inline <script>
+     * or event handlers that a compromised intermediary could smuggle in.
+     */
+    const qrCodeDataUrl = computed<string>(() => {
+        if (!qrCodeSvg.value) return '';
+        try {
+            const encoded = window.btoa(unescape(encodeURIComponent(qrCodeSvg.value)));
+            return `data:image/svg+xml;base64,${encoded}`;
+        } catch {
+            return '';
+        }
+    });

B) Template'te v-html bloğunu değiştirin:

-                            <!-- eslint-disable vue/no-v-html -- QR SVG from trusted server -->
-                            <div class="inline-block rounded-lg bg-white p-4" v-html="qrCodeSvg" />
-                            <!-- eslint-enable vue/no-v-html -->
+                            <div class="inline-block rounded-lg bg-white p-4">
+                                <img
+                                    v-if="qrCodeDataUrl"
+                                    :src="qrCodeDataUrl"
+                                    :alt="$t('sk-profile.two_factor_scan')"
+                                    class="h-48 w-48"
+                                />
+                            </div>

3.10 (FE-H3) useDefinition.load() error handling

Dosya: resources/js/composables/useDefinition.ts

load() ve loadAll() metodlarını vendor/lvntr/laravel-starter-kit/stubs/resources/js/composables/useDefinition.ts dosyasındaki yeni sürümle değiştirin. Ana değişiklik: fetch çağrısı try/catch içinde, res.ok kontrol ediliyor, hata durumunda loaded.value false bırakılıyor, console'a log atılıyor.


4. MEDIUM — Yetkilendirme, performans, UX

4.1 (BE-M1) FormRequest authorize(): true temizliği

Aşağıdaki dosyalarda return true; yerine ilgili permission kontrolünü koyun:

Dosya Permission
app/Http/Requests/Admin/User/StoreUserRequest.php users.create
app/Http/Requests/Api/User/StoreUserRequest.php users.create
app/Http/Requests/Admin/Role/StoreRoleRequest.php roles.create
app/Http/Requests/Admin/Settings/UpdateAuthSettingsRequest.php settings.update
app/Http/Requests/Admin/Settings/UpdateGeneralSettingsRequest.php settings.update
app/Http/Requests/Admin/Settings/UpdateMailSettingsRequest.php settings.update
app/Http/Requests/Admin/Settings/UpdateStorageSettingsRequest.php settings.update
app/Http/Requests/Admin/Settings/UpdateFileManagerSettingsRequest.php settings.update
app/Http/Requests/Admin/Settings/UpdateTurnstileSettingsRequest.php settings.update
app/Http/Requests/Admin/Settings/SendTestMailRequest.php settings.update
     public function authorize(): bool
     {
-        return true;
+        return $this->user()?->can('users.create') ?? false;
     }

(Permission adını uygun olanla değiştirin.)

Ek olarak app/Http/Requests/DestroySessionsRequest.php:

-        return true;
+        return $this->user() !== null;

Auth / public endpoint'lere dokunmayın: Api/Auth/LoginRequest.php, RegisterRequest.php, TwoFactorChallengeRequest.php public kalır.

FileManager endpoint'lerine dokunmayın: FileManagerRequest.php ve alt sınıflar context-tabanlı yetkilendirme kullanır.


4.2 (BE-M4) TwoFactorChallenge brute-force hardening

Dosya: app/Domain/Auth/Actions/TwoFactorChallengeAction.php

Üç başarısız dala da Cache::forget($cacheKey) ekleyin — challenge artık tek kullanımlık:

         if ($code !== null && $code !== '') {
             $valid = $this->provider->verify(...);

             if (! $valid) {
+                Cache::forget($cacheKey);
+
                 return null;
             }
         } elseif ($recoveryCode !== null && $recoveryCode !== '') {
             $match = collect($user->recoveryCodes())->first(...);

             if ($match === null) {
+                Cache::forget($cacheKey);
+
                 return null;
             }

             $user->replaceRecoveryCode($match);
         } else {
+            Cache::forget($cacheKey);
+
             return null;
         }

Route tarafındaki throttle:5,1 zaten mevcut.


4.3 (BE-M7 + BE-M12) SettingService transaction + cache

Dosya: app/Domain/Setting/SettingService.php

Tüm dosyayı vendor/lvntr/laravel-starter-kit/stubs/app/Domain/Setting/SettingService.php dosyasından kopyalamak en kolayı. Özetle:

  1. DB facade import'u eklendi.
  2. getValue() ve getGroup() artık allGrouped() cache'i üstünden okuyor — tekil sorgu yok.
  3. setGroup() DB::transaction(...) içine alındı.

Davranış aynı, performans ve atomisite yükseldi.


4.4 (BE-M8) MoveItemRequest validation sıkılaştırma

Dosya: app/Http/Requests/FileManager/MoveItemRequest.php

 <?php

 namespace App\Http\Requests\FileManager;

+use Illuminate\Validation\Rule;
+
 class MoveItemRequest extends FileManagerRequest
 {
     public function rules(): array
     {
+        $itemType = $this->input('item_type');
+
+        $itemIdRules = ['required'];
+        if ($itemType === 'file') {
+            $itemIdRules = ['required', 'integer', 'min:1'];
+        } elseif ($itemType === 'folder') {
+            $itemIdRules = ['required', 'uuid'];
+        }
+
         return [
             ...$this->contextRules(),
-            'item_type' => ['required', 'string', 'in:folder,file'],
-            'item_id' => ['required'],
+            'item_type' => ['required', 'string', Rule::in(['folder', 'file'])],
+            'item_id' => $itemIdRules,
             'target_folder_id' => ['nullable', 'uuid'],
         ];
     }
 }

4.5 (BE-M9) DeleteFolderRequest FormRequest

Yeni dosya: app/Http/Requests/FileManager/DeleteFolderRequest.php

<?php

namespace App\Http\Requests\FileManager;

class DeleteFolderRequest extends FileManagerRequest
{
    /**
     * @return array<string, array<int, mixed>>
     */
    public function rules(): array
    {
        return $this->contextRules();
    }
}

Dosya: app/Http/Controllers/FileManagerController.php

Use satırına ekleyin + metod signature değiştirin:

 use App\Http\Requests\FileManager\BulkDeleteRequest;
+use App\Http\Requests\FileManager\DeleteFolderRequest;
 use App\Http\Requests\FileManager\MoveItemRequest;
 ...

-    public function deleteFolder(Request $request, FileFolder $folder, DeleteFolderAction $action): ApiResponse
+    public function deleteFolder(DeleteFolderRequest $request, FileFolder $folder, DeleteFolderAction $action): ApiResponse
     {
-        $context = $this->contextFromRequest($request);
+        $context = $request->context();
         $this->authorizer->authorizeWrite($context);

4.6 (BE-M10) uploadAvatar Gate::authorize tutarlılığı

Dosya: app/Http/Controllers/Admin/UserController.php

     public function uploadAvatar(UploadAvatarRequest $request, User $user, UploadMediaAction $action): ApiResponse
     {
+        Gate::authorize('update', $user);
+
         $action->execute($user, $request, 'avatar');

4.7 (FE-M1) useDialog timer leak

Dosya: resources/js/composables/useDialog.ts

Tam sürüm için vendor/lvntr/laravel-starter-kit/stubs/resources/js/composables/useDialog.ts'e bakın. Değişiklikler:

  1. state'in altına module seviyesinde let closeTimer: ReturnType<typeof setTimeout> | null = null; eklendi.
  2. open() başında clearTimeout(closeTimer) + closeTimer = null.
  3. close() başında da aynı clear, sonra closeTimer = setTimeout(..., 300), timeout body'sinde closeTimer = null.

4.8 (FE-M2) useImageLightbox timer leak

useDialog ile aynı pattern. vendor/lvntr/laravel-starter-kit/stubs/resources/js/composables/useImageLightbox.ts'den kopyalayın.


4.9 (FE-M4) SkForm isDirty guard — veri kaybı koruması

Dosya: resources/js/components/Lvntr-Starter-Kit/FormBuilder/SkForm.vue (veya paket importu kullanıyorsanız bu değişiklik composer update ile gelir — paket kaynağı düzeltildi).

watch(derivedDefaults, …) bloğuna isDirty dalı ekleyin:

     watch(derivedDefaults, (newValues, oldValues) => {
         if (!isInternalMode.value) {
             return;
         }
         if (oldValues && shallowRecordEqual(newValues, oldValues)) {
             return;
         }
+        if (internalForm.isDirty) {
+            internalForm.defaults(newValues);
+            return;
+        }
         restoringDefaults.value = true;

4.10 (FE-M6) SkDatatable urlFilters api.get

Dosya: resources/js/components/Lvntr-Starter-Kit/DatatableBuilder/SkDatatable.vue

     if (urlFilters.length) {
         onMounted(async () => {
-            await Promise.all(
+            await Promise.allSettled(
                 urlFilters.map(async (f) => {
-                    const res = await fetch(f.optionsUrl!, {
-                        headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
-                        credentials: 'same-origin',
-                    });
-                    const json = await res.json();
-                    urlOptions[f.key] = json.data ?? json;
+                    try {
+                        const data = await api.get<FilterOption[]>(f.optionsUrl!);
+                        urlOptions[f.key] = data ?? [];
+                    } catch {
+                        urlOptions[f.key] = [];
+                    }
                 }),
             );
         });
     }

Aynı dosyada let activeMenuItems = ref<MenuItem[]>([]); → const activeMenuItems = ref<MenuItem[]>([]); (FE-M9).


4.11 (FE-M7) TwoFactorTab router.reload await

Dosya: resources/js/pages/Profile/components/TwoFactorTab.vue

     async function enableTwoFactor() {
         twoFactorProcessing.value = true;

         if (!props.twoFactorEnabled) {
             await axios.post('/user/two-factor-authentication');
-            router.reload({ only: ['twoFactorEnabled', 'twoFactorConfirmed'] });
+            await new Promise<void>((resolve) => {
+                router.reload({
+                    only: ['twoFactorEnabled', 'twoFactorConfirmed'],
+                    onFinish: () => resolve(),
+                });
+            });
         }

         await loadQrAndSetupKey();

4.12 (FE-M8) as any cast'leri temizleyin

Dosya: resources/js/pages/Profile/components/ProfileInfoTab.vue

-        :avatar-url="(user as any)?.avatar_url"
+        :avatar-url="user?.avatar_url"

Dosya: resources/js/pages/Admin/Users/components/UserForm.vue

-            :avatar-url="(formRef.remoteData as any)?.avatar_url"
+            :avatar-url="(formRef.remoteData as { avatar_url?: string | null } | null)?.avatar_url"

5. Config / Env hardening

5.1 (GV-M1) .env.example ve .env'de LOG_LEVEL

Dosya: .env.example

-LOG_LEVEL=debug
+LOG_LEVEL=error

Production .env'lerde de LOG_LEVEL=error ya da warning olduğundan emin olun.


5.2 (GV-M2) Tinker require → require-dev

Dosya: composer.json

     "require": {
         "php": "^8.3",
         "laravel/framework": "^13.0",
         "laravel/pulse": "^1.7",
-        "laravel/tinker": "^2.10.1 || ^3.0",
         "lvntr/laravel-starter-kit": "@dev"
     },
     "require-dev": {
         ...
         "laravel/sail": "^1.41",
+        "laravel/tinker": "^2.10.1 || ^3.0",
         "mockery/mockery": "^1.6",

Sonra: composer update.


5.3 (GV-M3, GV-M4) .env.example — Turnstile & Passport key placeholder'ları

Dosya: .env.example

Passport bölümünün altına ekleyin:

# Passport OAuth2 keys — prefer loading via env in production instead of
# committing the key files at storage/oauth-*.key. Run `php artisan passport:keys`
# once, move the generated strings into these env vars, then delete the files.
# PASSPORT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
# PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"

# Cloudflare Turnstile (bot / captcha). When TURNSTILE_ENABLED=false the
# `turnstile` middleware becomes a no-op, so leaving the keys empty during
# development is safe.
TURNSTILE_ENABLED=false
TURNSTILE_SITE_KEY=
TURNSTILE_SECRET_KEY=

5.4 (GV-M5) HandleInertiaRequests — appEnv / appDebug scope

Dosya: app/Http/Middleware/HandleInertiaRequests.php

             'appVersion' => InstalledVersions::getPrettyVersion('lvntr/laravel-starter-kit'),
-            'appEnv' => config('app.env'),
-            'appDebug' => config('app.debug'),
+            'appEnv' => fn () => app()->environment('production') ? null : config('app.env'),
+            'appDebug' => fn () => app()->environment('production') ? false : (bool) config('app.debug'),

Eğer frontend'te appEnv === 'production' kontrolü yapan kod varsa artık null bekleyecek şekilde güncellenmeli.


5.5 (GV-M7) CORS preflight cache

Dosya: config/cors.php

-    'max_age' => 0,
+    // Cache preflight (OPTIONS) results in the browser for 2 hours so SPA /
+    // mobile clients don't re-run the CORS handshake on every mutating call.
+    'max_age' => 7200,

5.6 (GV-L1) Password::defaults policy

Dosya: app/Providers/AppServiceProvider.php

 use Illuminate\Support\Facades\Event;
 use Illuminate\Support\ServiceProvider;
+use Illuminate\Validation\Rules\Password;

 class AppServiceProvider extends ServiceProvider
 {
     ...
     public function boot(): void
     {
         Event::listen(Login::class, UpdateLastLogin::class);
+
+        Password::defaults(function () {
+            return Password::min(10)
+                ->mixedCase()
+                ->letters()
+                ->numbers()
+                ->symbols();
+        });
     }
 }

Uyarı: Bu değişiklik mevcut kullanıcıların şifrelerini geçersiz KILMAZ, ama yeni kayıt / şifre değiştirme akışlarında artık 10+ karakter, karışık büyük/küçük, rakam ve sembol zorunlu.


6. GV-H1 — Passport private key rotasyonu (KRİTİK, MANUEL)

Bu adım destructive işlemler içerir; iş günü dışında, takım onayı + rollback planıyla uygulayın.

# 1. git-filter-repo kur (filter-branch deprecated)
brew install git-filter-repo          # veya: pipx install git-filter-repo

# 2. Key dosyalarını history'den sil
cd /yolu/starter-kit-app
git filter-repo --path storage/oauth-private.key --invert-paths
git filter-repo --path storage/oauth-public.key  --invert-paths

# 3. Yeni key üret (geçici olarak dosya kalsın)
php artisan passport:keys --force

# 4. İçeriği .env'e geçir, dosyaları sil
# (PASSPORT_PRIVATE_KEY ve PASSPORT_PUBLIC_KEY — config/passport.php zaten okuyor)
rm storage/oauth-private.key storage/oauth-public.key

# 5. Aktif token'ları purge et
php artisan passport:purge

# 6. Force push (takım onayı şart)
git push --force-with-lease origin <branch>

Dikkat:

  • Tüm ekibin force-push sonrası git fetch && git reset --hard origin/<branch> yapması gerekir.
  • CI / CD sunucularında kayıtlı repo kopyaları da temizlenmeli.
  • PASSPORT_* env değerleri production vault / secrets manager'a eklenmeli (git'e ASLA commit edilmemeli).

7. Doğrulama

# Backend
composer install
php artisan migrate --force
php artisan sk:seed-permissions --fresh
vendor/bin/pint --dirty --format agent

# Frontend
npm install
npm run build

# Tests
php artisan test --compact
npm run test

Her şey yeşile dönene kadar commit etmeyin. Bir test başarısız olursa ilgili patch'i izole edip hot-fix yapın; bu sürümdeki diğer patch'lere ertelemeyin — hepsi bağımsız.

8. Son kontrol — smoke test senaryoları

  • [ ] Login → 2FA challenge → kod yanlış → tek hakkı tüketir (BE-M4).
  • [ ] API DELETE /api/v1/users/{kendi_id} 403 döner (BE-H1).
  • [ ] Role create + permission atama: DB'ye yansıyor (BE-H2).
  • [ ] Settings > Auth sayfasından 2FA kapat: tüm kullanıcıların 2FA secret'ları temizleniyor + setting kaydedildi (BE-H3).
  • [ ] Büyük klasör (50+ seviye) bulk delete: sayfa timeout olmuyor (BE-H5).
  • [ ] SMTP encryption "none" seçili: mail gönderimi başarılı (BE-H6).
  • [ ] APP_DEBUG=true iken 500 hatası alan API endpoint: response message generic; detay debug bloğunda (GV-H2).
  • [ ] X-Request-ID: ../etc/passwd header'ı ile istek: response header X-Request-ID UUID formatında; log'da client_request_id: null (GV-H3).
  • [ ] 2FA kurulum sayfası: QR kod <img> olarak render, v-html yok (FE-H2).
  • [ ] Dialog aç/kapat/aç hızlı yapınca içerik silinmiyor (FE-M1).
  • [ ] FormBuilder formu açıldıktan sonra parent prop değişirse: kullanıcının yazdığı input silinmiyor (FE-M4).

Sorun giderme

Genel

sk:update sonrası sınıflar bulunamıyorsa:

composer dump-autoload

sk:update sonrası Vite manifest hatası:

npm run build
# veya dev sunucusunu başlatın
npm run dev

sk:update sonrası migration hatası:

migrate:fresh / migrate:refresh'e başvurmayın — bu projenin migration'ları artımlıdır ve tek tek çalıştırılabilir. Başarısız migration'ı düzeltin (ya da php artisan migrate:rollback --step=1 ile geri alın) ve php artisan migrate'i tekrar çalıştırın.

Yükseltme sonrası Passport anahtarları eksikse:

php artisan passport:keys --force

"422 Unprocessable Content" — yeni FormRequest authorize

Yeni authorize() kontrolü sert. İlgili permission'ın user'a atanmış olduğundan emin olun: php artisan sk:seed-permissions --fresh çalıştırın.

2FA doğrulamasında "challenge expired"

BE-M4 sonrası tek deneme hakkı var. 6 haneli kodu yanlış girerseniz tüm akış baştan başlar — Fortify OTP uygulamasındaki yeni kodu (30 saniyede bir rotates) alıp login'e yeniden girin.

Axios istekleri 419 dönmüyor ama session yok

FE-H1 sonrası withCredentials = true. Eğer front-end'iniz farklı bir domain'den geliyorsa (subdomain dahil) config/cors.php içinde supports_credentials => true olmalı + allowed_origins wildcard içermemeli.

Dashboard boş görünüyor

appEnv / appDebug artık prod'da null / false — Vue template'te koşullu rendering varsa fallback değer kullandığından emin olun.


Önceki sürümler

  • 13.3.3 (2026-04-20) — Windows build fix: Builder core/ import'ları için sibling core.ts barrel. Detaylar: CHANGELOG.md.
  • 13.3.2 (2026-04-19) — Güvenlik hardening + user audit + API auth parity. Detaylar: CHANGELOG.md.

Tam değişiklik tarihçesi için CHANGELOG.md'ye bakın.