I'm trying to test a model Observer for my User class and I'm experiencing the same problems as many other programmers.
The updating/updated/... events won't fire when using PHPUnit. Trying it with tinker works.
So here's the problem: I tried this solution. Unfortunately without success.
This is my TestClass:
class UserObserverTest extends TestCase {
use DatabaseTransactions;
public function setUp()
{
parent::setUp();
$this->user = factory("App\User")->create();
$this->plan = new UserSettings();
$this->difficulty1 = factory("App\Difficulty")->create();
$this->difficulty2 = factory("App\Difficulty")->create();
$this->plan->getExercisePlan()->appendAppExercise(factory("App\Exercise")->create(), false, $this->difficulty1, Constants::VORWAERTS);
$this->plan->getExercisePlan()->appendAppExercise(factory("App\Exercise")->create(), false, $this->difficulty2, Constants::VORWAERTS);
$this->user->settings = $this->plan->toJson();
$this->user->save();
$this->user = $this->user->fresh();
}
public function test_Difficulty2HasUsedCounter0AfterDeletingFromPlan()
{
$this->assertEquals(1, $this->difficulty2->fresh()->used);
$plan = new UserSettings($this->user->settings);
$exercises = $plan->getExercisePlan()->getExercisesArray();
array_pull($exercises, 1);
$this->user->settings = $plan->toJson();
$this->user->save();
$this->assertEquals(0, $this->difficulty2->fresh()->used);
}
}
UserObserver:
class UserObserver
{
/**
* Listen to the User updating event
* #param User $user
*/
public function updating(User $user)
{
$settings = new UserSettings($user->settings);
foreach($settings->getExercisePlan()->getExercisesArray() as $exercise)
{
$difficulty = Difficulty::find($exercise->getDifficultyId());
$difficulty->used--;
$difficulty->save();
}
}
/**
* Listen to the user updated event
* #param User $user
*/
public function updated(User $user)
{
$settings = new UserSettings($user->settings);
foreach($settings->getExercisePlan()->getExercisesArray() as $exercise)
{
$difficulty = Difficulty::find($exercise->getDifficultyId());
$difficulty->used++;
$difficulty->save();
}
}
/**
* Listen to the User deleting event.
*
* #param User $user
* #return void
*/
public function deleting(User $user)
{
$settings = new UserSettings($user->settings);
foreach($settings->getExercisePlan()->getExercisesArray() as $exercise)
{
$difficulty = Difficulty::find($exercise->getDifficultyId());
$difficulty->used--;
$difficulty->save();
}
}
}
I registered the Observer using User::observe(UserObserver::class); inside boot() of AppServiceProvider
Related
I am implementing two-factor authentication (2FA) in my Laravel 8 application.
The 2FA is applied every time the user logs in. However, I don't really feel that 2FA is necessary every time, I even find it annoying. As a solution I am thinking of applying it only when the user connects from a new device. Is there someone who has already done it or who can give me a hint of the changes that would be necessary?
I have got it. Here are the steps I have followed:
In the config file fortify.php I have added
'pipelines' => [
'login' => [
App\Actions\Fortify\RedirectIfTwoFactorAuthenticatable::class,
Laravel\Fortify\Actions\AttemptToAuthenticate::class,
Laravel\Fortify\Actions\PrepareAuthenticatedSession::class,
]
]
I have added the field two_factor_cookies to the User class.
I have customized the RedirectIfTwoFactorAuthenticatable class of
Fortify:
<?php
namespace App\Actions\Fortify;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable as DefaultRedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
class RedirectIfTwoFactorAuthenticatable extends DefaultRedirectIfTwoFactorAuthenticatable
{
/**
* Handle the incoming request.
*
* #param \Illuminate\Http\Request $request
* #param callable $next
* #return mixed
*/
public function handle($request, $next)
{
$user = $this->validateCredentials($request);
if (optional($user)->two_factor_secret &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user)) &&
$this->checkIfUserDeviceHasNotCookie($user)) {
return $this->twoFactorChallengeResponse($request, $user);
}
return $next($request);
}
/**
* This checks if the user's device has the cookie stored
* in the database.
*
* #param \App\Models\User\User $user
* #return bool
*/
protected function checkIfUserDeviceHasNotCookie($user)
{
$two_factor_cookies = json_decode($user->two_factor_cookies);
if (!is_array($two_factor_cookies)){
$two_factor_cookies = [];
}
$two_factor_cookie = \Cookie::get('2fa');
return !in_array($two_factor_cookie,$two_factor_cookies);
}
}
In the FortifyServiceProvider I have added a customized TwoFactorLoginResponse.
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Http\Responses\FailedPasswordResetLinkRequestResponse;
use App\Http\Responses\FailedPasswordResetResponse;
use App\Http\Responses\LockoutResponse;
use App\Http\Responses\LoginResponse;
use App\Http\Responses\LogoutResponse;
use App\Http\Responses\PasswordResetResponse;
use App\Http\Responses\RegisterResponse;
use App\Http\Responses\SuccessfulPasswordResetLinkRequestResponse;
use App\Http\Responses\TwoFactorLoginResponse;
use App\Http\Responses\VerifyEmail;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse as FailedPasswordResetLinkRequestResponseContract;
use Laravel\Fortify\Contracts\FailedPasswordResetResponse as FailedPasswordResetResponseContract;
use Laravel\Fortify\Contracts\LockoutResponse as LockoutResponseContract;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Contracts\LogoutResponse as LogoutResponseContract;
use Laravel\Fortify\Contracts\PasswordResetResponse as PasswordResetResponseContract;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse as SuccessfulPasswordResetLinkRequestResponseContract;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->registerResponseBindings();
}
/**
* Register the response bindings.
*
* #return void
*/
protected function registerResponseBindings()
{
$this->app->singleton(LoginResponseContract::class, LoginResponse::class);
$this->app->singleton(LogoutResponseContract::class, LogoutResponse::class);
$this->app->singleton(TwoFactorLoginResponseContract::class, TwoFactorLoginResponse::class);
$this->app->singleton(RegisterResponseContract::class, RegisterResponse::class);
$this->app->singleton(LockoutResponseContract::class, LockoutResponse::class);
$this->app->singleton(SuccessfulPasswordResetLinkRequestResponseContract::class, SuccessfulPasswordResetLinkRequestResponse::class);
$this->app->singleton(FailedPasswordResetLinkRequestResponseContract::class, FailedPasswordResetLinkRequestResponse::class);
$this->app->singleton(PasswordResetResponseContract::class, PasswordResetResponse::class);
$this->app->singleton(FailedPasswordResetResponseContract::class, FailedPasswordResetResponse::class);
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Fortify::ignoreRoutes();
Fortify::loginView(function () {
return view('auth.login');
});
Fortify::twoFactorChallengeView('auth.two-factor-challenge');
Fortify::confirmPasswordView(function (Request $request) {
if ($request->ajax()) {
return view('auth.confirm-password-form');
} else {
return view('auth.confirm-password');
}
});
Fortify::requestPasswordResetLinkView(function () {
return view('auth.forgot-password');
});
Fortify::resetPasswordView(function ($request) {
return view('auth.reset-password', ['request' => $request,'token' => $request->route('token')]);
});
Fortify::registerView(function () {
return view('auth.register');
});
Fortify::verifyEmailView(function () {
return view('auth.verify');
});
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
/*RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->email.$request->ip());
});*/
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}
Finally, the TwoFactorLoginResponse:
<?php
namespace App\Http\Responses;
use Illuminate\Http\JsonResponse;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
class TwoFactorLoginResponse implements TwoFactorLoginResponseContract
{
/**
* Create an HTTP response that represents the object.
*
* #param \Illuminate\Http\Request $request
* #return \Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request)
{
$user = \Auth::user();
$this->storeCookieIfNotInDB($user);
$role = $user->role;
if ($request->wantsJson()) {
return new JsonResponse('', 204);
}
if ($role == "0") {
return redirect()->route('user.home');
} else {
return redirect()->route('admin.home');
}
}
/**
* Store the cookie if it is not in the database.
*
* #param \App\Models\User\User $user
* #return void
*/
protected function storeCookieIfNotInDB($user)
{
$two_factor_cookies = json_decode($user->two_factor_cookies);
if (!is_array($two_factor_cookies)){
$two_factor_cookies = [];
}
$two_factor_cookie = \Cookie::get('2fa');
if (!in_array($two_factor_cookie,$two_factor_cookies)) {
$two_factor_cookie = md5(now());
$two_factor_cookies[] = $two_factor_cookie;
if (count($two_factor_cookies) > 3) {
array_shift($two_factor_cookies);
}
$user->two_factor_cookies = json_encode($two_factor_cookies);
$user->save();
$lifetime = 60 * 24 * 365; //one year
\Cookie::queue('2fa',$two_factor_cookie,$lifetime);
}
}
}
Upon login, it will look for the cookie 2fa. If its content is stored in the database, it will not be necessary to enter the code again. To prevent unlimited cookie content from being saved in the DB you can add a maximum limit (I have set it 3).
Thanks to Maarten Veerman for the inital help.
According to this line: https://github.com/laravel/fortify/blob/82c99b6999f7e89f402cfd7eb4074e619382b3b7/src/Http/Controllers/AuthenticatedSessionController.php#L80
you can create a pipelines.login entry in your fortify config file.
The solution would be to:
create the config entry
copy the pipeline setup in the above file, line 84.
create a custom AttemptToAuthenticate class, make sure the pipeline config points to your new class.
make the new class extend the default fortify AttemptToAuthenticate class.
overwrite the handle function, add your logic in the new function, where you check for a cookie on the device.
I have a Model named "Resource".
by using this command
php artisan make:observer ResourceObserver --model=Resource
this command create a new file, i update created, updated functions and update constructor
<?php
namespace App\Observers;
use App\Resource;
class ResourceObserver
{
protected $userID;
public function __construct()
{
$this->userID = auth()->user()->id;
}
/**
* Handle the resource "created" event.
*
* #param \App\Resource $resource
* #return void
*/
public function created(Resource $resource)
{
$resource->created_by = $this->userID;
}
/**
* Handle the resource "updated" event.
*
* #param \App\Resource $resource
* #return void
*/
public function updated(Resource $resource)
{
$resource->updated_by = $this->userID;
}
/**
* Handle the resource "deleted" event.
*
* #param \App\Resource $resource
* #return void
*/
public function deleted(Resource $resource)
{
//
}
/**
* Handle the resource "restored" event.
*
* #param \App\Resource $resource
* #return void
*/
public function restored(Resource $resource)
{
//
}
/**
* Handle the resource "force deleted" event.
*
* #param \App\Resource $resource
* #return void
*/
public function forceDeleted(Resource $resource)
{
//
}
}
this is my migration:
public function up()
{
Schema::create('resources', function (Blueprint $table) {
$table->id();
// some fields here
$table->foreignId('created_by')->nullable()->default(null)->constrained('users')->onDelete('set null');
$table->foreignId('updated_by')->nullable()->default(null)->constrained('users')->onDelete('set null');
$table->timestamps();
});
}
then you should register the observer in AppServiceProvider like this:
use App\Observers\ResourceObserver;
use App\Resource;
public function boot()
{
Schema::defaultStringLength(191);
Resource::observe(ResourceObserver::class);
}
Now the problem appears when update any record it is not save the user_id
to update i use update function in ResourceController
public function update(Request $request, Resource $resource)
{
$validations = [
// some validations
];
$request->validate($validations);
try {
if (!empty($resource)) {
$resource->field_a = $request->field_a;
$resource->field_b = $request->field_b;
$resource->field_c = $request->field_c;
$resource->save();
return 'done messge';
} else {
return 'error message';
}
} catch (\Exception $e) {
return 'bug message';
}
}
Any help please?!
When issuing a mass update or delete via Eloquent, the saved, updated, deleting, and deleted model events will not be fired for the affected models. This is because the models are never actually retrieved when issuing a mass update or delete.
So, In ResourceObserver i just changed from method from updated to updating,
and created to creating
I'm trying to build a package for my application and there some config files that user can publish and modify configs (like every laravel package).
I recently noticed that the config file are loads only when the user publishes config files.
Here is my package repo on GitHub: Laravel Adobe Connect Client
Here is my service provider:
class AdobeConnectServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register services.
*
* #return void
*/
public function register()
{
$this->mergeConfigFrom(__DIR__ . "/config/adobeConnect.php", 'adobeConnect');
$this->bindFacades();
}
/**
* Bind Facades
*
* #return void
*/
private function bindFacades()
{
$config = $this->getAdobeConfig();
$entities = $config["entities"];
$this->app->singleton(Client::class, function () {
return $this->processClient();
});
$this->app->bind('sco', function () use ($entities) {
return new $entities["sco"]();
});
$this->app->bind('sco-record', function () use ($entities) {
return new $entities["sco-record"]();
});
$this->app->bind('principal', function () use ($entities) {
return new $entities["principal"]();
});
$this->app->bind('permission', function () use ($entities) {
return new $entities["permission"]();
});
$this->app->bind('common-info', function () use ($entities) {
return new $entities["common-info"]();
});
$this->app->singleton('adobe-connect', function () {
return App::make(Client::class);
});
}
/**
* get Adobe Connect Client Config
*
* #return array|null
*/
private function getAdobeConfig()
{
return $this->app["config"]->get("adobeConnect");
}
/**
* login adobe client in case of there is no session configured
*
* #return Client
*/
private function processClient()
{
$config = $this->getAdobeConfig();
$connection = new Connection($config["host"], $config["connection"]);
$client = new Client($connection);
if ($config["session-cache"]["enabled"]) {
$this->loginClient($client);
}
return $client;
}
/**
* Login Client Based information that introduced in environment/config file
*
* #param Client $client
*
* #return void
*/
private function loginClient(Client $client)
{
$config = $this->getAdobeConfig();
$driver = $this->getCacheDriver();
$session = Cache::store($driver)->remember(
$config['session-cache']['key'],
$config['session-cache']['timeout'],
function () use ($config, $client) {
$client->login($config["user-name"], $config["password"]);
return $client->getSession();
});
$client->setSession($session);
}
/**
* get Preferred cache driver
*
* #return string|null
*/
private function getCacheDriver()
{
$config = $this->app["config"]->get("adobeConnect");
if ($driver = $config["session-cache"]["driver"]) {
return $driver;
}
return $this->app["config"]->get("cache.default");
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
$this->publishes([
__DIR__ . '/config/adobeConnect.php' => config_path('adobeConnect.php'),
], 'adobe-connect');
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return [
SCORecord::class,
Principal::class,
Permission::class,
CommonInfo::class,
Client::class,
SCO::class,
'adobe-connect'
];
}
}
I've read laravel's official package and I've found nothing!
I've found the problem. the problem resists under defer loading provider:
It should return facade binding names instead of the class name.
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return [
Client::class,
'sco-record',
'principal',
'permission',
'common-info',
'sco',
'adobe-connect'
];
}
Using the nilportuguess' eloquent repository library, I made the following (with bugs) repository:
namespace App\Repositories;
use NilPortugues\Foundation\Infrastructure\Model\Repository\Eloquent\EloquentRepository;
use App\Model\Rover;
class RoverRepository extends EloquentRepository
{
/**
* {#inheritdoc}
*/
protected function modelClassName()
{
return Rover::class;
}
/**
* {#inheritdoc}
*/
public function find(Identity $id, Fields $fields = null)
{
$eloquentModel = parent::find($id, $fields);
return $eloquentModel->toArray();
}
/**
* {#inheritdoc}
*/
public function findBy(Filter $filter = null, Sort $sort = null, Fields $fields = null)
{
$eloquentModelArray = parent::findBy($filter, $sort, $fields);
return $this->fromEloquentArray($eloquentModelArray);
}
/**
* {#inheritdoc}
*/
public function findAll(Pageable $pageable = null)
{
$page = parent::findAll($pageable);
return new Page(
$this->fromEloquentArray($page->content()),
$page->totalElements(),
$page->pageNumber(),
$page->totalPages(),
$page->sortings(),
$page->filters(),
$page->fields()
);
}
/**
* #param array $eloquentModelArray
* #return array
*/
protected function fromEloquentArray(array $eloquentModelArray) :array
{
$results = [];
foreach ($eloquentModelArray as $eloquentModel) {
//This is required to handle findAll returning array, not objects.
$eloquentModel = (object) $eloquentModel;
$results[] = $eloquentModel->attributesToArray();
}
return $results;
}
}
And In order to locate them I thought to make an Integration test on an sqlite inmemory db:
namespace Test\Database\Integration\Repositories;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Repositories\RoverRepository;
use App\Model\Rover;
use App\Model\Grid;
class RoverRepositoryTest extends TestCase
{
use RefreshDatabase;
private $repository=null;
public function setUp(): void
{
parent::setUp();
$grid=factory(Grid::class)->create([
'width'=>5,
'height'=>5
]);
$rover=factory(Rover::class, 5)->create([
'grid_id' => $grid->id,
'grid_pos_x' => rand(0, $grid->width),
'grid_pos_y' => rand(0, $grid->height),
]);
//How do I run Migrations and generate the db?
$this->repository = new RoverRepository();
}
public function tearDown(): void
{
parent::tearDown();
//How I truncate and destroy Database?
}
/**
* Testing Base Search
*
* #return void
*/
public function testBasicSearch(): void
{
//Some Db test
}
}
But I have some questions:
How do I save the generated via factory Models?
How do I nuke my database in tearDown()?
I need to access to the current user in a service provider of a module. The Auth::user() returns null. I know the middleware is called after the service provider and that is why this is null. Is there any solution to this problem? it is my code
namespace Modules\User\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Nwidart\Modules\Facades\Module;
class ViewComposerProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* #var bool
*/
protected $defer = false;
/**
* Register the service provider.
*
* #return void
*/
public function boot()
{
$this->buildMenu();
$this->buildAvatar();
}
public function register()
{
dd(Auth::user());//null
}
private function buildAvatar(){
$f = Auth::user();
dd($f); // null
}
public function buildMenu()
{
view()->composer('layouts.subnavbar', function ($view) {
$t = \Nwidart\Modules\Facades\Module::getByStatus(1);
$modules = [];
foreach ($t as $item)
$modules[] = $item->name;
$view->with('modules', $modules);
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return [];
}
}
Instead of calling the user in the provider you could make 2 view composers, 1 for the menu and 1 for the avatar
AvatarComposer.php
class AvatarComposer
{
public function compose(View $view)
{
$avatar = Auth::user()->avatar//AVATAR HERE
$view->with('avatar', $avatar);
}
}
ModuleComposer.php
class ModuleComposer
{
public function compose(View $view)
{
$t = \Nwidart\Modules\Facades\Module::getByStatus(1);
$modules = [];
foreach ($t as $item)
$modules[] = $item->name;
$view->with('modules', $modules);
}
}
and then in the boot of your provider:
//make it public for all routes
View::composer('*', AvatarComposer::class);
View::composer('layouts.subnavbar', ModuleComposer::class);