diff --git a/app/Livewire/Customer/Course/Index.php b/app/Livewire/Customer/Course/Index.php index 9fff27cd..adcc496e 100644 --- a/app/Livewire/Customer/Course/Index.php +++ b/app/Livewire/Customer/Course/Index.php @@ -15,15 +15,22 @@ #[Title('Course')] class Index extends Component { + #[Computed] + public function isAdmin(): bool + { + return auth()->user()?->isAdmin() ?? false; + } + #[Computed] public function course(): ?Course { - return Course::where('is_published', true) + return Course::query() + ->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true)) ->with(['modules' => function ($query) { - $query->where('is_published', true) + $query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true)) ->orderBy('sort_order') ->with(['lessons' => function ($query) { - $query->where('is_published', true)->orderBy('sort_order'); + $query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))->orderBy('sort_order'); }]); }]) ->first(); diff --git a/app/Livewire/Customer/Course/LessonShow.php b/app/Livewire/Customer/Course/LessonShow.php index f1c38b15..6f563945 100644 --- a/app/Livewire/Customer/Course/LessonShow.php +++ b/app/Livewire/Customer/Course/LessonShow.php @@ -6,6 +6,7 @@ use App\Models\CourseLesson; use App\Models\LessonProgress; use App\Models\Product; +use Illuminate\Database\Eloquent\Collection; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; use Livewire\Component; @@ -19,19 +20,27 @@ public function mount(CourseLesson $lesson): void { $this->lesson = $lesson->load('module.course'); - if (! $this->lesson->is_free && ! $this->hasPurchased) { + abort_unless($this->lesson->is_published || $this->isAdmin, 404); + + if (! $this->lesson->is_free && ! $this->hasPurchased && ! $this->isAdmin) { abort(403, 'You need Pro access to view this lesson.'); } } + #[Computed] + public function isAdmin(): bool + { + return auth()->user()?->isAdmin() ?? false; + } + #[Computed] public function course(): Course { return $this->lesson->module->course->load(['modules' => function ($query) { - $query->where('is_published', true) + $query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true)) ->orderBy('sort_order') ->with(['lessons' => function ($query) { - $query->where('is_published', true)->orderBy('sort_order'); + $query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))->orderBy('sort_order'); }]); }]); } @@ -44,6 +53,17 @@ public function hasPurchased(): bool return $product && $product->isOwnedBy(auth()->user()); } + /** + * @return Collection + */ + #[Computed] + public function moduleLessons(): Collection + { + return $this->lesson->module->lessons() + ->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true)) + ->get(); + } + #[Computed] public function completedLessonIds(): array { diff --git a/resources/views/livewire/customer/course.blade.php b/resources/views/livewire/customer/course.blade.php index 65545f2c..24bc8498 100644 --- a/resources/views/livewire/customer/course.blade.php +++ b/resources/views/livewire/customer/course.blade.php @@ -1,9 +1,8 @@
- @if ($this->hasPurchased) - {{-- Purchased: Show course content --}} + @if ($this->hasPurchased || $this->isAdmin) + {{-- Purchased (or admin preview): Show course content --}}
- Course - The NativePHP Masterclass + The NativePHP Masterclass
@if ($this->course) @@ -55,6 +54,9 @@ class="h-2 rounded-full bg-emerald-500 transition-all" @else Pro @endif + @if (! $module->is_published) + Draft + @endif
@if ($module->description) {{ $module->description }} @@ -79,6 +81,9 @@ class="h-2 rounded-full bg-emerald-500 transition-all" {{ $lesson->title }} + @if (! $lesson->is_published) + Draft + @endif @if ($lesson->duration_in_seconds) {{ gmdate('i:s', $lesson->duration_in_seconds) }} @endif diff --git a/resources/views/livewire/customer/course/lesson-show.blade.php b/resources/views/livewire/customer/course/lesson-show.blade.php index 3edaf9df..30a47ce2 100644 --- a/resources/views/livewire/customer/course/lesson-show.blade.php +++ b/resources/views/livewire/customer/course/lesson-show.blade.php @@ -29,6 +29,9 @@ class="absolute inset-0 w-full h-full" @else Pro @endif + @if (! $lesson->is_published) + Draft + @endif {{ $lesson->module->title }} {{ $lesson->title }} @@ -69,17 +72,16 @@ class="shrink-0 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-semibo {{-- Other lessons in this module --}} - @php $moduleLessons = $lesson->module->lessons()->where('is_published', true)->orderBy('sort_order')->get(); @endphp - @if ($moduleLessons->count() > 1) + @if ($this->moduleLessons->count() > 1)
{{ $lesson->module->title }}
- @foreach ($moduleLessons as $moduleLesson) + @foreach ($this->moduleLessons as $moduleLesson) @php $isCurrent = $moduleLesson->id === $lesson->id; $isLessonComplete = in_array($moduleLesson->id, $this->completedLessonIds); - $canAccess = $moduleLesson->is_free || $this->hasPurchased; + $canAccess = $moduleLesson->is_free || $this->hasPurchased || $this->isAdmin; @endphp @if ($canAccess) {{ $moduleLesson->title }} + @if (! $moduleLesson->is_published) + Draft + @endif @if ($moduleLesson->duration_in_seconds) {{ gmdate('i:s', $moduleLesson->duration_in_seconds) }} @endif diff --git a/tests/Feature/CourseContentTest.php b/tests/Feature/CourseContentTest.php index 0e4023d6..4cfca403 100644 --- a/tests/Feature/CourseContentTest.php +++ b/tests/Feature/CourseContentTest.php @@ -170,6 +170,7 @@ public function course_dashboard_shows_modules_and_lessons_for_owners(): void Livewire::actingAs($user) ->test(Index::class) + ->assertSee('The NativePHP Masterclass') ->assertSee('Test Module') ->assertSee('Test Lesson'); } @@ -280,6 +281,101 @@ public function user_can_uncomplete_a_lesson(): void ]); } + #[Test] + public function admin_sees_unpublished_course_content_in_dashboard(): void + { + config(['filament.users' => ['admin@test.com']]); + $admin = User::factory()->create(['email' => 'admin@test.com']); + + $course = Course::factory()->create(); + $module = CourseModule::factory()->create([ + 'course_id' => $course->id, + 'title' => 'Hidden Module', + ]); + CourseLesson::factory()->create([ + 'course_module_id' => $module->id, + 'title' => 'Hidden Lesson', + ]); + + Livewire::actingAs($admin) + ->test(Index::class) + ->assertSee('Hidden Module') + ->assertSee('Hidden Lesson') + ->assertSee('Draft'); + } + + #[Test] + public function non_admin_owner_does_not_see_unpublished_content_in_dashboard(): void + { + $user = User::factory()->create(); + $product = Product::where('slug', 'nativephp-masterclass')->first(); + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + $course = Course::factory()->published()->create(); + $liveModule = CourseModule::factory()->published()->create([ + 'course_id' => $course->id, + 'title' => 'Live Module', + ]); + CourseLesson::factory()->create([ + 'course_module_id' => $liveModule->id, + 'title' => 'Hidden Lesson', + ]); + CourseModule::factory()->create([ + 'course_id' => $course->id, + 'title' => 'Hidden Module', + ]); + + Livewire::actingAs($user) + ->test(Index::class) + ->assertSee('Live Module') + ->assertDontSee('Hidden Lesson') + ->assertDontSee('Hidden Module'); + } + + #[Test] + public function admin_can_view_unpublished_pro_lesson_without_purchase(): void + { + config(['filament.users' => ['admin@test.com']]); + $admin = User::factory()->create(['email' => 'admin@test.com']); + + $course = Course::factory()->create(); + $module = CourseModule::factory()->create(['course_id' => $course->id]); + $lesson = CourseLesson::factory()->create([ + 'course_module_id' => $module->id, + 'is_free' => false, + 'title' => 'Hidden Pro Lesson', + ]); + + Livewire::actingAs($admin) + ->test(LessonShow::class, ['lesson' => $lesson]) + ->assertSee('Hidden Pro Lesson') + ->assertSee('Draft'); + } + + #[Test] + public function unpublished_lesson_is_not_accessible_to_non_admins(): void + { + $user = User::factory()->create(); + $product = Product::where('slug', 'nativephp-masterclass')->first(); + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + $course = Course::factory()->published()->create(); + $module = CourseModule::factory()->published()->create(['course_id' => $course->id]); + $lesson = CourseLesson::factory()->create([ + 'course_module_id' => $module->id, + ]); + + Livewire::actingAs($user) + ->test(LessonShow::class, ['lesson' => $lesson]) + ->assertNotFound(); + } + #[Test] public function course_model_has_modules_relationship(): void {