vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Vanguard\UserActivity;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Vanguard\User;
use Vanguard\UserActivity\Database\Factories\ActivityFactory;
class Activity extends Model
{
use HasFactory;
const UPDATED_AT = null;
protected $table = 'user_activity';
protected $fillable = ['description', 'user_id', 'ip_address', 'user_agent'];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* Create a new factory instance for the model.
*/
protected static function newFactory(): ActivityFactory
{
return new ActivityFactory;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Vanguard\UserActivity\Http\Controllers\Api;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
use Vanguard\Http\Controllers\Api\ApiController;
use Vanguard\UserActivity\Activity;
use Vanguard\UserActivity\Http\Requests\GetActivitiesRequest;
use Vanguard\UserActivity\Http\Resources\ActivityResource;
class ActivityController extends ApiController
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('permission:users.activity');
}
/**
* Paginate user activities.
*/
public function index(GetActivitiesRequest $request): \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
$activities = QueryBuilder::for(Activity::class)
->allowedIncludes('user')
->allowedFilters([
AllowedFilter::partial('description'),
AllowedFilter::exact('user', 'user_id'),
])
->allowedSorts('created_at')
->defaultSort('-created_at')
->paginate($request->per_page ?: 20);
return ActivityResource::collection($activities);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Vanguard\UserActivity\Http\Controllers\Api;
use Auth;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Vanguard\Http\Controllers\Api\ApiController;
use Vanguard\UserActivity\Repositories\Activity\ActivityRepository;
class StatsController extends ApiController
{
public function __construct(private readonly ActivityRepository $activities)
{
$this->middleware('auth');
}
public function show(): JsonResponse
{
$data = $this->activities->userActivityForPeriod(
Auth::user()->id,
Carbon::now()->subWeeks(2),
Carbon::now()
);
return response()->json($data);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Vanguard\UserActivity\Http\Controllers\Web;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Vanguard\Http\Controllers\Controller;
use Vanguard\UserActivity\Repositories\Activity\ActivityRepository;
class ActivityController extends Controller
{
public function __construct(private readonly ActivityRepository $activities)
{
}
/**
* Displays the page with activities for all system users.
*/
public function index(Request $request): View
{
$activities = $this->activities->paginateActivities(perPage: 20, search: $request->search);
return view('user-activity::index', [
'adminView' => true,
'activities' => $activities,
]);
}
}
@@ -0,0 +1,48 @@
<?php
namespace Vanguard\UserActivity\Http\Controllers\Web;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Vanguard\Http\Controllers\Controller;
use Vanguard\User;
use Vanguard\UserActivity\Repositories\Activity\ActivityRepository;
class UserActivityController extends Controller
{
public function __construct(private readonly ActivityRepository $activities)
{
}
/**
* Displays the activity log page for specific user.
*/
public function index(User $user, Request $request): View
{
$activities = $this->activities->paginateActivitiesForUser(
userId: $user->id,
search: $request->search,
);
return view('user-activity::index', [
'user' => $user,
'adminView' => true,
'activities' => $activities,
]);
}
/**
* Display user activity log.
*/
public function show(Request $request): View
{
$user = auth()->user();
$activities = $this->activities->paginateActivitiesForUser(
userId: $user->id,
search: $request->get('search'),
);
return view('user-activity::index', compact('activities', 'user'));
}
}
@@ -0,0 +1,25 @@
<?php
namespace Vanguard\UserActivity\Http\Requests;
use Vanguard\Http\Requests\Request;
class GetActivitiesRequest extends Request
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'per_page' => 'integer|max:100',
];
}
public function messages(): array
{
return [
'per_page.max' => __('Maximum number of records per page is 100.'),
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace Vanguard\UserActivity\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ActivityResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
*/
public function toArray($request): array
{
$agent = app('agent');
$agent->setUserAgent($this->resource->user_agent);
return [
'id' => (int) $this->id,
'user_id' => (int) $this->user_id,
'ip_address' => $this->ip_address,
'user_agent' => $this->user_agent,
'browser' => $agent->browser(),
'platform' => $agent->platform(),
'device' => $agent->device(),
'description' => $this->description,
'created_at' => (string) $this->created_at,
];
}
}
@@ -0,0 +1,20 @@
<?php
namespace Vanguard\UserActivity\Http\View\Composers;
use Illuminate\View\View;
use Vanguard\UserActivity\Repositories\Activity\ActivityRepository;
class ShowUserComposer
{
public function __construct(private readonly ActivityRepository $activity)
{
}
public function compose(View $view): void
{
$user = $view->getData()['user'];
$view->with('activities', $this->activity->getLatestActivitiesForUser($user->id));
}
}
@@ -0,0 +1,58 @@
<?php
namespace Vanguard\UserActivity\Listeners;
use Illuminate\Events\Dispatcher;
use Vanguard\Events\Permission\Created;
use Vanguard\Events\Permission\Deleted;
use Vanguard\Events\Permission\Updated;
use Vanguard\UserActivity\Logger;
class PermissionEventsSubscriber
{
public function __construct(private readonly Logger $logger)
{
}
public function onCreate(Created $event): void
{
$permission = $event->getPermission();
$name = $permission->display_name ?: $permission->name;
$message = trans('user-activity::log.new_permission', ['name' => $name]);
$this->logger->log($message);
}
public function onUpdate(Updated $event): void
{
$permission = $event->getPermission();
$name = $permission->display_name ?: $permission->name;
$message = trans('user-activity::log.updated_permission', ['name' => $name]);
$this->logger->log($message);
}
public function onDelete(Deleted $event): void
{
$permission = $event->getPermission();
$name = $permission->display_name ?: $permission->name;
$message = trans('user-activity::log.deleted_permission', ['name' => $name]);
$this->logger->log($message);
}
/**
* Register the listeners for the subscriber.
*/
public function subscribe(Dispatcher $events): void
{
$class = self::class;
$events->listen(Created::class, "{$class}@onCreate");
$events->listen(Updated::class, "{$class}@onUpdate");
$events->listen(Deleted::class, "{$class}@onDelete");
}
}
@@ -0,0 +1,65 @@
<?php
namespace Vanguard\UserActivity\Listeners;
use Illuminate\Events\Dispatcher;
use Vanguard\Events\Role\Created;
use Vanguard\Events\Role\Deleted;
use Vanguard\Events\Role\PermissionsUpdated;
use Vanguard\Events\Role\Updated;
use Vanguard\UserActivity\Logger;
class RoleEventsSubscriber
{
public function __construct(private readonly Logger $logger)
{
}
public function onCreate(Created $event): void
{
$message = trans(
'user-activity::log.new_role',
['name' => $event->getRole()->display_name]
);
$this->logger->log($message);
}
public function onUpdate(Updated $event): void
{
$message = trans(
'user-activity::log.updated_role',
['name' => $event->getRole()->display_name]
);
$this->logger->log($message);
}
public function onDelete(Deleted $event): void
{
$message = trans(
'user-activity::log.deleted_role',
['name' => $event->getRole()->display_name]
);
$this->logger->log($message);
}
public function onPermissionsUpdate(PermissionsUpdated $event): void
{
$this->logger->log(trans('user-activity::log.updated_role_permissions'));
}
/**
* Register the listeners for the subscriber.
*/
public function subscribe(Dispatcher $events): void
{
$class = self::class;
$events->listen(Created::class, "{$class}@onCreate");
$events->listen(Updated::class, "{$class}@onUpdate");
$events->listen(Deleted::class, "{$class}@onDelete");
$events->listen(PermissionsUpdated::class, "{$class}@onPermissionsUpdate");
}
}
@@ -0,0 +1,195 @@
<?php
namespace Vanguard\UserActivity\Listeners;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Auth\Events\Registered;
use Illuminate\Events\Dispatcher;
use Lab404\Impersonate\Events\LeaveImpersonation;
use Lab404\Impersonate\Events\TakeImpersonation;
use Vanguard\Events\Settings\Updated as SettingsUpdated;
use Vanguard\Events\User\Banned;
use Vanguard\Events\User\ChangedAvatar;
use Vanguard\Events\User\Created;
use Vanguard\Events\User\Deleted;
use Vanguard\Events\User\LoggedIn;
use Vanguard\Events\User\LoggedOut;
use Vanguard\Events\User\RequestedPasswordResetEmail;
use Vanguard\Events\User\TwoFactorDisabled;
use Vanguard\Events\User\TwoFactorDisabledByAdmin;
use Vanguard\Events\User\TwoFactorEnabled;
use Vanguard\Events\User\TwoFactorEnabledByAdmin;
use Vanguard\Events\User\UpdatedByAdmin;
use Vanguard\Events\User\UpdatedProfileDetails;
use Vanguard\UserActivity\Logger;
class UserEventsSubscriber
{
public function __construct(private readonly Logger $logger)
{
}
public function onLogin(LoggedIn $event): void
{
$this->logger->log(trans('user-activity::log.logged_in'));
}
public function onLogout(LoggedOut $event): void
{
$this->logger->log(trans('user-activity::log.logged_out'));
}
public function onRegister(Registered $event): void
{
$this->logger->setUser($event->user);
$this->logger->log(trans('user-activity::log.created_account'));
}
public function onAvatarChange(ChangedAvatar $event): void
{
$this->logger->log(trans('user-activity::log.updated_avatar'));
}
public function onProfileDetailsUpdate(UpdatedProfileDetails $event): void
{
$this->logger->log(trans('user-activity::log.updated_profile'));
}
public function onDelete(Deleted $event): void
{
$message = trans(
'user-activity::log.deleted_user',
['name' => $event->getDeletedUser()->present()->nameOrEmail]
);
$this->logger->log($message);
}
public function onBan(Banned $event): void
{
$message = trans(
'user-activity::log.banned_user',
['name' => $event->getBannedUser()->present()->nameOrEmail]
);
$this->logger->log($message);
}
public function onUpdateByAdmin(UpdatedByAdmin $event): void
{
$message = trans(
'user-activity::log.updated_profile_details_for',
['name' => $event->getUpdatedUser()->present()->nameOrEmail]
);
$this->logger->log($message);
}
public function onCreate(Created $event): void
{
$message = trans(
'user-activity::log.created_account_for',
['name' => $event->getCreatedUser()->present()->nameOrEmail]
);
$this->logger->log($message);
}
public function onSettingsUpdate(SettingsUpdated $event): void
{
$this->logger->log(trans('user-activity::log.updated_settings'));
}
public function onTwoFactorEnable(TwoFactorEnabled $event): void
{
$this->logger->log(trans('user-activity::log.enabled_2fa'));
}
public function onTwoFactorDisable(TwoFactorDisabled $event): void
{
$this->logger->log(trans('user-activity::log.disabled_2fa'));
}
public function onTwoFactorEnableByAdmin(TwoFactorEnabledByAdmin $event): void
{
$message = trans(
'user-activity::log.enabled_2fa_for',
['name' => $event->getUser()->present()->nameOrEmail]
);
$this->logger->log($message);
}
public function onTwoFactorDisableByAdmin(TwoFactorDisabledByAdmin $event): void
{
$message = trans(
'user-activity::log.disabled_2fa_for',
['name' => $event->getUser()->present()->nameOrEmail]
);
$this->logger->log($message);
}
public function onPasswordResetEmailRequest(RequestedPasswordResetEmail $event): void
{
$this->logger->setUser($event->getUser());
$this->logger->log(trans('user-activity::log.requested_password_reset'));
}
public function onPasswordReset(PasswordReset $event): void
{
$this->logger->setUser($event->user);
$this->logger->log(trans('user-activity::log.reseted_password'));
}
public function onStartImpersonating(TakeImpersonation $event): void
{
$this->logger->setUser($event->impersonator);
$message = trans('user-activity::log.started_impersonating', [
'id' => $event->impersonated->id,
'name' => $event->impersonated->present()->name,
]);
$this->logger->log($message);
}
public function onStopImpersonating(LeaveImpersonation $event): void
{
$this->logger->setUser($event->impersonator);
$message = trans('user-activity::log.stopped_impersonating', [
'id' => $event->impersonated->id,
'name' => $event->impersonated->present()->name,
]);
$this->logger->log($message);
}
/**
* Register the listeners for the subscriber.
*/
public function subscribe(Dispatcher $events)
{
$class = self::class;
$events->listen(LoggedIn::class, "{$class}@onLogin");
$events->listen(LoggedOut::class, "{$class}@onLogout");
$events->listen(Registered::class, "{$class}@onRegister");
$events->listen(Created::class, "{$class}@onCreate");
$events->listen(ChangedAvatar::class, "{$class}@onAvatarChange");
$events->listen(UpdatedProfileDetails::class, "{$class}@onProfileDetailsUpdate");
$events->listen(UpdatedByAdmin::class, "{$class}@onUpdateByAdmin");
$events->listen(Deleted::class, "{$class}@onDelete");
$events->listen(Banned::class, "{$class}@onBan");
$events->listen(SettingsUpdated::class, "{$class}@onSettingsUpdate");
$events->listen(TwoFactorEnabled::class, "{$class}@onTwoFactorEnable");
$events->listen(TwoFactorDisabled::class, "{$class}@onTwoFactorDisable");
$events->listen(TwoFactorEnabledByAdmin::class, "{$class}@onTwoFactorEnableByAdmin");
$events->listen(TwoFactorDisabledByAdmin::class, "{$class}@onTwoFactorDisableByAdmin");
$events->listen(RequestedPasswordResetEmail::class, "{$class}@onPasswordResetEmailRequest");
$events->listen(PasswordReset::class, "{$class}@onPasswordReset");
$events->listen(TakeImpersonation::class, "{$class}@onStartImpersonating");
$events->listen(LeaveImpersonation::class, "{$class}@onStopImpersonating");
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Vanguard\UserActivity;
use Illuminate\Contracts\Auth\Factory;
use Illuminate\Http\Request;
use Vanguard\User;
use Vanguard\UserActivity\Repositories\Activity\ActivityRepository;
class Logger
{
protected ?User $user = null;
public function __construct(
private readonly Request $request,
private readonly Factory $auth,
private readonly ActivityRepository $activities
) {
}
/**
* Log user action.
*/
public function log($description): Activity
{
return $this->activities->log([
'description' => $description,
'user_id' => $this->getUserId(),
'ip_address' => $this->request->ip(),
'user_agent' => $this->getUserAgent(),
]);
}
/**
* Get id if the user for who we want to log this action.
* If user was manually set, then we will just return id of that user.
* If not, we will return the id of currently logged user.
*/
private function getUserId(): ?int
{
if ($this->user) {
return $this->user->id;
}
return $this->auth->guard()->id();
}
/**
* Get user agent from request headers.
*/
private function getUserAgent(): string
{
return substr((string) $this->request->header('User-Agent'), 0, 500);
}
public function setUser(?User $user): void
{
$this->user = $user;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Vanguard\UserActivity\Repositories\Activity;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as BaseCollection;
use Vanguard\UserActivity\Activity;
interface ActivityRepository
{
/**
* Log user activity.
*
* @param $data array Array with following fields:
* description (string) - Description of user activity.
* user_id (int) - User unique identifier.
* ip_address (string) - Ip address from which user is accessing the website.
* user_agent (string) - User's browser info.
* @return mixed
*/
public function log(array $data): Activity;
/**
* Paginate activities for user.
*/
public function paginateActivitiesForUser(
int $userId,
int $perPage = 20,
?string $search = null
): LengthAwarePaginator;
/**
* Get specified number of latest user activity logs.
*
* @return Collection<Activity>
*/
public function getLatestActivitiesForUser(int $userId, int $activitiesCount = 10): Collection;
/**
* Paginate all activity records.
*/
public function paginateActivities(int $perPage = 20, ?string $search = null): LengthAwarePaginator;
/**
* Get count of user activities per day for given period of time.
*/
public function userActivityForPeriod(int $userId, Carbon $from, Carbon $to): BaseCollection;
}
@@ -0,0 +1,98 @@
<?php
namespace Vanguard\UserActivity\Repositories\Activity;
use Carbon\Carbon;
use DB;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection as BaseCollection;
use Vanguard\UserActivity\Activity;
class EloquentActivity implements ActivityRepository
{
/**
* {@inheritdoc}
*/
public function log($data): Activity
{
return Activity::create($data);
}
/**
* {@inheritdoc}
*/
public function paginateActivitiesForUser(
int $userId,
int $perPage = 20,
?string $search = null
): LengthAwarePaginator {
$query = Activity::where('user_id', $userId);
return $this->paginateAndFilterResults($perPage, $search, $query);
}
/**
* {@inheritdoc}
*/
public function getLatestActivitiesForUser(int $userId, int $activitiesCount = 10): Collection
{
return Activity::where('user_id', $userId)
->orderBy('created_at', 'DESC')
->limit($activitiesCount)
->get();
}
/**
* {@inheritdoc}
*/
public function paginateActivities(int $perPage = 20, ?string $search = null): LengthAwarePaginator
{
$query = Activity::with('user');
return $this->paginateAndFilterResults($perPage, $search, $query);
}
private function paginateAndFilterResults($perPage, $search, $query): LengthAwarePaginator
{
if ($search) {
$query->where('description', 'LIKE', "%$search%");
}
$result = $query->orderBy('created_at', 'DESC')
->paginate($perPage);
if ($search) {
$result->appends(['search' => $search]);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function userActivityForPeriod($userId, Carbon $from, Carbon $to): BaseCollection
{
$result = Activity::select([
DB::raw('DATE(created_at) as day'),
DB::raw('count(id) as count'),
])
->where('user_id', $userId)
->whereBetween('created_at', [$from, $to])
->groupBy('day')
->orderBy('day', 'ASC')
->pluck('count', 'day');
while (! $from->isSameDay($to)) {
if (! $result->has($from->toDateString())) {
$result->put($from->toDateString(), 0);
}
$from->addDay();
}
return $result->sortBy(function ($value, $key) {
return strtotime($key);
});
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace Vanguard\UserActivity;
use Event;
use Route;
use Vanguard\Plugins\Plugin;
use Vanguard\Support\Sidebar\Item;
use Vanguard\UserActivity\Http\View\Composers\ShowUserComposer;
use Vanguard\UserActivity\Listeners\PermissionEventsSubscriber;
use Vanguard\UserActivity\Listeners\RoleEventsSubscriber;
use Vanguard\UserActivity\Listeners\UserEventsSubscriber;
use Vanguard\UserActivity\Repositories\Activity\ActivityRepository;
use Vanguard\UserActivity\Repositories\Activity\EloquentActivity;
use View;
class UserActivity extends Plugin
{
/**
* {@inheritDoc}
*/
public function sidebar(): Item
{
return Item::create(__('Activity Log'))
->route('activity.index')
->icon('fas fa-server')
->active('activity*')
->permissions('users.activity');
}
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton(ActivityRepository::class, EloquentActivity::class);
}
/**
* Bootstrap services.
*
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function boot(): void
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'user-activity');
$this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'user-activity');
$this->loadJsonTranslationsFrom(__DIR__.'/../resources/lang');
$this->publishes([
__DIR__.'/../database/migrations' => database_path('migrations'),
], 'migrations');
$this->app->booted(function () {
$this->mapWebRoutes();
if ($this->app['config']->get('auth.expose_api')) {
$this->mapApiRoutes();
}
});
$this->attachViewComposers();
$this->registerEventListeners();
}
/**
* Map web plugin related routes.
*/
protected function mapWebRoutes(): void
{
Route::group([
'namespace' => 'Vanguard\UserActivity\Http\Controllers\Web',
'middleware' => 'web',
], function () {
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
});
}
/**
* Map API plugin related routes.
*/
protected function mapApiRoutes(): void
{
Route::group([
'namespace' => 'Vanguard\UserActivity\Http\Controllers\Api',
'middleware' => 'api',
'prefix' => 'api',
], function () {
$this->loadRoutesFrom(__DIR__.'/../routes/api.php');
});
}
/**
* Register event subscribers for the plugin.
*/
private function registerEventListeners(): void
{
Event::subscribe(PermissionEventsSubscriber::class);
Event::subscribe(RoleEventsSubscriber::class);
Event::subscribe(UserEventsSubscriber::class);
}
/**
* Attach view composers to add necessary data to the view.
*/
private function attachViewComposers(): void
{
View::composer('user.view', ShowUserComposer::class);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Vanguard\UserActivity\Widgets;
use Auth;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Vanguard\Plugins\Widget;
use Vanguard\User;
use Vanguard\UserActivity\Repositories\Activity\ActivityRepository;
class ActivityWidget extends Widget
{
/**
* {@inheritdoc}
*/
public ?string $width = '12';
private ?array $userActivity = null;
public function __construct(private readonly ActivityRepository $activities)
{
$this->permissions(function (User $user) {
return $user->hasRole('User');
});
}
public function render(): View
{
return view('user-activity::widgets.user-activity', [
'activities' => $this->getActivity(),
]);
}
public function scripts(): View
{
return view('user-activity::widgets.user-activity-scripts', [
'activities' => $this->getActivity(),
]);
}
private function getActivity(): array
{
if ($this->userActivity) {
return $this->userActivity;
}
return $this->userActivity = $this->activities->userActivityForPeriod(
Auth::user()->id,
Carbon::now()->subWeeks(2),
Carbon::now()
)->toArray();
}
}