Lewati ke konten utama

Curator Ownership, Privacy & Usage Tracking

Dokumen ini menjelaskan dua aspek penting dari Filament Curator di starter kit ini:

  1. Ownership & Privacy — siapa yang memiliki media dan siapa yang bisa melihatnya
  2. Usage Tracking — pelacakan penggunaan media untuk menjaga integritas data

Bagian 1: Ownership & Privacy (Blameable)

Starter kit ini mengimplementasikan sistem kepemilikan (ownership) dan privasi (privacy) pada Curator Media Manager untuk meningkatkan keamanan dan akuntabilitas data.

Fitur Utama

  • Explicit Ownership: Setiap record media mencatat siapa yang mengunggahnya melalui kolom created_by.
  • Granular Privacy: Media dapat diatur menjadi PRIVATE, MEMBER, atau PUBLIC.
  • Automatic Visibility Sync: Sistem secara otomatis mensinkronkan status privacy (logical) dengan visibility (physical) di storage (misalnya S3).
  • Policy Enforcement: Akses untuk mengedit dan menghapus media dibatasi hanya untuk pemilik, admin, atau super admin.

Arsitektur Database

Kolom-kolom berikut telah ditambahkan ke tabel curator (via migration):

  • created_by (UUID): Foreign key ke tabel users.
  • deleted_by (UUID): Siapa yang menghapus (Soft Delete).
  • deleted_at (Timestamp): Waktu penghapusan.
  • privacy (String): Menyimpan status privasi media (Default: private).

Model CuratorMedia

Model App\Models\CuratorMedia telah diperbarui dengan:

  • Relationship: Method creator() yang merujuk ke model User.
  • Casts: Properti privacy di-cast ke enum App\Enums\Privacy.
  • Auto-blame: Menggunakan event creating untuk otomatis mengisi created_by dengan ID user yang sedang login.
  • Visibility Sync: Menggunakan event saving untuk sinkronisasi dua arah otomatis antara privacy dan visibility (physical storage).
// app/Models/CuratorMedia.php

protected static function booted(): void
{
self::saving(function (self $media): void {
// Priority 1: If privacy is changed, visibility must follow.
if ($media->isDirty('privacy')) {
$media->visibility = $media->privacy === Privacy::PUBLIC ? 'public' : 'private';
}
// Priority 2: If visibility is changed, privacy must follow (to stay in sync).
elseif ($media->isDirty('visibility')) {
$media->privacy = $media->visibility === 'public' ? Privacy::PUBLIC : Privacy::PRIVATE;
}
// Priority 3: Always ensure they match based on privacy source of truth.
else {
$media->visibility = $media->privacy === Privacy::PUBLIC ? 'public' : 'private';
}
});
}

Enum Privacy

Enum App\Enums\Privacy mendefinisikan tiga level akses:

  1. PRIVATE (Default): Hanya dapat dilihat oleh creator, admin, dan super_admin. Physical visibility di storage adalah private.
  2. MEMBER: Dapat dilihat oleh semua user yang sudah login. Physical visibility di storage adalah private.
  3. PUBLIC: Dapat dilihat oleh siapa saja. Physical visibility di storage akan otomatis menjadi public (memungkinkan akses langsung tanpa signed URL pada S3).

Security Policy (Permission-Only)

App\Policies\CuratorMediaPolicy menangani otorisasi menggunakan sistem Permission secara ketat (Best Practice Spatie & Filament Shield). Policy mengikuti konvensi penamaan {Action}:{Model} yang digunakan di proyek ini.

Logika Akses

  1. View:

    • Public/Member privacy diizinkan otomatis.
    • Private diizinkan jika punya permission View:CuratorMedia atau (pemilik dan punya ViewOwn:CuratorMedia).
  2. Update/Delete/Restore:

    • Diizinkan jika punya permission {Action}:CuratorMedia (Global).
    • Diizinkan jika pemilik (created_by) dan punya permission {Action}Own:CuratorMedia (Ownership).

Hal ini memungkinkan Admin untuk memiliki semua permission (misalnya Update:CuratorMedia), sedangkan Member hanya diberikan permission *Own (misalnya UpdateOwn:CuratorMedia) saja.

Proteksi Integritas

Aksi Delete dan Force Delete akan diblokir jika media masih digunakan, kecuali user memiliki permission override khusus.

Permission override yang dipakai:

  • DeleteUsed:CuratorMedia
  • ForceDeleteUsed:CuratorMedia

Integritas ini dijaga lewat CheckMediaUsageAction + CuratorMediaPolicy.


Pemilihan Privacy di Form

Untuk mendukung pemilihan privasi saat mengunggah atau mengedit media, starter kit menggunakan CustomMediaForm di app/Filament/Curator/CustomMediaForm.php.

Form ini menambahkan field Select untuk privasi:

Select::make('privacy')
->label('Privacy')
->options(Privacy::class)
->default(Privacy::PUBLIC)
->required()

Bagian 2: Media Usage Tracking & Integrity

Starter kit ini dilengkapi dengan sistem pelacakan penggunaan media secara eksplisit untuk menjamin integritas data, mencegah "Broken Images", dan mendukung siklus hidup aset yang aman.

Mengapa Menggunakan Usage Tracking?

Secara default, Media Manager tidak tahu di mana file media digunakan. Sistem pelacakan ini memberikan manfaat:

  1. Integritas Data: Mencegah penghapusan media yang masih digunakan oleh model lain (Post, User, dll).
  2. Audit Aset: Mengetahui dengan pasti aset mana saja yang masih aktif.
  3. Soft Delete & Restore: Mendukung fitur "Recycle Bin" di mana media yang dihapus tidak langsung hilang, namun otomatis tersembunyi dari relasi model yang menggunakannya.

Arsitektur Database: Tabel curator_media_usages

Tabel ini mencatat relasi antara media dan model yang menggunakannya secara Polymorphic:

  • curator_media_id (UUID): Referensi ke media.
  • model_id, model_type: Model domain (misalnya App\Models\Post).
  • field_name: Nama kolom (misalnya thumbnail_curator_id).

Action Pattern untuk Usage Tracking

Sistem ini menggunakan pola Action Pattern dengan method handle() dan DB::transaction untuk menjamin atomisitas data.

1. SyncMediaUsageAction

Menyinkronkan penggunaan media saat model dibuat atau diperbarui:

resolve(SyncMediaUsageAction::class)->handle($model, 'field_name', $curatorId);

2. DeleteCuratorMediaAction

Melakukan Soft Delete pada media dan mencatat siapa yang melakukan penghapusan (deleted_by):

resolve(DeleteCuratorMediaAction::class)->handle(
media: $media,
deleterId: $user->id,
allowDeleteWhenUsed: false,
);

3. DeleteAllMediaUsagesAction

Membersihkan catatan penggunaan media saat model pemilik (User/Post) benar-benar dihapus (Force Delete).

4. ListCuratorMediaUsagesAction

Mengambil daftar lokasi penggunaan media untuk kebutuhan audit di UI Curator:

resolve(ListCuratorMediaUsagesAction::class)->handle($media);

Proteksi Penghapusan

App\Policies\CuratorMediaPolicy menerapkan aturan industri:

  • Cegah Penghapusan Default: Media tidak dapat di-delete atau di-force delete jika statusnya masih tercatat digunakan (CheckMediaUsageAction).
  • Override Terbatas: Penghapusan media in-use hanya boleh lewat permission override (DeleteUsed:CuratorMedia / ForceDeleteUsed:CuratorMedia) atau permission global yang setara.
  • Otomatis Tersembunyi: Karena menggunakan SoftDeletes pada model CuratorMedia, relasi Eloquent (seperti $user->avatarMedia) secara otomatis akan mengembalikan null jika media tersebut sedang berada di "Recycle Bin" (di-soft delete). Jika di-restore, maka akan muncul kembali secara otomatis.

Selain proteksi di policy, starter kit ini juga menambahkan proteksi langsung di layer Filament Curator:

  • halaman edit media menampilkan warning kalau media masih dipakai
  • row delete action di tabel media diblok untuk media yang masih dipakai
  • bulk delete dibatalkan jika ada media terpilih yang masih dipakai

Jadi proteksinya berlapis:

  1. UI memberi warning dan mencegah aksi yang salah.
  2. Policy menolak delete in-use jika user tidak punya override.
  3. DeleteCuratorMediaAction tetap aman by default (allowDeleteWhenUsed=false).

Pendekatan ini dibuat defensif supaya integritas aset tidak bergantung pada satu lapisan saja.

Orkestrasi Wajib Lewat Action (Tanpa Observer)

Project ini sengaja tidak mengandalkan observer untuk sinkronisasi usage media agar alur bisnis lebih eksplisit dan mudah di-maintain.

Pola yang dipakai:

  1. Create/Update domain model memanggil Action domain (CreatePostAction, UpdatePostAction, CreateUserAction, UpdateUserAction).
  2. Action domain tersebut memanggil SyncMediaUsageAction.
  3. Delete domain model memanggil Action delete domain (DeletePostAction, dll) yang memanggil DeleteAllMediaUsagesAction.

Keuntungan:

  • Alur mudah ditelusuri dari controller/API/Filament.
  • Behavior konsisten antara panel dan API.
  • Tidak ada side-effect tersembunyi dari observer.

Catatan Naming di Closure Action Filament

Untuk code style internal project ini, service injection di closure action Filament sebaiknya tidak diberi nama $action jika closure tersebut sudah berada di context action Filament.

Contoh yang disarankan:

->using(fn (CuratorMedia $record, DeleteCuratorMediaAction $deleteMediaAction): bool => $deleteMediaAction->handle($record))

Alasannya:

  • $action secara mental sudah identik dengan action Filament
  • nama seperti $deleteMediaAction lebih mudah dipahami developer baru
  • mengurangi ambiguitas saat membaca closure before(), using(), action(), dan hook action lainnya

Pengujian (Testing)

Fitur ini telah dilindungi dengan unit & feature tests menggunakan Pest:

  • tests/Feature/CuratorMediaTest.php: Menguji ownership, soft delete, blameable, sinkronisasi privasi, dan logika akses per level privasi.
  • tests/Feature/MediaUsageTest.php: Menguji integritas tabel usage.

Untuk menjalankan tes:

php artisan test --compact --filter=CuratorMediaTest
php artisan test --compact --filter=MediaUsageTest