Proje: OkulUp API · Hub: OkulUp API — Decisions
S3 Bucket Public/Private Hiyerarşisi
Karar
okulup-dev bucket’ı iki üst düzey prefix’e ayrıldı:
public/ — bucket policy ile public-read, Storage::url() döner
private/ — public policy yok, Storage::temporaryUrl() (15 dk TTL) döner
Yeni yüklemelerin tamamı App\Support\StoragePath helper’ı üzerinden gidiyor; eski (root-level) path’ler olduğu yerde kalıyor ve geriye uyumluluk için yine public-read.
Path Eşlemesi
Public (public/ altı):
| Path | Ne için |
|---|---|
public/avatars/{userId}/ | Kullanıcı/öğrenci/veli avatar |
public/events/{eventId}/cover/ | Etkinlik kapak görseli |
public/galleries/{galleryId}/[thumbnails/] | Onaylı galeri medya + video thumbnail |
public/announcements/{announcementId}/ | Duyuru ekleri |
public/meal-menus/{mealMenuId}/ | Yemek menüsü (PDF/xlsx) |
public/assignments/{assignmentId}/ | Öğretmen ödev materyalleri |
Private (private/ altı, presigned URL):
| Path | Ne için |
|---|---|
private/tracking/{studentId}/ | Öğrenci takip fotoğrafları |
private/messages/{conversationId}/ | Özel mesaj ekleri |
private/submissions/{assignmentId}/ | Öğrenci ödev teslimi |
private/document-requests/{drId}/ | Belge talebi ekleri (transkript vs.) |
private/payments/{paymentId}/receipts/ | Ödeme dekontları |
private/exports/{userId}/ | CSV rapor export’ları (zaten backend route üzerinden indiriliyor) |
Gerekçe
- Bucket root’unda 12 farklı kategori karışık duruyordu; hangi prefix kim için belirsizdi. Tek
okulup-devbucket aynı zamanda kullanıcı içeriği ve hassas veri tutuyordu, IAM policy granülarite zordu. - “Public” / “private” ayrımı bucket policy seviyesinde işletilince kod sözleşmesinin doğru olduğu garanti edilir — yeni endpoint eklerken sadece doğru helper metodu seçmek yeterli, “bunu URL olarak dönmeli miyim?” diye düşünmek gerekmiyor.
- Tracking/messages/submissions gibi gerçekten hassas veri presigned URL ile servis edilince link sızsa bile 15 dk içinde geçersiz olur.
Alternatifler (red)
- İki ayrı disk (
s3_public/s3_private) — Flysystem disk başına ayrıroot. Daha “Laravel-vari” ama 30+ controller çağrısının disk seçimini değiştirmek + URL üretimini iki ayrı yere bağlamak external API surface’i karıştırırdı. - Tüm bucket private + her şey presigned — avatar ve duyuru gibi yüksek hacimli public içerikler her istekte presigned URL ister, mobil cache invalidate olur (URL her seferinde değişir), CDN tarafında cache yapılamaz.
- Per-resource ACL (
object_acl='public-read'/'private') — S3 ACL yerine bucket policy önerilir (AWS resmi tavsiye); ACL granülerliği bizim için gereksiz karmaşıklık.
Backward Compatibility
Karar: Mevcut dosyalar dokunulmuyor, kod hem eski hem yeni path’i bilir.
StoragePath::url($path):private/prefix’i varsatemporaryUrl(), yoksa (eski root-levelavatars/...dahil)url().UserResource::resolveAvatarUrl()veDirectRecipientResource::avatarUrl(): hemavatars/hempublic/avatars/prefix’lerini tanır; başka bir şey ise (external URL, null) olduğu gibi döner.- Avatar silme (
UserController::deleteAvatarFile): aynı iki prefix kuralı.
Etkilenen DB kolonları (mevcut data dokunulmadı):
users.avatar_url,events.cover_image_path,gallery_media.file_path/thumbnail_path,announcement_attachments.file_path/thumbnail_path,meal_menu_attachments.file_path,assignment_attachments.file_path,assignment_submissions.file_path,document_request_attachments.file_path,messages.attachment_path,tracking_entries.photo_path,payments.receipt_url,report_exports.file_path.
Yeni Bucket Policy (AWS Console’da güncellenecek)
Mevcut policy Resource: arn:aws:s3:::okulup-dev/* ile her şeyi public-read açıyor. Yeni policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadNewLayout",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::okulup-dev/public/*"
},
{
"Sid": "PublicReadLegacyPaths",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::okulup-dev/avatars/*",
"arn:aws:s3:::okulup-dev/events/*",
"arn:aws:s3:::okulup-dev/galleries/*",
"arn:aws:s3:::okulup-dev/announcements/*",
"arn:aws:s3:::okulup-dev/meal-menus/*",
"arn:aws:s3:::okulup-dev/assignments/*"
]
}
]
}private/* için açık bir statement YOK → default deny, sadece IAM kullanıcısı (presigned URL yoluyla) erişebilir. Bu policy ile eski submissions/... ve messages/... root-level dosyalar artık 403 olur; bu kabul edildi (dev environment, prod’a geçilmedi).
Legacy private cleanup (manuel, console’dan): Console’da bucket root’undaki submissions/, messages/, tracking/, document-requests/, payments/, exports/ klasörlerini silmek isterse silebilir. URL’leri dönmüyor olsa bile bucket’ta hassas dosya tutmak istemiyoruz.
IAM Policy (kullanıcı için)
Değişmedi:
s3:PutObject, s3:GetObject, s3:DeleteObject
Resource: arn:aws:s3:::okulup-dev/*
GetObject yetkisi IAM seviyesinde olduğu için presigned URL üretimi private/* için de çalışır (bucket policy explicit deny etmiyor, sadece “allow” eklemiyor).
CORS (Web uygulaması için ileride)
Mobile native presigned URL’i fetch ettiğinde CORS uygulanmaz. Ancak web tarayıcısından (React/Vue admin paneli) presigned URL fetch edilirse CORS gerekir. Şu an web admin S3 fetch yapmıyor; ileride yaparsa:
[{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["https://admin.okulup.com"],
"MaxAgeSeconds": 3600
}]Test Davranışı
Storage::fake('s3') lokal disk kullandığı için temporaryUrl() desteklemez. StoragePath::url() \RuntimeException yakalayıp url() fallback’ine geçiyor; testler bozulmuyor.
Etkilenen Dosyalar
Yeni:
app/Support/StoragePath.php— path sabitleri +url()helper
Controller’lar (12, hepsi ->store($path, 's3') çağrısında StoragePath::* kullanıyor):
UserController(avatar) — özel:deleteAvatarFile()helper iki prefix de tanırEventController(cover),GalleryController(media + thumbnail),AnnouncementController(attachment),MealMenuController,AssignmentController(material + submission),DocumentRequestController,MessageController,TrackingController(store+update),PaymentController(receipt)ClassController— student avatar URL helper güncellendi
Service:
ExportService—exports/{userId}→private/exports/{userId}
Resources (private → StoragePath::url() ki o da temporaryUrl):
MessageResource,AssignmentSubmissionResource,TrackingEntryResource,DocumentRequestAttachmentResource,PaymentResourceUserResource,DirectRecipientResource— avatar resolver hem eski hem yeni prefix’i tanır
Related
- 2026-05-14-s3-okulup-dev-konfigurasyonu — Bucket konfig + ortak kullanım kararı (bu notun temeli)
- medya-isleme — Avatar/galeri/etkinlik kapağı işleme jobs