feat: added notifications (#795)

This commit is contained in:
Kyrch
2025-04-11 18:38:36 -03:00
committed by GitHub
parent 9e46ba9a2c
commit bc9df20162
60 changed files with 1259 additions and 114 deletions
@@ -88,6 +88,8 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction
* Make the request to the external api.
*
* @return static
*
* @throws RequestException
*/
protected function makeRequest(): static
{
@@ -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),
]);
}
}
@@ -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<int, Anime>
* @return Collection<int, int>
*/
protected function getAnimesByExternalId(int $externalId): Collection
{
return $this->resources[$externalId] ?? new Collection();
return $this->resources->get($externalId) ?? collect();
}
}
@@ -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;
@@ -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;
+3 -3
View File
@@ -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;
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Events;
use Filament\Notifications\Notification;
use Illuminate\Support\Collection;
/**
* Interface NotifiesUsersEvent.
*/
interface NotifiesUsersEvent
{
/**
* Notify the users.
*
* @return void
*/
public function notify(): void;
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enums\Models\Admin;
namespace App\Enums\Models\User;
use App\Concerns\Enums\LocalizesName;
use Filament\Support\Contracts\HasColor;
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enums;
namespace App\Enums\Models\User;
/**
* Enum NotificationType.
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enums\Models\Admin;
namespace App\Enums\Models\User;
use App\Concerns\Enums\LocalizesName;
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile;
use App\Contracts\Events\NotifiesUsersEvent;
use App\Enums\Models\User\NotificationType;
use App\Models\List\ExternalProfile;
use App\Notifications\UserNotification;
use Illuminate\Foundation\Events\Dispatchable;
/**
* Class ExternalProfileSynced.
*/
class ExternalProfileSynced implements NotifiesUsersEvent
{
use Dispatchable;
/**
* Create new event instance.
*
* @param ExternalProfile $profile
*/
public function __construct(protected ExternalProfile $profile)
{
}
/**
* Notify the users.
*
* @return void
*/
public function notify(): void
{
$profile = $this->profile;
$notification = new UserNotification(
'External Profile Synced',
"Your external profile [{$profile->getName()}]({$profile->getClientUrl()}) has been synced.",
NotificationType::SYNCED_PROFILE,
);
$profile->user?->notifyNow($notification);
}
}
@@ -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;
@@ -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');
}
/**
@@ -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;
/**
@@ -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.
@@ -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;
/**
@@ -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');
}
/**
@@ -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;
/**
@@ -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;
/**
+90
View File
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Base;
use App\Contracts\Http\Api\Field\FilterableField;
use App\Contracts\Http\Api\Field\RenderableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Filter\Filter;
use App\Http\Api\Filter\StringFilter;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\BaseResource;
use Illuminate\Database\Eloquent\Model;
/**
* Class UuidField.
*/
class UuidField extends Field implements FilterableField, RenderableField, SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
* @param string $column
*/
public function __construct(Schema $schema, string $column)
{
parent::__construct($schema, BaseResource::ATTRIBUTE_ID, $column);
}
/**
* Get the filter that can be applied to the field.
*
* @return Filter
*/
public function getFilter(): Filter
{
return new StringFilter($this->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;
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field;
use App\Contracts\Http\Api\Field\RenderableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use Illuminate\Database\Eloquent\Model;
/**
* Class JsonField.
*/
abstract class JsonField extends Field implements RenderableField, SelectableField
{
/**
* 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
{
$criteria = $query->getFieldCriteria($this->schema->type());
return $criteria === null || $criteria->isAllowedField($this->getKey());
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\User\Notification;
use App\Http\Api\Field\JsonField;
use App\Http\Api\Schema\Schema;
use App\Models\User\Notification;
/**
* Class NotificationDataField.
*/
class NotificationDataField extends JsonField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, Notification::ATTRIBUTE_DATA);
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\User\Notification;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\User\Notification;
/**
* Class NotificationNotifiableIdField.
*/
class NotificationNotifiableIdField extends Field implements SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, Notification::ATTRIBUTE_NOTIFIABLE_ID);
}
/**
* 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
{
// Needed to filter the user notifications on query.
return true;
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\User\Notification;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\User\Notification;
/**
* Class NotificationNotifiableTypeField.
*/
class NotificationNotifiableTypeField extends Field implements SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, Notification::ATTRIBUTE_NOTIFIABLE_TYPE);
}
/**
* 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
{
// Needed to filter the user notifications on query.
return true;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\User\Notification;
use App\Http\Api\Field\DateField;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\User\Notification;
/**
* Class NotificationReadAtField.
*/
class NotificationReadAtField extends DateField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, Notification::ATTRIBUTE_READ_AT);
}
/**
* Determine if the field should be displayed to the user.
*
* @param Query $query
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldRender(Query $query): bool
{
$criteria = $query->getFieldCriteria($this->schema->type());
return $criteria === null || $criteria->isAllowedField($this->getKey());
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\User\Notification;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\User\Notification;
/**
* Class NotificationTypeField.
*/
class NotificationTypeField extends Field implements SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, Notification::ATTRIBUTE_TYPE);
}
/**
* 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
{
// Needed to match user notifications on query.
return true;
}
}
@@ -16,6 +16,7 @@ use App\Http\Api\Schema\Auth\RoleSchema;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\List\ExternalProfileSchema;
use App\Http\Api\Schema\List\PlaylistSchema;
use App\Http\Api\Schema\User\NotificationSchema;
use App\Http\Resources\Auth\User\Resource\MyResource;
use App\Models\Auth\User;
use Illuminate\Database\Eloquent\Model;
@@ -44,6 +45,7 @@ class MySchema extends EloquentSchema
{
return $this->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),
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\User;
use App\Http\Api\Field\User\Notification\NotificationDataField;
use App\Http\Api\Field\User\Notification\NotificationNotifiableIdField;
use App\Http\Api\Field\User\Notification\NotificationNotifiableTypeField;
use App\Http\Api\Field\User\Notification\NotificationReadAtField;
use App\Http\Api\Field\User\Notification\NotificationTypeField;
use App\Http\Api\Field\Base\CreatedAtField;
use App\Http\Api\Field\Base\UpdatedAtField;
use App\Http\Api\Field\Base\UuidField;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Resources\User\Resource\NotificationResource;
/**
* Class NotificationSchema.
*/
class NotificationSchema extends EloquentSchema
{
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return NotificationResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return $this->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),
];
}
}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Auth\User\Me\List;
use App\Actions\Http\Api\IndexAction;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\User\NotificationSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Auth\Authenticate;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Resources\User\Collection\NotificationCollection;
use App\Models\Auth\User;
use App\Models\User\Notification;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
/**
* Class MyNotificationController.
*/
class MyNotificationController extends BaseController
{
/**
* Create a new controller instance.
*/
public function __construct()
{
$this->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();
}
}
@@ -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;
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\User\Collection;
use App\Http\Resources\User\Resource\NotificationResource;
use App\Http\Resources\BaseCollection;
use App\Models\User\Notification;
use Illuminate\Http\Request;
/**
* Class NotificationCollection.
*/
class NotificationCollection extends BaseCollection
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'notifications';
/**
* Transform the resource into a JSON array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray(Request $request): array
{
return $this->collection->map(fn (Notification $notification) => new NotificationResource($notification, $this->query))->all();
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\User\Resource;
use App\Http\Api\Schema\User\NotificationSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\BaseResource;
/**
* Class NotificationResource.
*/
class NotificationResource extends BaseResource
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'notification';
/**
* Get the resource schema.
*
* @return Schema
*/
protected function schema(): Schema
{
return new NotificationSchema();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Listeners;
use App\Contracts\Events\NotifiesUsersEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
/**
* Class NotifiesUsers.
*/
class NotifiesUsers implements ShouldQueue
{
use InteractsWithQueue;
/**
* Handle the event.
*
* @param NotifiesUsersEvent $event
* @return void
*/
public function handle(NotifiesUsersEvent $event): void
{
$event->notify();
}
}
+23 -1
View File
@@ -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<Notification, $this>
*/
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);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Models\User;
use Database\Factories\User\NotificationFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\DatabaseNotification;
/**
* Class Notification.
*
* @method static NotificationFactory factory(...$parameters)
*/
class Notification extends DatabaseNotification
{
use HasFactory;
final public const ATTRIBUTE_ID = 'id';
final public const ATTRIBUTE_TYPE = 'type';
final public const ATTRIBUTE_NOTIFIABLE_TYPE = 'notifiable_type';
final public const ATTRIBUTE_NOTIFIABLE_ID = 'notifiable_id';
final public const ATTRIBUTE_DATA = 'data';
final public const ATTRIBUTE_READ_AT = 'read_at';
final public const RELATION_NOTIFIABLE = 'notifiable';
}
@@ -2,14 +2,14 @@
declare(strict_types=1);
namespace App\Models\Admin;
namespace App\Models\User;
use App\Contracts\Models\HasSubtitle;
use App\Contracts\Models\Nameable;
use App\Enums\Models\Admin\ApprovableStatus;
use App\Models\Admin\Report\ReportStep;
use App\Enums\Models\User\ApprovableStatus;
use App\Models\User\Report\ReportStep;
use App\Models\Auth\User;
use Database\Factories\Admin\ReportFactory;
use Database\Factories\User\ReportFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -2,12 +2,12 @@
declare(strict_types=1);
namespace App\Models\Admin\Report;
namespace App\Models\User\Report;
use App\Enums\Models\Admin\ApprovableStatus;
use App\Enums\Models\Admin\ReportActionType;
use App\Models\Admin\Report;
use Database\Factories\Admin\Report\ReportStepFactory;
use App\Enums\Models\User\ApprovableStatus;
use App\Enums\Models\User\ReportActionType;
use App\Models\User\Report;
use Database\Factories\User\Report\ReportStepFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
+1 -4
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Notifications;
use App\Enums\NotificationType;
use App\Enums\Models\User\NotificationType;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Support\Arrayable;
@@ -24,14 +24,12 @@ class UserNotification extends Notification implements Arrayable, ShouldQueue
* @param string $body
* @param NotificationType $type
* @param string|null $image
* @param array<string, mixed>|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,
];
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace App\Policies\User;
use App\Enums\Auth\CrudPermission;
use App\Models\Auth\User;
use App\Models\BaseModel;
use App\Models\User\Notification;
use App\Policies\BasePolicy;
use Illuminate\Database\Eloquent\Model;
/**
* Class NotificationPolicy.
*/
class NotificationPolicy extends BasePolicy
{
/**
* Determine whether the user can view any models.
*
* @param User|null $user
* @return bool
*/
public function viewAny(?User $user): bool
{
return $user === null || $user->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;
}
}
@@ -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;
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Database\Factories\User;
use App\Models\User\Notification;
use App\Notifications\UserNotification;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* Class NotificationFactory.
*
* @method Notification createOne($attributes = [])
* @method Notification makeOne($attributes = [])
*
* @extends Factory<Notification>
*/
class NotificationFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<Notification>
*/
protected $model = Notification::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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,
];
}
}
@@ -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;
@@ -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;
@@ -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;
@@ -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);
+6 -2
View File
@@ -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);
@@ -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]);
@@ -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);
@@ -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]);
@@ -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(),
[
@@ -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]);
+2 -2
View File
@@ -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;
+2
View File
@@ -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' => [
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Notifications Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during notification responses
| to send as response to the user.
|
*/
'read_all' => 'All unread notifications were marked as read.',
'read' => 'Notification was marked as read.',
'unread' => 'Notification was marked as unread.',
];
+5
View File
@@ -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
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Auth\User\Me\List\ExternalProfile;
use App\Enums\Auth\CrudPermission;
use App\Http\Api\Query\Query;
use App\Http\Resources\List\Collection\ExternalProfileCollection;
use App\Models\Auth\User;
use App\Models\List\ExternalProfile;
use Illuminate\Foundation\Testing\WithFaker;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class MyExternalProfileIndexTest.
*/
class MyExternalProfileIndexTest extends TestCase
{
use WithFaker;
/**
* The My External Profile Index Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$response = $this->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
)
);
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Auth\User\Me\Notification;
use App\Enums\Auth\CrudPermission;
use App\Http\Api\Query\Query;
use App\Http\Resources\User\Collection\NotificationCollection;
use App\Models\Auth\User;
use App\Models\User\Notification;
use Illuminate\Foundation\Testing\WithFaker;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class MyNotificationIndexTest.
*/
class MyNotificationIndexTest extends TestCase
{
use WithFaker;
/**
* The My Notification Index Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$response = $this->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
)
);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Events;
use App\Contracts\Events\NotifiesUsersEvent;
use App\Listeners\NotifiesUsers;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
/**
* Class NotifiesUsersTest.
*/
class NotifiesUsersTest extends TestCase
{
/**
* NotifiesUsers shall listen to NotifiesUsersEvent.
*
* @return void
*/
public function testListening(): void
{
Event::assertListening(NotifiesUsersEvent::class, NotifiesUsers::class);
}
}
@@ -2,12 +2,12 @@
declare(strict_types=1);
namespace Tests\Unit\Models\Admin\Report;
namespace Tests\Unit\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 App\Models\Wiki\Anime;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Testing\WithFaker;
@@ -2,11 +2,11 @@
declare(strict_types=1);
namespace Tests\Unit\Models\Admin;
namespace Tests\Unit\Models\User;
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\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;