diff --git a/app/Actions/Models/List/ExternalProfile/ExternalEntry/Token/AnilistExternalEntryTokenAction.php b/app/Actions/Models/List/ExternalProfile/ExternalEntry/Token/AnilistExternalEntryTokenAction.php index 4d08895ca..b5a69e185 100644 --- a/app/Actions/Models/List/ExternalProfile/ExternalEntry/Token/AnilistExternalEntryTokenAction.php +++ b/app/Actions/Models/List/ExternalProfile/ExternalEntry/Token/AnilistExternalEntryTokenAction.php @@ -88,6 +88,8 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction * Make the request to the external api. * * @return static + * + * @throws RequestException */ protected function makeRequest(): static { diff --git a/app/Actions/Models/List/ExternalProfile/ExternalToken/Site/MalExternalTokenAction.php b/app/Actions/Models/List/ExternalProfile/ExternalToken/Site/MalExternalTokenAction.php index 47ea1e1b1..8749e9713 100644 --- a/app/Actions/Models/List/ExternalProfile/ExternalToken/Site/MalExternalTokenAction.php +++ b/app/Actions/Models/List/ExternalProfile/ExternalToken/Site/MalExternalTokenAction.php @@ -55,10 +55,6 @@ class MalExternalTokenAction extends BaseExternalTokenAction throw new Error('Failed to get token'); } - return ExternalToken::query()->create([ - ExternalToken::ATTRIBUTE_ACCESS_TOKEN => Crypt::encrypt($token), - ExternalToken::ATTRIBUTE_REFRESH_TOKEN => Crypt::encrypt($refreshToken), - ]); } catch (Exception $e) { Log::error($e->getMessage()); @@ -66,5 +62,10 @@ class MalExternalTokenAction extends BaseExternalTokenAction } finally { Cache::forget("mal-external-token-request-{$state}"); } + + return ExternalToken::query()->create([ + ExternalToken::ATTRIBUTE_ACCESS_TOKEN => Crypt::encrypt($token), + ExternalToken::ATTRIBUTE_REFRESH_TOKEN => Crypt::encrypt($refreshToken), + ]); } } diff --git a/app/Actions/Models/List/ExternalProfile/SyncExternalProfileAction.php b/app/Actions/Models/List/ExternalProfile/SyncExternalProfileAction.php index 640a5ba60..270f2262d 100644 --- a/app/Actions/Models/List/ExternalProfile/SyncExternalProfileAction.php +++ b/app/Actions/Models/List/ExternalProfile/SyncExternalProfileAction.php @@ -7,12 +7,11 @@ namespace App\Actions\Models\List\ExternalProfile; use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryAction; use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryTokenAction; use App\Enums\Models\List\ExternalProfileSite; -use App\Enums\NotificationType; +use App\Events\List\ExternalProfile\ExternalProfileSynced; use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\Wiki\Anime; use App\Models\Wiki\ExternalResource; -use App\Notifications\UserNotification; use Exception; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -47,7 +46,7 @@ class SyncExternalProfileAction $entries = $action->getEntries(); - $this->preloadResources($profile->site, $entries); + $this->cacheResources($profile->site); $externalEntries = []; foreach ($entries as $entry) { @@ -58,7 +57,7 @@ class SyncExternalProfileAction ExternalEntry::ATTRIBUTE_SCORE => Arr::get($entry, ExternalEntry::ATTRIBUTE_SCORE), ExternalEntry::ATTRIBUTE_IS_FAVORITE => Arr::get($entry, ExternalEntry::ATTRIBUTE_IS_FAVORITE), ExternalEntry::ATTRIBUTE_WATCH_STATUS => Arr::get($entry, ExternalEntry::ATTRIBUTE_WATCH_STATUS), - ExternalEntry::ATTRIBUTE_ANIME => $anime->getKey(), + ExternalEntry::ATTRIBUTE_ANIME => $anime, ExternalEntry::ATTRIBUTE_PROFILE => $profile->getKey(), ]; } @@ -70,11 +69,7 @@ class SyncExternalProfileAction DB::commit(); - $profile->user?->notifyNow(new UserNotification( - 'External Profile Synced', - "Your external profile [{$profile->getName()}]({$profile->getClientUrl()}) has been synced.", - NotificationType::SYNCED_PROFILE, - )); + ExternalProfileSynced::dispatch($profile); return $profile; } catch (Exception $e) { @@ -113,21 +108,20 @@ class SyncExternalProfileAction } /** - * Preload the resources for performance proposals. + * Cache the resources for performance proposals. * * @param ExternalProfileSite $profileSite - * @param array $entries * @return void */ - protected function preloadResources(ExternalProfileSite $profileSite, array $entries): void + protected function cacheResources(ExternalProfileSite $profileSite): void { - $this->resources = Cache::flexible("externalprofile_resources", [60, 300], function () use ($profileSite, $entries) { + $this->resources = Cache::flexible("resources_{$profileSite->getResourceSite()->localize()}", [60, 300], function () use ($profileSite) { return ExternalResource::query() ->where(ExternalResource::ATTRIBUTE_SITE, $profileSite->getResourceSite()->value) - ->whereIn(ExternalResource::ATTRIBUTE_EXTERNAL_ID, Arr::pluck($entries, 'external_id')) - ->with(ExternalResource::RELATION_ANIME) + ->with([ExternalResource::RELATION_ANIME => fn ($query) => $query->select([Anime::TABLE.'.'.Anime::ATTRIBUTE_ID])]) + ->whereHas(ExternalResource::RELATION_ANIME) ->get() - ->mapWithKeys(fn (ExternalResource $resource) => [$resource->external_id => $resource->anime]); + ->mapWithKeys(fn (ExternalResource $resource) => [$resource->external_id => $resource->anime->map(fn (Anime $anime) => $anime->getKey())]); }); } @@ -135,10 +129,10 @@ class SyncExternalProfileAction * Get the animes by the external id. * * @param int $externalId - * @return Collection + * @return Collection */ protected function getAnimesByExternalId(int $externalId): Collection { - return $this->resources[$externalId] ?? new Collection(); + return $this->resources->get($externalId) ?? collect(); } } diff --git a/app/Actions/Models/Admin/Report/ManageStepAction.php b/app/Actions/Models/User/Report/ManageStepAction.php similarity index 95% rename from app/Actions/Models/Admin/Report/ManageStepAction.php rename to app/Actions/Models/User/Report/ManageStepAction.php index e7c6bae0f..99f7f9cbf 100644 --- a/app/Actions/Models/Admin/Report/ManageStepAction.php +++ b/app/Actions/Models/User/Report/ManageStepAction.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace App\Actions\Models\Admin\Report; +namespace App\Actions\Models\User\Report; -use App\Enums\Models\Admin\ApprovableStatus; -use App\Enums\Models\Admin\ReportActionType; -use App\Models\Admin\Report\ReportStep; +use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\ReportActionType; +use App\Models\User\Report\ReportStep; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Facades\Date; diff --git a/app/Actions/Models/Admin/Report/ReportAction.php b/app/Actions/Models/User/Report/ReportAction.php similarity index 95% rename from app/Actions/Models/Admin/Report/ReportAction.php rename to app/Actions/Models/User/Report/ReportAction.php index 36ad66f22..7d15a338e 100644 --- a/app/Actions/Models/Admin/Report/ReportAction.php +++ b/app/Actions/Models/User/Report/ReportAction.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace App\Actions\Models\Admin\Report; +namespace App\Actions\Models\User\Report; -use App\Enums\Models\Admin\ApprovableStatus; -use App\Enums\Models\Admin\ReportActionType; -use App\Models\Admin\Report; -use App\Models\Admin\Report\ReportStep; +use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\ReportActionType; +use App\Models\User\Report; +use App\Models\User\Report\ReportStep; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Arr; diff --git a/app/Concerns/Models/Reportable.php b/app/Concerns/Models/Reportable.php index b0c97b84d..7ae66bc96 100644 --- a/app/Concerns/Models/Reportable.php +++ b/app/Concerns/Models/Reportable.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace App\Concerns\Models; -use App\Actions\Models\Admin\Report\ReportAction; -use App\Models\Admin\Report; -use App\Models\Admin\Report\ReportStep; +use App\Actions\Models\User\Report\ReportAction; +use App\Models\User\Report; +use App\Models\User\Report\ReportStep; use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Support\Facades\Auth; diff --git a/app/Contracts/Events/NotifiesUsersEvent.php b/app/Contracts/Events/NotifiesUsersEvent.php new file mode 100644 index 000000000..b4f373a0c --- /dev/null +++ b/app/Contracts/Events/NotifiesUsersEvent.php @@ -0,0 +1,21 @@ +profile; + + $notification = new UserNotification( + 'External Profile Synced', + "Your external profile [{$profile->getName()}]({$profile->getClientUrl()}) has been synced.", + NotificationType::SYNCED_PROFILE, + ); + + $profile->user?->notifyNow($notification); + } +} diff --git a/app/Filament/RelationManagers/Admin/ReportStepRelationManager.php b/app/Filament/RelationManagers/User/ReportStepRelationManager.php similarity index 94% rename from app/Filament/RelationManagers/Admin/ReportStepRelationManager.php rename to app/Filament/RelationManagers/User/ReportStepRelationManager.php index 5bba1939d..2b98d461b 100644 --- a/app/Filament/RelationManagers/Admin/ReportStepRelationManager.php +++ b/app/Filament/RelationManagers/User/ReportStepRelationManager.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace App\Filament\RelationManagers\Admin; +namespace App\Filament\RelationManagers\User; use App\Filament\RelationManagers\BaseRelationManager; -use App\Filament\Resources\Admin\Report\ReportStep as ReportStepResource; -use App\Models\Admin\Report\ReportStep; +use App\Filament\Resources\User\Report\ReportStep as ReportStepResource; +use App\Models\User\Report\ReportStep; use Filament\Forms\Form; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; diff --git a/app/Filament/Resources/Admin/Report.php b/app/Filament/Resources/User/Report.php similarity index 95% rename from app/Filament/Resources/Admin/Report.php rename to app/Filament/Resources/User/Report.php index 622250512..ac9fead7e 100644 --- a/app/Filament/Resources/Admin/Report.php +++ b/app/Filament/Resources/User/Report.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace App\Filament\Resources\Admin; +namespace App\Filament\Resources\User; -use App\Enums\Models\Admin\ApprovableStatus; +use App\Enums\Models\User\ApprovableStatus; use App\Filament\Components\Columns\BelongsToColumn; use App\Filament\Components\Columns\TextColumn; use App\Filament\Components\Infolist\BelongsToEntry; use App\Filament\Components\Infolist\TextEntry; use App\Filament\Components\Infolist\TimestampSection; use App\Filament\Resources\BaseResource; -use App\Filament\Resources\Admin\Report\Pages\ListReports; -use App\Filament\Resources\Admin\Report\Pages\ViewReport; -use App\Filament\Resources\Admin\Report\RelationManagers\StepReportRelationManager; +use App\Filament\Resources\User\Report\Pages\ListReports; +use App\Filament\Resources\User\Report\Pages\ViewReport; +use App\Filament\Resources\User\Report\RelationManagers\StepReportRelationManager; use App\Filament\Resources\Auth\User as UserResource; -use App\Models\Admin\Report as ReportModel; +use App\Models\User\Report as ReportModel; use Filament\Forms\Form; use Filament\Infolists\Components\Section; use Filament\Infolists\Infolist; @@ -68,7 +68,7 @@ class Report extends BaseResource */ public static function getNavigationGroup(): string { - return __('filament.resources.group.admin'); + return __('filament.resources.group.user'); } /** diff --git a/app/Filament/Resources/Admin/Report/Pages/ListReports.php b/app/Filament/Resources/User/Report/Pages/ListReports.php similarity index 84% rename from app/Filament/Resources/Admin/Report/Pages/ListReports.php rename to app/Filament/Resources/User/Report/Pages/ListReports.php index 4ba7994fd..6796d96ee 100644 --- a/app/Filament/Resources/Admin/Report/Pages/ListReports.php +++ b/app/Filament/Resources/User/Report/Pages/ListReports.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Filament\Resources\Admin\Report\Pages; +namespace App\Filament\Resources\User\Report\Pages; -use App\Filament\Resources\Admin\Report; +use App\Filament\Resources\User\Report; use App\Filament\Resources\Base\BaseListResources; /** diff --git a/app/Filament/Resources/Admin/Report/Pages/ViewReport.php b/app/Filament/Resources/User/Report/Pages/ViewReport.php similarity index 84% rename from app/Filament/Resources/Admin/Report/Pages/ViewReport.php rename to app/Filament/Resources/User/Report/Pages/ViewReport.php index 7e1c31420..2f76e822f 100644 --- a/app/Filament/Resources/Admin/Report/Pages/ViewReport.php +++ b/app/Filament/Resources/User/Report/Pages/ViewReport.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace App\Filament\Resources\Admin\Report\Pages; +namespace App\Filament\Resources\User\Report\Pages; use App\Filament\Resources\Base\BaseViewResource; -use App\Filament\Resources\Admin\Report; +use App\Filament\Resources\User\Report; /** * Class ViewReport. diff --git a/app/Filament/Resources/Admin/Report/RelationManagers/StepReportRelationManager.php b/app/Filament/Resources/User/Report/RelationManagers/StepReportRelationManager.php similarity index 89% rename from app/Filament/Resources/Admin/Report/RelationManagers/StepReportRelationManager.php rename to app/Filament/Resources/User/Report/RelationManagers/StepReportRelationManager.php index 5c660d977..06ea7b34e 100644 --- a/app/Filament/Resources/Admin/Report/RelationManagers/StepReportRelationManager.php +++ b/app/Filament/Resources/User/Report/RelationManagers/StepReportRelationManager.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace App\Filament\Resources\Admin\Report\RelationManagers; +namespace App\Filament\Resources\User\Report\RelationManagers; -use App\Filament\RelationManagers\Admin\ReportStepRelationManager; -use App\Models\Admin\Report; -use App\Models\Admin\Report\ReportStep; +use App\Filament\RelationManagers\User\ReportStepRelationManager; +use App\Models\User\Report; +use App\Models\User\Report\ReportStep; use Filament\Tables\Table; /** diff --git a/app/Filament/Resources/Admin/Report/ReportStep.php b/app/Filament/Resources/User/Report/ReportStep.php similarity index 95% rename from app/Filament/Resources/Admin/Report/ReportStep.php rename to app/Filament/Resources/User/Report/ReportStep.php index 8426d942b..e8101bbb2 100644 --- a/app/Filament/Resources/Admin/Report/ReportStep.php +++ b/app/Filament/Resources/User/Report/ReportStep.php @@ -2,22 +2,22 @@ declare(strict_types=1); -namespace App\Filament\Resources\Admin\Report; +namespace App\Filament\Resources\User\Report; use App\Contracts\Models\Nameable; -use App\Enums\Models\Admin\ApprovableStatus; -use App\Enums\Models\Admin\ReportActionType; +use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\ReportActionType; use App\Filament\Components\Columns\BelongsToColumn; use App\Filament\Components\Columns\TextColumn; use App\Filament\Components\Infolist\BelongsToEntry; use App\Filament\Components\Infolist\KeyValueThreeEntry; use App\Filament\Components\Infolist\TextEntry; use App\Filament\Components\Infolist\TimestampSection; -use App\Filament\Resources\Admin\Report as ReportResource; +use App\Filament\Resources\User\Report as ReportResource; use App\Filament\Resources\BaseResource; -use App\Filament\Resources\Admin\Report\ReportStep\Pages\ListReportSteps; -use App\Filament\Resources\Admin\Report\ReportStep\Pages\ViewReportStep; -use App\Models\Admin\Report\ReportStep as ReportStepModel; +use App\Filament\Resources\User\Report\ReportStep\Pages\ListReportSteps; +use App\Filament\Resources\User\Report\ReportStep\Pages\ViewReportStep; +use App\Models\User\Report\ReportStep as ReportStepModel; use Filament\Facades\Filament; use Filament\Forms\Form; use Filament\Infolists\Components\KeyValueEntry; @@ -71,7 +71,7 @@ class ReportStep extends BaseResource */ public static function getNavigationGroup(): string { - return __('filament.resources.group.admin'); + return __('filament.resources.group.user'); } /** diff --git a/app/Filament/Resources/Admin/Report/ReportStep/Pages/ListReportSteps.php b/app/Filament/Resources/User/Report/ReportStep/Pages/ListReportSteps.php similarity index 81% rename from app/Filament/Resources/Admin/Report/ReportStep/Pages/ListReportSteps.php rename to app/Filament/Resources/User/Report/ReportStep/Pages/ListReportSteps.php index 5518abdf8..9a2fbb6ab 100644 --- a/app/Filament/Resources/Admin/Report/ReportStep/Pages/ListReportSteps.php +++ b/app/Filament/Resources/User/Report/ReportStep/Pages/ListReportSteps.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Filament\Resources\Admin\Report\ReportStep\Pages; +namespace App\Filament\Resources\User\Report\ReportStep\Pages; -use App\Filament\Resources\Admin\Report\ReportStep; +use App\Filament\Resources\User\Report\ReportStep; use App\Filament\Resources\Base\BaseListResources; /** diff --git a/app/Filament/Resources/Admin/Report/ReportStep/Pages/ViewReportStep.php b/app/Filament/Resources/User/Report/ReportStep/Pages/ViewReportStep.php similarity index 81% rename from app/Filament/Resources/Admin/Report/ReportStep/Pages/ViewReportStep.php rename to app/Filament/Resources/User/Report/ReportStep/Pages/ViewReportStep.php index 1402d68e5..5e34003e7 100644 --- a/app/Filament/Resources/Admin/Report/ReportStep/Pages/ViewReportStep.php +++ b/app/Filament/Resources/User/Report/ReportStep/Pages/ViewReportStep.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Filament\Resources\Admin\Report\ReportStep\Pages; +namespace App\Filament\Resources\User\Report\ReportStep\Pages; -use App\Filament\Resources\Admin\Report\ReportStep; +use App\Filament\Resources\User\Report\ReportStep; use App\Filament\Resources\Base\BaseViewResource; /** diff --git a/app/Http/Api/Field/Base/UuidField.php b/app/Http/Api/Field/Base/UuidField.php new file mode 100644 index 000000000..960f43234 --- /dev/null +++ b/app/Http/Api/Field/Base/UuidField.php @@ -0,0 +1,90 @@ +getKey(), $this->getColumn()); + } + + /** + * Determine if the field should be displayed to the user. + * + * @param Query $query + * @return bool + */ + public function shouldRender(Query $query): bool + { + $criteria = $query->getFieldCriteria($this->schema->type()); + + return $criteria === null || $criteria->isAllowedField($this->getKey()); + } + + /** + * Get the value to display to the user. + * + * @param Model $model + * @return mixed + */ + public function render(Model $model): mixed + { + return $model->getAttribute($this->getColumn()); + } + + /** + * Determine if the field should be included in the select clause of our query. + * + * @param Query $query + * @param Schema $schema + * @return bool + */ + public function shouldSelect(Query $query, Schema $schema): bool + { + // We can only exclude ID fields for top-level models that are not including related resources. + $includeCriteria = $query->getIncludeCriteria($this->schema->type()); + if ( + $this->schema->type() === $schema->type() + && ($includeCriteria === null || $includeCriteria->getPaths()->isEmpty()) + ) { + $criteria = $query->getFieldCriteria($this->schema->type()); + + return $criteria === null || $criteria->isAllowedField($this->getKey()); + } + + return true; + } +} diff --git a/app/Http/Api/Field/JsonField.php b/app/Http/Api/Field/JsonField.php new file mode 100644 index 000000000..11d852771 --- /dev/null +++ b/app/Http/Api/Field/JsonField.php @@ -0,0 +1,55 @@ +getFieldCriteria($this->schema->type()); + + return $criteria === null || $criteria->isAllowedField($this->getKey()); + } + + /** + * Get the value to display to the user. + * + * @param Model $model + * @return mixed + */ + public function render(Model $model): mixed + { + return $model->getAttribute($this->getColumn()); + } + + /** + * Determine if the field should be included in the select clause of our query. + * + * @param Query $query + * @param Schema $schema + * @return bool + */ + public function shouldSelect(Query $query, Schema $schema): bool + { + $criteria = $query->getFieldCriteria($this->schema->type()); + + return $criteria === null || $criteria->isAllowedField($this->getKey()); + } +} diff --git a/app/Http/Api/Field/User/Notification/NotificationDataField.php b/app/Http/Api/Field/User/Notification/NotificationDataField.php new file mode 100644 index 000000000..678dae064 --- /dev/null +++ b/app/Http/Api/Field/User/Notification/NotificationDataField.php @@ -0,0 +1,25 @@ +getFieldCriteria($this->schema->type()); + + return $criteria === null || $criteria->isAllowedField($this->getKey()); + } +} diff --git a/app/Http/Api/Field/User/Notification/NotificationTypeField.php b/app/Http/Api/Field/User/Notification/NotificationTypeField.php new file mode 100644 index 000000000..bd302f6a3 --- /dev/null +++ b/app/Http/Api/Field/User/Notification/NotificationTypeField.php @@ -0,0 +1,40 @@ +withIntermediatePaths([ new AllowedInclude(new ExternalProfileSchema(), User::RELATION_EXTERNAL_PROFILES), + new AllowedInclude(new NotificationSchema(), User::RELATION_NOTIFICATIONS), new AllowedInclude(new PermissionSchema(), User::RELATION_PERMISSIONS), new AllowedInclude(new PlaylistSchema(), User::RELATION_PLAYLISTS), new AllowedInclude(new RoleSchema(), User::RELATION_ROLES), diff --git a/app/Http/Api/Schema/User/NotificationSchema.php b/app/Http/Api/Schema/User/NotificationSchema.php new file mode 100644 index 000000000..aa7a0988c --- /dev/null +++ b/app/Http/Api/Schema/User/NotificationSchema.php @@ -0,0 +1,65 @@ +withIntermediatePaths([]); + } + + /** + * Get the direct fields of the resource. + * + * @return Field[] + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public function fields(): array + { + return [ + new CreatedAtField($this), + new UpdatedAtField($this), + new UuidField($this, 'id'), + new NotificationTypeField($this), + new NotificationNotifiableTypeField($this), + new NotificationNotifiableIdField($this), + new NotificationDataField($this), + new NotificationReadAtField($this), + ]; + } +} diff --git a/app/Http/Controllers/Api/Auth/User/Me/List/MyNotificationController.php b/app/Http/Controllers/Api/Auth/User/Me/List/MyNotificationController.php new file mode 100644 index 000000000..23d48e904 --- /dev/null +++ b/app/Http/Controllers/Api/Auth/User/Me/List/MyNotificationController.php @@ -0,0 +1,119 @@ +middleware(Authenticate::using('sanctum')); + parent::__construct(Notification::class, 'notification'); + } + + /** + * Display a listing of the resource. + * + * @param IndexRequest $request + * @param IndexAction $action + * @return NotificationCollection + */ + public function index(IndexRequest $request, IndexAction $action): NotificationCollection + { + $query = new Query($request->validated()); + + /** @var User $user */ + $user = Auth::user(); + + $builder = $user->notifications()->getQuery(); + + $notifications = $action->index($builder, $query, $request->schema()); + + return new NotificationCollection($notifications, $query); + } + + /** + * Mark an unread notification as read. + * + * @param Notification $notification + * @return JsonResponse + */ + public function read(Notification $notification): JsonResponse + { + $this->authorize('read', $notification); + + $notification->markAsRead(); + + return new JsonResponse([ + 'message' => __('notifications.read'), + ]); + } + + /** + * Mark a read notification as unread. + * + * @param Notification $notification + * @return JsonResponse + */ + public function unread(Notification $notification): JsonResponse + { + $this->authorize('unread', $notification); + + $notification->markAsUnread(); + + return new JsonResponse([ + 'message' => __('notifications.unread'), + ]); + } + + /** + * Mark unread notifications as read. + * + * @return JsonResponse + */ + public function readall(): JsonResponse + { + $this->authorize('readall', Notification::class); + + /** @var User $user */ + $user = Auth::user(); + + $user->unreadNotifications()->update([Notification::ATTRIBUTE_READ_AT => now()]); + + return new JsonResponse([ + 'message' => __('notifications.read_all'), + ]); + } + + /** + * Get the underlying schema. + * + * @return Schema + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public function schema(): Schema + { + return new NotificationSchema(); + } +} diff --git a/app/Http/Middleware/Models/Admin/UserExceedsReportLimit.php b/app/Http/Middleware/Models/Admin/UserExceedsReportLimit.php index 78550a799..0f47d6ca4 100644 --- a/app/Http/Middleware/Models/Admin/UserExceedsReportLimit.php +++ b/app/Http/Middleware/Models/Admin/UserExceedsReportLimit.php @@ -6,8 +6,8 @@ namespace App\Http\Middleware\Models\Admin; use App\Constants\Config\ReportConstants; use App\Enums\Auth\SpecialPermission; -use App\Enums\Models\Admin\ApprovableStatus; -use App\Models\Admin\Report; +use App\Enums\Models\User\ApprovableStatus; +use App\Models\User\Report; use App\Models\Auth\User; use Closure; use Illuminate\Http\Request; diff --git a/app/Http/Resources/User/Collection/NotificationCollection.php b/app/Http/Resources/User/Collection/NotificationCollection.php new file mode 100644 index 000000000..223e47406 --- /dev/null +++ b/app/Http/Resources/User/Collection/NotificationCollection.php @@ -0,0 +1,36 @@ +collection->map(fn (Notification $notification) => new NotificationResource($notification, $this->query))->all(); + } +} diff --git a/app/Http/Resources/User/Resource/NotificationResource.php b/app/Http/Resources/User/Resource/NotificationResource.php new file mode 100644 index 000000000..04f0fe6ef --- /dev/null +++ b/app/Http/Resources/User/Resource/NotificationResource.php @@ -0,0 +1,32 @@ +notify(); + } +} diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index 7392f1cce..c45d57b13 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -12,16 +12,21 @@ use App\Events\Auth\User\UserDeleted; use App\Events\Auth\User\UserRestored; use App\Events\Auth\User\UserUpdated; use App\Models\Admin\ActionLog; -use App\Models\Admin\Report; +use App\Models\User\Report; use App\Models\List\Playlist; use App\Models\List\ExternalProfile; +use App\Models\User\Notification; +use App\Notifications\UserNotification; use Database\Factories\Auth\UserFactory; +use Filament\Facades\Filament; use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasAvatar; +use Filament\Notifications\DatabaseNotification as FilamentNotification; use Filament\Panel; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -80,6 +85,7 @@ class User extends Authenticatable implements MustVerifyEmail, Nameable, HasSubt final public const RELATION_EXTERNAL_PROFILES = 'externalprofiles'; final public const RELATION_MANAGED_REPORTS = 'managedreports'; + final public const RELATION_NOTIFICATIONS = 'notifications'; final public const RELATION_PERMISSIONS = 'permissions'; final public const RELATION_PLAYLISTS = 'playlists'; final public const RELATION_REPORTS = 'reports'; @@ -272,4 +278,20 @@ class User extends Authenticatable implements MustVerifyEmail, Nameable, HasSubt { return $this->hasMany(ActionLog::class, ActionLog::ATTRIBUTE_USER); } + + /** + * Get the entity's notifications. + * + * @return MorphMany + */ + public function notifications(): MorphMany + { + $notifications = $this->morphMany(Notification::class, 'notifiable')->latest(); + + if (Filament::isServing()) { + return $notifications->where(Notification::ATTRIBUTE_TYPE, FilamentNotification::class); + } + + return $notifications->where(Notification::ATTRIBUTE_TYPE, UserNotification::class); + } } diff --git a/app/Models/User/Notification.php b/app/Models/User/Notification.php new file mode 100644 index 000000000..d429879df --- /dev/null +++ b/app/Models/User/Notification.php @@ -0,0 +1,28 @@ +|null $data */ public function __construct( public string $title, public string $body, public NotificationType $type, public ?string $image = null, - public ?array $data = [], ) {} /** @@ -59,7 +57,6 @@ class UserNotification extends Notification implements Arrayable, ShouldQueue 'body' => $this->body, 'type' => $this->type->value, 'image' => $this->image, - 'data' => $this->data, ]; } } diff --git a/app/Policies/User/NotificationPolicy.php b/app/Policies/User/NotificationPolicy.php new file mode 100644 index 000000000..3c6d5136d --- /dev/null +++ b/app/Policies/User/NotificationPolicy.php @@ -0,0 +1,111 @@ +can(CrudPermission::VIEW->format(Notification::class)); + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param Notification $notification + * @return bool + */ + public function view(?User $user, BaseModel|Model $notification): bool + { + return $notification->notifiable()->is($user) && $user->can(CrudPermission::VIEW->format(Notification::class)); + } + + /** + * Determine whether the user can mark the notification as read. + * + * @param User $user + * @param Notification $notification + * @return bool + */ + public function read(User $user, BaseModel|Model $notification): bool + { + return $notification->notifiable()->is($user) && $user->can(CrudPermission::UPDATE->format(Notification::class)); + } + + /** + * Determine whether the user can mark the notification as unread. + * + * @param User $user + * @param Notification $notification + * @return bool + */ + public function unread(User $user, BaseModel|Model $notification): bool + { + return $notification->notifiable()->is($user) && $user->can(CrudPermission::UPDATE->format(Notification::class)); + } + + /** + * Determine whether the user can mark the all their notifications as read. + * + * @param User $user + * @return bool + */ + public function readall(User $user): bool + { + return $user->can(CrudPermission::UPDATE->format(Notification::class)); + } + + /** + * Determine whether the user can create models. + * + * @param User $user + * @return bool + */ + public function create(User $user): bool + { + return false; + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param Notification $notification + * @return bool + */ + public function update(User $user, BaseModel|Model $notification): bool + { + return false; + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param Notification $notification + * @return bool + */ + public function delete(User $user, BaseModel|Model $notification): bool + { + return false; + } +} diff --git a/app/Policies/Admin/ReportPolicy.php b/app/Policies/User/ReportPolicy.php similarity index 96% rename from app/Policies/Admin/ReportPolicy.php rename to app/Policies/User/ReportPolicy.php index 636e71e66..01dc2d345 100644 --- a/app/Policies/Admin/ReportPolicy.php +++ b/app/Policies/User/ReportPolicy.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace App\Policies\Admin; +namespace App\Policies\User; use App\Enums\Auth\CrudPermission; use App\Enums\Auth\Role; -use App\Models\Admin\Report; +use App\Models\User\Report; use App\Models\Auth\User; use App\Models\BaseModel; use App\Policies\BasePolicy; diff --git a/database/factories/User/NotificationFactory.php b/database/factories/User/NotificationFactory.php new file mode 100644 index 000000000..f791f8635 --- /dev/null +++ b/database/factories/User/NotificationFactory.php @@ -0,0 +1,48 @@ + + */ +class NotificationFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = Notification::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $data = [ + 'title' => fake()->text(), + 'body' => fake()->text(), + 'image' => fake()->imageUrl(), + ]; + + return [ + Notification::ATTRIBUTE_ID => Str::uuid()->__toString(), + Notification::ATTRIBUTE_TYPE => UserNotification::class, + Notification::ATTRIBUTE_DATA => $data, + ]; + } +} diff --git a/database/factories/Admin/Report/ReportStepFactory.php b/database/factories/User/Report/ReportStepFactory.php similarity index 83% rename from database/factories/Admin/Report/ReportStepFactory.php rename to database/factories/User/Report/ReportStepFactory.php index 5e58e5eb3..fca10ef0a 100644 --- a/database/factories/Admin/Report/ReportStepFactory.php +++ b/database/factories/User/Report/ReportStepFactory.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Database\Factories\Admin\Report; +namespace Database\Factories\User\Report; -use App\Enums\Models\Admin\ApprovableStatus; -use App\Enums\Models\Admin\ReportActionType; -use App\Models\Admin\Report\ReportStep; +use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\ReportActionType; +use App\Models\User\Report\ReportStep; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Arr; diff --git a/database/factories/Admin/ReportFactory.php b/database/factories/User/ReportFactory.php similarity index 88% rename from database/factories/Admin/ReportFactory.php rename to database/factories/User/ReportFactory.php index 8b6fd5111..686058b1c 100644 --- a/database/factories/Admin/ReportFactory.php +++ b/database/factories/User/ReportFactory.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Database\Factories\Admin; +namespace Database\Factories\User; -use App\Enums\Models\Admin\ApprovableStatus; -use App\Models\Admin\Report; +use App\Enums\Models\User\ApprovableStatus; +use App\Models\User\Report; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Arr; diff --git a/database/migrations/2024_12_13_225202_create_reports_table.php b/database/migrations/2024_12_13_225202_create_reports_table.php index e30b6792c..3c8ff6e5e 100644 --- a/database/migrations/2024_12_13_225202_create_reports_table.php +++ b/database/migrations/2024_12_13_225202_create_reports_table.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use App\Enums\Models\Admin\ApprovableStatus; -use App\Models\Admin\Report; -use App\Models\Admin\Report\ReportStep; +use App\Enums\Models\User\ApprovableStatus; +use App\Models\User\Report; +use App\Models\User\Report\ReportStep; use App\Models\Auth\User; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; diff --git a/database/seeders/Auth/Permission/PermissionSeeder.php b/database/seeders/Auth/Permission/PermissionSeeder.php index 17f12b555..f0a495152 100644 --- a/database/seeders/Auth/Permission/PermissionSeeder.php +++ b/database/seeders/Auth/Permission/PermissionSeeder.php @@ -11,7 +11,7 @@ use App\Models\Admin\Announcement; use App\Models\Admin\Dump; use App\Models\Admin\Feature; use App\Models\Admin\FeaturedTheme; -use App\Models\Admin\Report; +use App\Models\User\Report; use App\Models\Auth\Permission; use App\Models\Auth\Role; use App\Models\Auth\User; @@ -21,6 +21,7 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Notification; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -61,7 +62,6 @@ class PermissionSeeder extends Seeder $this->registerResource(Dump::class, $extendedCrudPermissions); $this->registerResource(Feature::class, CrudPermission::cases()); $this->registerResource(FeaturedTheme::class, $extendedCrudPermissions); - $this->registerResource(Report::class, CrudPermission::cases()); // Auth Resources $this->registerResource(Permission::class, [CrudPermission::VIEW]); @@ -77,6 +77,10 @@ class PermissionSeeder extends Seeder $this->registerResource(Playlist::class, $extendedCrudPermissions); $this->registerResource(PlaylistTrack::class, $extendedCrudPermissions); + // User Resources + $this->registerResource(Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->registerResource(Report::class, CrudPermission::cases()); + // Wiki Resources $this->registerResource(Anime::class, $extendedCrudPermissions); $this->registerResource(AnimeSynonym::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/AdminSeeder.php b/database/seeders/Auth/Role/AdminSeeder.php index 6295685fa..415cb5e82 100644 --- a/database/seeders/Auth/Role/AdminSeeder.php +++ b/database/seeders/Auth/Role/AdminSeeder.php @@ -12,7 +12,7 @@ use App\Models\Admin\Announcement; use App\Models\Admin\Dump; use App\Models\Admin\Feature; use App\Models\Admin\FeaturedTheme; -use App\Models\Admin\Report; +use App\Models\User\Report; use App\Models\Auth\Permission; use App\Models\Auth\Role; use App\Models\Auth\User; @@ -22,6 +22,7 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Notification; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -68,7 +69,6 @@ class AdminSeeder extends RoleSeeder $this->configureResource($role, Dump::class, $extendedCrudPermissions); $this->configureResource($role, Feature::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); $this->configureResource($role, FeaturedTheme::class, $extendedCrudPermissions); - $this->configureResource($role, Report::class, CrudPermission::cases()); // Auth Resources $this->configureResource($role, Permission::class, [CrudPermission::VIEW]); @@ -84,6 +84,10 @@ class AdminSeeder extends RoleSeeder $this->configureResource($role, Playlist::class, $extendedCrudPermissions); $this->configureResource($role, PlaylistTrack::class, $extendedCrudPermissions); + // User Resources + $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Report::class, CrudPermission::cases()); + // Wiki Resources $this->configureResource($role, Anime::class, $extendedCrudPermissions); $this->configureResource($role, AnimeSynonym::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/DeveloperRoleSeeder.php b/database/seeders/Auth/Role/DeveloperRoleSeeder.php index 204164ad2..b7ffc9563 100644 --- a/database/seeders/Auth/Role/DeveloperRoleSeeder.php +++ b/database/seeders/Auth/Role/DeveloperRoleSeeder.php @@ -15,6 +15,7 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Notification; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -65,6 +66,9 @@ class DeveloperRoleSeeder extends RoleSeeder $this->configureResource($role, Playlist::class, $extendedCrudPermissions); $this->configureResource($role, PlaylistTrack::class, $extendedCrudPermissions); + // User Resources + $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + // Wiki Resources $this->configureResource($role, Anime::class, [CrudPermission::VIEW]); $this->configureResource($role, AnimeSynonym::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/EncoderRoleSeeder.php b/database/seeders/Auth/Role/EncoderRoleSeeder.php index 3df6845fc..dae8a9ce2 100644 --- a/database/seeders/Auth/Role/EncoderRoleSeeder.php +++ b/database/seeders/Auth/Role/EncoderRoleSeeder.php @@ -15,6 +15,7 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Notification; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -72,6 +73,9 @@ class EncoderRoleSeeder extends RoleSeeder ], ); + // User Resources + $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + // Wiki Resources $this->configureResource($role, Anime::class, $extendedCrudPermissions); $this->configureResource($role, AnimeSynonym::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/PatronRoleSeeder.php b/database/seeders/Auth/Role/PatronRoleSeeder.php index 25d5ac2ab..24896fb9e 100644 --- a/database/seeders/Auth/Role/PatronRoleSeeder.php +++ b/database/seeders/Auth/Role/PatronRoleSeeder.php @@ -15,6 +15,7 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Notification; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -65,6 +66,9 @@ class PatronRoleSeeder extends RoleSeeder $this->configureResource($role, Playlist::class, $extendedCrudPermissions); $this->configureResource($role, PlaylistTrack::class, $extendedCrudPermissions); + // User Resources + $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + // Wiki Resources $this->configureResource($role, Anime::class, [CrudPermission::VIEW]); $this->configureResource($role, AnimeSynonym::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/WikiEditorRoleSeeder.php b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php index 64c55f13a..32e009310 100644 --- a/database/seeders/Auth/Role/WikiEditorRoleSeeder.php +++ b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php @@ -15,6 +15,7 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Notification; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -65,6 +66,9 @@ class WikiEditorRoleSeeder extends RoleSeeder $this->configureResource($role, Playlist::class, $extendedCrudPermissions); $this->configureResource($role, PlaylistTrack::class, $extendedCrudPermissions); + // User Resources + $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $extendedCrudPermissions = array_merge( CrudPermission::cases(), [ diff --git a/database/seeders/Auth/Role/WikiViewerRoleSeeder.php b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php index 33c08c7f8..77452463f 100644 --- a/database/seeders/Auth/Role/WikiViewerRoleSeeder.php +++ b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php @@ -8,7 +8,7 @@ use App\Enums\Auth\CrudPermission; use App\Enums\Auth\ExtendedCrudPermission; use App\Enums\Auth\Role as RoleEnum; use App\Enums\Auth\SpecialPermission; -use App\Models\Admin\Report; +use App\Models\User\Report; use App\Models\Auth\Role; use App\Models\Discord\DiscordThread; use App\Models\Document\Page; @@ -16,6 +16,7 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Notification; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -57,9 +58,6 @@ class WikiViewerRoleSeeder extends RoleSeeder ExtendedCrudPermission::cases(), ); - // Admin Resources - $this->configureResource($role, Report::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::UPDATE]); - // Discord Resources $this->configureResource($role, DiscordThread::class, [CrudPermission::VIEW]); @@ -69,6 +67,10 @@ class WikiViewerRoleSeeder extends RoleSeeder $this->configureResource($role, Playlist::class, $extendedCrudPermissions); $this->configureResource($role, PlaylistTrack::class, $extendedCrudPermissions); + // User Resources + $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Report::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::UPDATE]); + // Wiki Resources $this->configureResource($role, Anime::class, [CrudPermission::VIEW]); $this->configureResource($role, AnimeSynonym::class, [CrudPermission::VIEW]); diff --git a/lang/en/enums.php b/lang/en/enums.php index e732c80dc..797309a0d 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -3,8 +3,8 @@ declare(strict_types=1); use App\Enums\Models\Admin\ActionLogStatus; -use App\Enums\Models\Admin\ApprovableStatus; -use App\Enums\Models\Admin\ReportActionType; +use App\Enums\Models\User\ApprovableStatus; +use App\Enums\Models\User\ReportActionType; use App\Enums\Models\List\ExternalEntryWatchStatus; use App\Enums\Models\List\ExternalProfileSite; use App\Enums\Models\List\ExternalProfileVisibility; diff --git a/lang/en/filament.php b/lang/en/filament.php index 0a8799ef9..981b0e997 100644 --- a/lang/en/filament.php +++ b/lang/en/filament.php @@ -824,6 +824,7 @@ return [ 'finished_at' => 'Finished At', 'moderator' => 'Moderator', 'mod_notes' => 'Moderator Notes', + 'notes' => 'Notes', 'status' => 'Status', 'target' => 'Target', ], @@ -961,6 +962,7 @@ return [ 'discord' => 'Discord', 'document' => 'Document', 'list' => 'List', + 'user' => 'User', 'wiki' => 'Wiki', ], 'label' => [ diff --git a/lang/en/notifications.php b/lang/en/notifications.php new file mode 100644 index 000000000..589229de1 --- /dev/null +++ b/lang/en/notifications.php @@ -0,0 +1,20 @@ + 'All unread notifications were marked as read.', + 'read' => 'Notification was marked as read.', + 'unread' => 'Notification was marked as unread.', +]; diff --git a/routes/api.php b/routes/api.php index 7528773f6..2b778c6d0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -8,6 +8,7 @@ use App\Http\Controllers\Api\Admin\DumpController; use App\Http\Controllers\Api\Admin\FeatureController; use App\Http\Controllers\Api\Admin\FeaturedThemeController; use App\Http\Controllers\Api\Auth\User\Me\List\MyExternalProfileController; +use App\Http\Controllers\Api\Auth\User\Me\List\MyNotificationController; use App\Http\Controllers\Api\Auth\User\Me\List\MyPlaylistController; use App\Http\Controllers\Api\Auth\User\Me\MyController; use App\Http\Controllers\Api\Document\PageController; @@ -236,6 +237,10 @@ Route::apiResource('feature', FeatureController::class) // Auth Routes Route::get('/me', [MyController::class, 'show'])->name('me.show'); Route::get('/me/externalprofile', [MyExternalProfileController::class, 'index'])->name('me.externalprofile.index'); +Route::get('/me/notification', [MyNotificationController::class, 'index'])->name('me.notification.index'); +Route::match(['put', 'patch'], '/me/notification/readall', [MyNotificationController::class, 'readall'])->name('me.notification.readall'); +Route::match(['put', 'patch'], '/me/notification/{notification}/read', [MyNotificationController::class, 'read'])->name('me.notification.read'); +Route::match(['put', 'patch'], '/me/notification/{notification}/unread', [MyNotificationController::class, 'unread'])->name('me.notification.unread'); Route::get('/me/playlist', [MyPlaylistController::class, 'index'])->name('me.playlist.index'); // Document Routes diff --git a/tests/Feature/Http/Api/Auth/User/Me/List/ExternalProfile/MyExternalProfileIndexTest.php b/tests/Feature/Http/Api/Auth/User/Me/List/ExternalProfile/MyExternalProfileIndexTest.php new file mode 100644 index 000000000..d9fce8510 --- /dev/null +++ b/tests/Feature/Http/Api/Auth/User/Me/List/ExternalProfile/MyExternalProfileIndexTest.php @@ -0,0 +1,93 @@ +get(route('api.me.externalprofile.index')); + + $response->assertUnauthorized(); + } + + /** + * The My External Profile Index Endpoint shall forbid users without the view external profile permission. + * + * @return void + */ + public function testForbiddenIfMissingPermission(): void + { + $user = User::factory()->createOne(); + + Sanctum::actingAs($user); + + $response = $this->get(route('api.me.externalprofile.index')); + + $response->assertForbidden(); + } + + /** + * The My External Profile Index Endpoint shall return profiles owned by the user. + * + * @return void + */ + public function testOnlySeesOwnedProfiles(): void + { + ExternalProfile::factory() + ->for(User::factory()) + ->count($this->faker->randomDigitNotNull()) + ->create(); + + ExternalProfile::factory() + ->count($this->faker->randomDigitNotNull()) + ->create(); + + $user = User::factory()->withPermissions(CrudPermission::VIEW->format(ExternalProfile::class))->createOne(); + + $profileCount = $this->faker->randomDigitNotNull(); + + $profiles = ExternalProfile::factory() + ->for($user) + ->count($profileCount) + ->create(); + + Sanctum::actingAs($user); + + $response = $this->get(route('api.me.externalprofile.index')); + + $response->assertJsonCount($profileCount, ExternalProfileCollection::$wrap); + + $response->assertJson( + json_decode( + json_encode( + new ExternalProfileCollection($profiles, new Query()) + ->response() + ->getData() + ), + true + ) + ); + } +} diff --git a/tests/Feature/Http/Api/Auth/User/Me/Notification/MyNotificationIndexTest.php b/tests/Feature/Http/Api/Auth/User/Me/Notification/MyNotificationIndexTest.php new file mode 100644 index 000000000..a3e967a34 --- /dev/null +++ b/tests/Feature/Http/Api/Auth/User/Me/Notification/MyNotificationIndexTest.php @@ -0,0 +1,90 @@ +get(route('api.me.notification.index')); + + $response->assertUnauthorized(); + } + + /** + * The My Notification Index Endpoint shall forbid users without the view notification permission. + * + * @return void + */ + public function testForbiddenIfMissingPermission(): void + { + $user = User::factory()->createOne(); + + Sanctum::actingAs($user); + + $response = $this->get(route('api.me.notification.index')); + + $response->assertForbidden(); + } + + /** + * The My Notification Index Endpoint shall return notifications owned by the user. + * + * @return void + */ + public function testOnlySeesOwnedNotifications(): void + { + Notification::factory() + ->for(User::factory()->createOne(), Notification::RELATION_NOTIFIABLE) + ->count($this->faker->randomDigitNotNull()) + ->create(); + + $user = User::factory()->withPermissions(CrudPermission::VIEW->format(Notification::class))->createOne(); + + $notificationCount = $this->faker->randomDigitNotNull(); + + $notifications = Notification::factory() + ->for($user, Notification::RELATION_NOTIFIABLE) + ->count($notificationCount) + ->create() + ->sortBy(Notification::ATTRIBUTE_ID); + + Sanctum::actingAs($user); + + $response = $this->get(route('api.me.notification.index')); + + $response->assertJsonCount($notificationCount, NotificationCollection::$wrap); + + $response->assertJson( + json_decode( + json_encode( + new NotificationCollection($notifications, new Query()) + ->response() + ->getData() + ), + true + ) + ); + } +} diff --git a/tests/Unit/Events/NotifiesUsersTest.php b/tests/Unit/Events/NotifiesUsersTest.php new file mode 100644 index 000000000..9a2707211 --- /dev/null +++ b/tests/Unit/Events/NotifiesUsersTest.php @@ -0,0 +1,26 @@ +