From 885a99b309ae3ba2d1a1fd7277609435b7b9a66c Mon Sep 17 00:00:00 2001 From: Arthur Monney Date: Sat, 11 Jan 2025 01:30:52 +0100 Subject: [PATCH 1/2] feat: add security header and optimize queries --- .../Controllers/User/DashboardController.php | 33 -------- app/Http/Middleware/CacheHeaders.php | 48 ++++++++++++ .../Security/ContentSecurityPolicy.php | 26 +++++++ .../Middleware/Security/PermissionsPolicy.php | 26 +++++++ .../Middleware/Security/ReferrerPolicy.php | 26 +++++++ .../Security/StrictTransportSecurity.php | 26 +++++++ app/Http/Middleware/Security/XFrameOption.php | 26 +++++++ app/Http/Middleware/TrackLastActivity.php | 28 ------- .../Components/Discussion/Comments.php | 2 +- app/Livewire/Components/User/Articles.php | 2 +- app/Livewire/Components/User/Discussions.php | 2 +- app/Livewire/Components/User/Threads.php | 2 +- app/Livewire/Pages/Account/Dashboard.php | 2 +- app/Livewire/Pages/Articles/Index.php | 13 +++- app/Livewire/Pages/Discussions/Index.php | 15 ++-- .../Pages/Discussions/SingleDiscussion.php | 2 +- app/Livewire/Pages/Forum/DetailThread.php | 7 +- app/Livewire/Pages/Forum/Index.php | 3 +- app/Livewire/Pages/Home.php | 6 +- app/Models/Discussion.php | 2 + app/Models/Thread.php | 1 + app/Policies/ArticlePolicy.php | 4 +- app/Policies/DiscussionPolicy.php | 10 +-- app/Policies/ReplyPolicy.php | 8 +- app/Policies/ThreadPolicy.php | 8 +- app/Providers/AppServiceProvider.php | 76 ++++++++++++------- .../Composers/InactiveDiscussionsComposer.php | 2 +- .../Composers/TopContributorsComposer.php | 2 +- bootstrap/app.php | 14 ++-- .../components/discussions/overview.blade.php | 4 +- .../forum/thread-channels.blade.php | 2 +- .../forum/thread-metadata.blade.php | 2 +- .../views/components/layouts/footer.blade.php | 4 +- .../livewire/components/reactions.blade.php | 8 +- .../pages/account/dashboard.blade.php | 4 +- .../livewire/pages/articles/index.blade.php | 2 +- 36 files changed, 304 insertions(+), 144 deletions(-) delete mode 100644 app/Http/Controllers/User/DashboardController.php create mode 100644 app/Http/Middleware/CacheHeaders.php create mode 100644 app/Http/Middleware/Security/ContentSecurityPolicy.php create mode 100644 app/Http/Middleware/Security/PermissionsPolicy.php create mode 100644 app/Http/Middleware/Security/ReferrerPolicy.php create mode 100644 app/Http/Middleware/Security/StrictTransportSecurity.php create mode 100644 app/Http/Middleware/Security/XFrameOption.php delete mode 100644 app/Http/Middleware/TrackLastActivity.php diff --git a/app/Http/Controllers/User/DashboardController.php b/app/Http/Controllers/User/DashboardController.php deleted file mode 100644 index 28f255ff..00000000 --- a/app/Http/Controllers/User/DashboardController.php +++ /dev/null @@ -1,33 +0,0 @@ - $user = User::scopes('withCounts')->find(Auth::id()), - 'threads' => $user->threads() - ->recent() - ->paginate(5), - ]); - } - - public function discussions(): View - { - return view('user.discussions', [ - 'user' => $user = User::scopes('withCounts')->find(Auth::id()), - 'discussions' => $user->discussions() - ->orderByDesc('created_at') - ->paginate(5), - ]); - } -} diff --git a/app/Http/Middleware/CacheHeaders.php b/app/Http/Middleware/CacheHeaders.php new file mode 100644 index 00000000..b9be3bea --- /dev/null +++ b/app/Http/Middleware/CacheHeaders.php @@ -0,0 +1,48 @@ +setCache( + options: [ + 'private' => true, + 'max_age' => 0, + 's_maxage' => 0, + 'no_store' => true, + ], + ); + } else { + $response->setCache( + options: [ + 'public' => true, + 'max_age' => 60, + 's_maxage' => 60, + ], + ); + + foreach ($response->headers->getCookies() as $cookie) { + $response->headers->removeCookie( + name: $cookie->getName(), + ); + } + } + + return $response; + } +} diff --git a/app/Http/Middleware/Security/ContentSecurityPolicy.php b/app/Http/Middleware/Security/ContentSecurityPolicy.php new file mode 100644 index 00000000..3344b29f --- /dev/null +++ b/app/Http/Middleware/Security/ContentSecurityPolicy.php @@ -0,0 +1,26 @@ +headers->add([ + 'Content-Security-Policy' => "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src *;", + ]); + + return $response; + } +} diff --git a/app/Http/Middleware/Security/PermissionsPolicy.php b/app/Http/Middleware/Security/PermissionsPolicy.php new file mode 100644 index 00000000..a1c1a9e1 --- /dev/null +++ b/app/Http/Middleware/Security/PermissionsPolicy.php @@ -0,0 +1,26 @@ +headers->add([ + 'Permissions-Policy' => 'geolocation=(self), microphone=()', + ]); + + return $response; + } +} diff --git a/app/Http/Middleware/Security/ReferrerPolicy.php b/app/Http/Middleware/Security/ReferrerPolicy.php new file mode 100644 index 00000000..3c553ad6 --- /dev/null +++ b/app/Http/Middleware/Security/ReferrerPolicy.php @@ -0,0 +1,26 @@ +headers->add([ + 'Referrer-Policy' => 'no-referrer', + ]); + + return $response; + } +} diff --git a/app/Http/Middleware/Security/StrictTransportSecurity.php b/app/Http/Middleware/Security/StrictTransportSecurity.php new file mode 100644 index 00000000..abab2f3a --- /dev/null +++ b/app/Http/Middleware/Security/StrictTransportSecurity.php @@ -0,0 +1,26 @@ +headers->add([ + 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload', + ]); + + return $response; + } +} diff --git a/app/Http/Middleware/Security/XFrameOption.php b/app/Http/Middleware/Security/XFrameOption.php new file mode 100644 index 00000000..029405e6 --- /dev/null +++ b/app/Http/Middleware/Security/XFrameOption.php @@ -0,0 +1,26 @@ +headers->add([ + 'X-Frame-Options' => 'deny', + ]); + + return $response; + } +} diff --git a/app/Http/Middleware/TrackLastActivity.php b/app/Http/Middleware/TrackLastActivity.php deleted file mode 100644 index 449114d2..00000000 --- a/app/Http/Middleware/TrackLastActivity.php +++ /dev/null @@ -1,28 +0,0 @@ -user()) { - $lastActive = $user->last_active_at; - - if (! $lastActive || $lastActive->diffInMinutes(Carbon::now()) >= 3) { - $user->update([ - 'last_active_at' => now(), - ]); - } - } - - return $next($request); - } -} diff --git a/app/Livewire/Components/Discussion/Comments.php b/app/Livewire/Components/Discussion/Comments.php index 58b524cf..9f9e4d5d 100644 --- a/app/Livewire/Components/Discussion/Comments.php +++ b/app/Livewire/Components/Discussion/Comments.php @@ -75,7 +75,7 @@ public function comments(): Collection $replies = collect(); // @phpstan-ignore-next-line - foreach ($this->discussion->replies->load(['allChildReplies', 'user']) as $reply) { + foreach ($this->discussion->replies->load(['allChildReplies', 'user', 'user.media']) as $reply) { /** @var Reply $reply */ if ($reply->allChildReplies->isNotEmpty()) { foreach ($reply->allChildReplies as $childReply) { diff --git a/app/Livewire/Components/User/Articles.php b/app/Livewire/Components/User/Articles.php index 6847d17f..e84661da 100644 --- a/app/Livewire/Components/User/Articles.php +++ b/app/Livewire/Components/User/Articles.php @@ -29,7 +29,7 @@ final class Articles extends Component implements HasActions, HasForms #[Computed] public function articles(): LengthAwarePaginator { - return Article::with(['user', 'tags', 'reactions']) + return Article::with('tags', 'reactions') ->where('user_id', Auth::id()) ->latest() ->paginate(10); diff --git a/app/Livewire/Components/User/Discussions.php b/app/Livewire/Components/User/Discussions.php index c4f64797..f00f6963 100644 --- a/app/Livewire/Components/User/Discussions.php +++ b/app/Livewire/Components/User/Discussions.php @@ -32,7 +32,7 @@ final class Discussions extends Component implements HasActions, HasForms #[Computed] public function discussions(): LengthAwarePaginator { - return Discussion::with('user') + return Discussion::with('tags', 'replies') ->where('user_id', Auth::id()) ->latest() ->paginate(10); diff --git a/app/Livewire/Components/User/Threads.php b/app/Livewire/Components/User/Threads.php index a04b57d9..618108f9 100644 --- a/app/Livewire/Components/User/Threads.php +++ b/app/Livewire/Components/User/Threads.php @@ -32,7 +32,7 @@ final class Threads extends Component implements HasActions, HasForms #[Computed] public function threads(): LengthAwarePaginator { - return Thread::with('user') + return Thread::with('channels', 'solutionReply', 'replies') ->scopes('withViewsCount') ->where('user_id', Auth::id()) ->latest() diff --git a/app/Livewire/Pages/Account/Dashboard.php b/app/Livewire/Pages/Account/Dashboard.php index dede46b7..c411c4d3 100644 --- a/app/Livewire/Pages/Account/Dashboard.php +++ b/app/Livewire/Pages/Account/Dashboard.php @@ -18,7 +18,7 @@ final class Dashboard extends Component #[Computed] public function user(): User { - return User::query() + return User::with('providers') ->scopes('withCounts') ->find(Auth::id()); } diff --git a/app/Livewire/Pages/Articles/Index.php b/app/Livewire/Pages/Articles/Index.php index 34c9c0ef..2f64528f 100644 --- a/app/Livewire/Pages/Articles/Index.php +++ b/app/Livewire/Pages/Articles/Index.php @@ -8,6 +8,7 @@ use App\Models\Tag; use App\Traits\WithLocale; use Illuminate\Contracts\View\View; +use Illuminate\Support\Facades\Cache; use Livewire\Component; use Livewire\WithoutUrlPagination; use Livewire\WithPagination; @@ -26,7 +27,7 @@ public function mount(): void public function render(): View { return view('livewire.pages.articles.index', [ - 'articles' => Article::with(['tags', 'user', 'user.transactions']) // @phpstan-ignore-line + 'articles' => Article::with(['tags', 'user', 'user.transactions', 'user.media', 'media']) // @phpstan-ignore-line ->withCount(['views', 'reactions']) ->orderByDesc('sponsored_at') ->orderByDesc('published_at') @@ -35,9 +36,13 @@ public function render(): View ->forLocale($this->locale) ->simplePaginate(21), - 'tags' => Tag::query()->whereHas('articles', function ($query): void { - $query->published(); - })->orderBy('name')->get(), + 'tags' => Cache::remember( + key: 'articles.tags', + ttl: now()->addWeek(), + callback: fn () => Tag::query()->whereHas('articles', function ($query): void { + $query->published(); + })->orderBy('name')->get() + ), ]) ->title(__('pages/article.title')); } diff --git a/app/Livewire/Pages/Discussions/Index.php b/app/Livewire/Pages/Discussions/Index.php index 977384b5..3413f7d5 100644 --- a/app/Livewire/Pages/Discussions/Index.php +++ b/app/Livewire/Pages/Discussions/Index.php @@ -9,6 +9,7 @@ use App\Models\Tag; use App\Traits\WithLocale; use Illuminate\Contracts\View\View; +use Illuminate\Support\Facades\Cache; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithoutUrlPagination; @@ -57,15 +58,19 @@ public function tagExists(string $tag): bool public function render(): View { /** @var DiscussionQueryBuilder $query */ - $query = Discussion::with(['tags', 'replies', 'user']) // @phpstan-ignore-line + $query = Discussion::with(['tags', 'replies', 'user', 'user.media', 'reactions']) // @phpstan-ignore-line ->withCount('replies') ->forLocale($this->locale) ->notPinned(); - $tags = Tag::query() - ->whereJsonContains('concerns', ['discussion']) - ->orderBy('name') - ->get(); + $tags = Cache::remember( + key: 'discussions.tags', + ttl: now()->addWeek(), + callback: fn () => Tag::query() + ->whereJsonContains('concerns', ['discussion']) + ->orderBy('name') + ->get() + ); if ($this->currentTag) { $query->forTag($this->currentTag); // @phpstan-ignore-line diff --git a/app/Livewire/Pages/Discussions/SingleDiscussion.php b/app/Livewire/Pages/Discussions/SingleDiscussion.php index 47da73e5..2d54a76c 100644 --- a/app/Livewire/Pages/Discussions/SingleDiscussion.php +++ b/app/Livewire/Pages/Discussions/SingleDiscussion.php @@ -36,7 +36,7 @@ public function mount(): void ->twitterDescription($this->discussion->excerpt(100)) ->withUrl(); - $this->discussion->load('tags', 'replies', 'reactions', 'replies.user', 'user'); + $this->discussion->load('tags', 'replies', 'reactions', 'user', 'user.media'); } public function editAction(): Action diff --git a/app/Livewire/Pages/Forum/DetailThread.php b/app/Livewire/Pages/Forum/DetailThread.php index 359fae3b..23b3e74d 100644 --- a/app/Livewire/Pages/Forum/DetailThread.php +++ b/app/Livewire/Pages/Forum/DetailThread.php @@ -24,11 +24,12 @@ final class DetailThread extends Component implements HasActions, HasForms public Thread $thread; - public function mount(Thread $thread): void + public function mount(): void { - views($thread)->cooldown(now()->addHour())->record(); + views($this->thread)->cooldown(now()->addHour())->record(); - $this->thread = $thread->loadCount('views'); + $this->thread->load('channels', 'channels.parent', 'user', 'user.media', 'replies', 'solutionReply') + ->loadCount('views'); } public function editAction(): Action diff --git a/app/Livewire/Pages/Forum/Index.php b/app/Livewire/Pages/Forum/Index.php index 7992b61d..c0ea821b 100644 --- a/app/Livewire/Pages/Forum/Index.php +++ b/app/Livewire/Pages/Forum/Index.php @@ -171,7 +171,8 @@ protected function applySorting(Builder $query): Builder public function render(): View { - $query = Thread::with(['channels', 'user']); + $query = Thread::with(['channels', 'user', 'user.media']) + ->withCount('replies'); $query = $this->applyChannel($query); $query = $this->applySearch($query); diff --git a/app/Livewire/Pages/Home.php b/app/Livewire/Pages/Home.php index f4a25232..e33bfeef 100644 --- a/app/Livewire/Pages/Home.php +++ b/app/Livewire/Pages/Home.php @@ -28,7 +28,7 @@ public function render(): View 'latestArticles' => Cache::remember( key: 'latestArticles', ttl: $ttl, - callback: fn (): Collection => Article::with(['tags', 'user', 'user.transactions']) // @phpstan-ignore-line + callback: fn (): Collection => Article::with(['tags', 'media', 'user', 'user.transactions', 'user.media']) // @phpstan-ignore-line ->published() ->orderByDesc('sponsored_at') ->orderByDesc('published_at') @@ -40,7 +40,7 @@ public function render(): View 'latestThreads' => Cache::remember( key: 'latestThreads', ttl: $ttl, - callback: fn (): Collection => Thread::with(['user', 'user.transactions']) + callback: fn (): Collection => Thread::with(['user', 'user.transactions', 'user.media']) ->whereNull('solution_reply_id') ->whereBetween('threads.created_at', [now()->subMonths(3), now()]) ->inRandomOrder() @@ -50,7 +50,7 @@ public function render(): View 'latestDiscussions' => Cache::remember( key: 'latestDiscussions', ttl: $ttl, - callback: fn (): Collection => Discussion::with(['user', 'user.transactions']) // @phpstan-ignore-line + callback: fn (): Collection => Discussion::with(['user', 'user.transactions', 'user.media']) // @phpstan-ignore-line ->recent() ->orderByViews() ->limit(3) diff --git a/app/Models/Discussion.php b/app/Models/Discussion.php index cd96c408..df6a3282 100644 --- a/app/Models/Discussion.php +++ b/app/Models/Discussion.php @@ -44,6 +44,8 @@ * @property User $user * @property Collection | SpamReport[] $spamReports * @property Collection | Reply[] $replies + * @property Collection | Tag[] $tags + * @property Collection | Reaction[] $reactions */ final class Discussion extends Model implements ReactableInterface, ReplyInterface, Sitemapable, SpamReportableContract, SubscribeInterface, Viewable { diff --git a/app/Models/Thread.php b/app/Models/Thread.php index 6e5573cc..c139cd0f 100644 --- a/app/Models/Thread.php +++ b/app/Models/Thread.php @@ -52,6 +52,7 @@ * @property User $user * @property Reply | null $solutionReply * @property \Illuminate\Database\Eloquent\Collection | Channel[] $channels + * @property \Illuminate\Database\Eloquent\Collection | Reply[] $replies */ final class Thread extends Model implements Feedable, ReactableInterface, ReplyInterface, SpamReportableContract, SubscribeInterface, Viewable { diff --git a/app/Policies/ArticlePolicy.php b/app/Policies/ArticlePolicy.php index 5dfa732f..a50e6ed2 100644 --- a/app/Policies/ArticlePolicy.php +++ b/app/Policies/ArticlePolicy.php @@ -19,12 +19,12 @@ public function create(User $user): bool public function update(User $user, Article $article): bool { - return $article->isAuthoredBy($user); + return $article->user_id === $user->id; } public function delete(User $user, Article $article): bool { - return $article->isAuthoredBy($user) || $user->isModerator() || $user->isAdmin(); + return $article->user_id === $user->id || $user->isModerator() || $user->isAdmin(); } public function approve(User $user, Article $article): bool diff --git a/app/Policies/DiscussionPolicy.php b/app/Policies/DiscussionPolicy.php index a5aa88dd..1e9af331 100644 --- a/app/Policies/DiscussionPolicy.php +++ b/app/Policies/DiscussionPolicy.php @@ -19,17 +19,17 @@ public function create(User $user): bool public function manage(User $user, Discussion $discussion): bool { - return $discussion->isAuthoredBy($user) || $user->isModerator() || $user->isAdmin(); + return $discussion->user_id === $user->id || $user->isModerator() || $user->isAdmin(); } public function update(User $user, Discussion $discussion): bool { - return $discussion->isAuthoredBy($user); + return $discussion->user_id === $user->id; } public function delete(User $user, Discussion $discussion): bool { - return $discussion->isAuthoredBy($user) || $user->isModerator() || $user->isAdmin(); + return $discussion->user_id === $user->id || $user->isModerator() || $user->isAdmin(); } public function togglePinnedStatus(User $user, Discussion $discussion): bool @@ -49,11 +49,11 @@ public function unsubscribe(User $user, Discussion $discussion): bool public function report(User $user, Discussion $discussion): bool { - return $user->hasVerifiedEmail() && ! $discussion->isAuthoredBy($user); + return $user->hasVerifiedEmail() && $discussion->user_id !== $user->id; } public function convertedToThread(User $user, Discussion $discussion): bool { - return $discussion->isAuthoredBy($user) || $user->isAdmin(); + return $discussion->user_id === $user->id || $user->isAdmin(); } } diff --git a/app/Policies/ReplyPolicy.php b/app/Policies/ReplyPolicy.php index cbd75faf..1ce0494c 100644 --- a/app/Policies/ReplyPolicy.php +++ b/app/Policies/ReplyPolicy.php @@ -14,7 +14,7 @@ final class ReplyPolicy public function manage(User $user, Reply $reply): bool { - return $reply->isAuthoredBy($user) || $user->isModerator() || $user->isAdmin(); + return $reply->user_id === $user->id || $user->isModerator() || $user->isAdmin(); } public function create(User $user): bool @@ -24,17 +24,17 @@ public function create(User $user): bool public function update(User $user, Reply $reply): bool { - return $reply->isAuthoredBy($user) && $user->hasVerifiedEmail(); + return $reply->user_id === $user->id && $user->hasVerifiedEmail(); } public function delete(User $user, Reply $reply): bool { - return $reply->isAuthoredBy($user) || $user->isModerator() || $user->isAdmin(); + return $reply->user_id === $user->id || $user->isModerator() || $user->isAdmin(); } public function report(User $user, Reply $reply): bool { - return $user->hasVerifiedEmail() && ! $reply->isAuthoredBy($user); + return $user->hasVerifiedEmail() && $reply->user_id !== $user->id; } public function like(User $user, Reply $reply): bool diff --git a/app/Policies/ThreadPolicy.php b/app/Policies/ThreadPolicy.php index 702a2531..3d9bbfe1 100644 --- a/app/Policies/ThreadPolicy.php +++ b/app/Policies/ThreadPolicy.php @@ -19,17 +19,17 @@ public function create(User $user): bool public function manage(User $user, Thread $thread): bool { - return $thread->isAuthoredBy($user) || $user->isModerator() || $user->isAdmin(); + return $thread->user_id === $user->id || $user->isModerator() || $user->isAdmin(); } public function update(User $user, Thread $thread): bool { - return $thread->isAuthoredBy($user); + return $thread->user_id === $user->id; } public function delete(User $user, Thread $thread): bool { - return $thread->isAuthoredBy($user) || $user->isModerator() || $user->isAdmin(); + return $thread->user_id === $user->id || $user->isModerator() || $user->isAdmin(); } public function subscribe(User $user, Thread $thread): bool @@ -44,6 +44,6 @@ public function unsubscribe(User $user, Thread $thread): bool public function report(User $user, Thread $thread): bool { - return $user->hasVerifiedEmail() && ! $thread->isAuthoredBy($user); + return $user->hasVerifiedEmail() && $thread->user_id !== $user->id; } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ee25d4e9..58cb5fcb 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -21,6 +21,8 @@ use Filament\Tables; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; @@ -35,30 +37,13 @@ public function register(): void public function boot(): void { - $this->bootMacros(); $this->bootViewsComposer(); - $this->bootEloquentMorphs(); - $this->bootFilament(); - - // @phpstan-ignore-next-line - seo() - ->title( - default: __('pages/home.title'), - modify: fn (string $title) => $title.' | '.__('global.site_name') - ) - ->description(default: __('global.site_description')) - ->image(default: fn () => asset('images/socialcard.png')) - ->twitterSite('@laravelcm'); - - FilamentColor::register([ - 'primary' => Color::Emerald, - 'danger' => Color::Red, - 'info' => Color::Blue, - 'success' => Color::Green, - 'warning' => Color::Amber, - ]); - - ReplyResource::withoutWrapping(); + $this->configureMacros(); + $this->configureEloquent(); + $this->configureFilament(); + $this->configureSeo(); + $this->configureCommands(); + $this->configureUrl(); } public function registerBladeDirective(): void @@ -68,7 +53,7 @@ public function registerBladeDirective(): void Blade::directive('canonical', fn ($expression) => ""); } - public function bootMacros(): void + public function configureMacros(): void { Str::macro('readDuration', function (...$text) { $totalWords = str_word_count(implode(' ', $text)); @@ -85,8 +70,10 @@ public function bootViewsComposer(): void View::composer('components.profile-users', ProfileUsersComposer::class); } - public function bootEloquentMorphs(): void + protected function configureEloquent(): void { + ReplyResource::withoutWrapping(); + Relation::morphMap([ 'article' => Article::class, 'discussion' => Discussion::class, @@ -96,8 +83,16 @@ public function bootEloquentMorphs(): void ]); } - public function bootFilament(): void + protected function configureFilament(): void { + FilamentColor::register([ + 'primary' => Color::Emerald, + 'danger' => Color::Red, + 'info' => Color::Blue, + 'success' => Color::Green, + 'warning' => Color::Amber, + ]); + FilamentIcon::register([ 'panels::pages.dashboard.navigation-item' => 'untitledui-home-line', 'actions::delete-action' => 'untitledui-trash-03', @@ -119,7 +114,7 @@ public function bootFilament(): void Tables\Actions\DeleteAction::configureUsing(fn (Tables\Actions\Action $action) => $action->icon('untitledui-trash-03')); } - public function registerLocaleDate(): void + protected function registerLocaleDate(): void { date_default_timezone_set('Africa/Douala'); setlocale(LC_TIME, 'fr_FR', 'fr', 'FR', 'French', 'fr_FR.UTF-8'); @@ -127,4 +122,31 @@ public function registerLocaleDate(): void Carbon::setLocale('fr'); } + + protected function configureSeo(): void + { + // @phpstan-ignore-next-line + seo() + ->title( + default: __('pages/home.title'), + modify: fn (string $title) => $title.' | '.__('global.site_name') + ) + ->description(default: __('global.site_description')) + ->image(default: fn () => asset('images/socialcard.png')) + ->twitterSite('@laravelcm'); + } + + protected function configureCommands(): void + { + DB::prohibitDestructiveCommands( + $this->app->isProduction(), + ); + } + + protected function configureUrl(): void + { + if (! $this->app->isLocal()) { + URL::forceScheme('https'); + } + } } diff --git a/app/View/Composers/InactiveDiscussionsComposer.php b/app/View/Composers/InactiveDiscussionsComposer.php index 9b316ca8..4bdac45d 100644 --- a/app/View/Composers/InactiveDiscussionsComposer.php +++ b/app/View/Composers/InactiveDiscussionsComposer.php @@ -15,7 +15,7 @@ public function compose(View $view): void $discussions = Cache::remember( key: 'inactive_discussions', ttl: now()->addWeek(), - callback: fn () => Discussion::with('user')->noComments()->limit(5)->get() + callback: fn () => Discussion::with('user', 'user.media')->noComments()->limit(5)->get() ); $view->with('discussions', $discussions); diff --git a/app/View/Composers/TopContributorsComposer.php b/app/View/Composers/TopContributorsComposer.php index c0855415..ba0fda34 100644 --- a/app/View/Composers/TopContributorsComposer.php +++ b/app/View/Composers/TopContributorsComposer.php @@ -15,7 +15,7 @@ public function compose(View $view): void $topContributors = Cache::remember( key: 'contributors', ttl: now()->addWeek(), - callback: fn () => User::query()->scopes('topContributors') + callback: fn () => User::with('media')->scopes('topContributors') ->get() ->filter(fn (User $contributor) => $contributor->discussions_count >= 1) ->take(5) diff --git a/bootstrap/app.php b/bootstrap/app.php index 2522bac8..be81be28 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use App\Http\Middleware\LocaleMiddleware; +use App\Http\Middleware as AppMiddleware; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; @@ -15,12 +15,16 @@ ) ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ - 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, - 'checkIfBanned' => \App\Http\Middleware\CheckIfBanned::class, + 'checkIfBanned' => AppMiddleware\CheckIfBanned::class, ]); $middleware->web(append: [ - LocaleMiddleware::class, - \App\Http\Middleware\TrackLastActivity::class, + AppMiddleware\LocaleMiddleware::class, + AppMiddleware\CacheHeaders::class, + // AppMiddleware\Security\ContentSecurityPolicy::class, + AppMiddleware\Security\PermissionsPolicy::class, + AppMiddleware\Security\ReferrerPolicy::class, + AppMiddleware\Security\StrictTransportSecurity::class, + AppMiddleware\Security\XFrameOption::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/resources/views/components/discussions/overview.blade.php b/resources/views/components/discussions/overview.blade.php index 0ac7d15d..ea781235 100644 --- a/resources/views/components/discussions/overview.blade.php +++ b/resources/views/components/discussions/overview.blade.php @@ -49,7 +49,7 @@ class="text-gray-700 dark:text-gray-300 font-medium hover:underline"
- +
- +
diff --git a/resources/views/livewire/pages/articles/index.blade.php b/resources/views/livewire/pages/articles/index.blade.php index 7609f2c2..9e5469ab 100644 --- a/resources/views/livewire/pages/articles/index.blade.php +++ b/resources/views/livewire/pages/articles/index.blade.php @@ -112,7 +112,7 @@ class="flex size-8 items-center justify-center rounded-full text-gray-400 transi - +
@foreach ($articles as $article) From 51cffa6ad8725f993ace3e536c6aad72ece50f0a Mon Sep 17 00:00:00 2001 From: Arthur Monney Date: Sat, 11 Jan 2025 01:37:08 +0100 Subject: [PATCH 2/2] fix: user activities test --- tests/Feature/UserActivitiesTest.php | 63 +++++++++++++--------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/tests/Feature/UserActivitiesTest.php b/tests/Feature/UserActivitiesTest.php index 61a87e0a..3289a89d 100644 --- a/tests/Feature/UserActivitiesTest.php +++ b/tests/Feature/UserActivitiesTest.php @@ -5,47 +5,42 @@ use App\Models\Activity; use App\Models\Article; use Carbon\Carbon; -use Illuminate\Support\Facades\Route; -it('records activity when an article is created', function (): void { - $user = $this->login(); - - $article = Article::factory(['user_id' => $user->id])->create(); - $activity = Activity::query()->first(); - - expect($activity->subject->id) - ->toEqual($article->id) - ->and(Activity::query()->count()) - ->toEqual(1); +/** + * @var \Tests\TestCase $this + */ +beforeEach(function (): void { + $this->user = $this->login(); }); -it('get feed from any user', function (): void { - $user = $this->login(); - - Article::factory(['user_id' => $user->id]) - ->count(2) - ->create(); +describe('User Activities', function (): void { + it('records activity when an article is created', function (): void { + $article = Article::factory(['user_id' => $this->user->id])->create(); + $activity = Activity::query()->first(); - $user->activities() - ->first() - ->update(['created_at' => Carbon::now()->subWeek()]); + expect($activity->subject->id) + ->toEqual($article->id) + ->and(Activity::query()->count()) + ->toEqual(1); + }); - $feed = Activity::feed($user); + it('get feed from any user', function (): void { + Article::factory(['user_id' => $this->user->id]) + ->count(2) + ->create(); - $this->assertTrue($feed->keys()->contains( - Carbon::now()->format('Y-m-d') - )); - - $this->assertFalse($feed->keys()->contains( - Carbon::now()->subWeek()->format('Y-m-d') - )); -}); + $this->user->activities() + ->first() + ->update(['created_at' => Carbon::now()->subWeek()]); -it('does not update the last activity for unauthenticated users', function (): void { - Route::middleware(\App\Http\Middleware\TrackLastActivity::class) - ->get('/activity-user', fn () => 'ok'); + $feed = Activity::feed($this->user); - $this->get('/activity-user')->assertOk(); + $this->assertTrue($feed->keys()->contains( + Carbon::now()->format('Y-m-d') + )); - $this->assertDatabaseMissing('users', ['last_active_at' => now()]); + $this->assertFalse($feed->keys()->contains( + Carbon::now()->subWeek()->format('Y-m-d') + )); + }); });