I want to send an SMS to a mobile phone (if he had already turned on the two-factor authentication system).
So at LoginController I added this method:
protected function authenticated(Request $request, $user)
{
return $this->loggendin($request , $user);
}
And this loggendin method is inside of a trait called TwoFactorAuthentication, which goes like this:
trait TwoFactorAuthenticate
{
public function loggendin(Request $request , $user)
{
if($user->hasTwoFactorAuthenticatedEnabled()) {
auth()->logout();
$request->session()->flash('auth' , [
'user_id' => $user->id,
'using_sms' => false,
'remember' => $request->has('remember')
]);
if($user->two_factor_type == 'sms') {
$code = ActiveCode::generateCode($user);
// Todo Send Sms
$request->user()->notify(new ActiveCodeNotification($code , $user->phone_number));
$request->session()->push('auth.using_sms' , true);
}
return redirect(route('twofa.token'));
}
return false;
}
}
Now the problem is when I want to log in, this message appears on the screen which is saying:
Error Call to a member function notify() on null
Which is referring to this line:
$request->user()->notify(new ActiveCodeNotification($code , $user->phone_number));
And this ActiveCodeNotification holds some settings for sending the SMS.
If you would like to visit that, here it is:
class ActiveCodeNotification extends Notification
{
use Queueable;
public $code;
public $phoneNumber;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct($code , $phoneNumber)
{
$this->code = $code;
$this->phoneNumber = $phoneNumber;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return [GhasedakChannel::class];
}
public function toGhasedakSms($notifiable)
{
return [
'text' => "{$this->code}",
'number' => $this->phoneNumber
];
}
}
So what's going wrong here that I get Call to a member function notify() on null while it's two parameters have value.
So if you know, please let me know. I would really appreciate any idea from you guys...
Thanks.
Try this:
First, make sure your User model has the Notifiable trait.
Top of the User Model class:
use Illuminate\Notifications\Notifiable;
After that:
class User extends Model{
use Notifiable; // ....
And then...
Instead of
$request->user()->notify(new ActiveCodeNotification($code , $user->phone_number));
Use this
$user->notify(new ActiveCodeNotification($code, $user->phone_number));
Or
Before calling auth()->logout();
use it at first:
auth()->user()->notify(new ActiveCodeNotification($code, $user->phone_number));
then, you can call auth()->logout();
Worked for me recently
The $request->user() would be null at that point on LoginController because the request was not authenticated and the user is not set on the request instance yet.
You have to use the $user argument instead:
$user->notify(new ActiveCodeNotification($code , $user->phone_number));
Note: make sure that your User model has Illuminate\Notifications\Notifiable trait in order to be able to use notify().
Related
I'm have created a custom form request in my laravel 5.6 something like this:
<?php
namespace Noetic\Plugins\blog\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
];
}
}
When I do not place anything in rules, I get the controllers working, and when I put any rule inside suppose I put
return [
'title' => 'required',
'body' => 'required',
];
It works until it gets validated true, I mean if title and body is passed it gets validated, but when I don't send any data for title or body I'm not getting errors as response, I see the home page belonging to web middleware, I want to return the error data as response.
My controller is something like this:
public function store( StorePostRequest $request )
{
if ($request->fails()) {
return $this->errorResponse($request->errors()->all());
}
$data = $request->only('title', 'body');
$post = Post::create($data);
return response()->json(['post'=> $post ],200);
}
Help me out with these. Thanks
In your controller function you don't need to catch the validation just try with success path.
Handler will handle your validation
public function store( StorePostRequest $request )
{
$data = $request->only('title', 'body');
$post = Post::create($data);
return response()->json(['post'=> $post ],200);
}
In your Handler
use Illuminate\Validation\ValidationException;
if ($exception instanceof ValidationException)
{
return response($exception->errors())->header('Content-Type', 'application/json');
}
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
after that
protected function failedValidation(Validator $validator) {
throw new HttpResponseException(response()->json($validator->errors(), 422));
}
Invoice app development is going on using Laravel. I store date and amount format for every users in settings table.
When user login to their account how to set Session variable? Please give any suggestions. I am using Laravel 5.3.
Of course the docs tell us how to store session data*, but they don't address the OP's question regarding storing session data at login. You have a couple options but I think the clearest way is to override the AuthenticatesUsers trait's authenticated method.
Add the override to your LoginController:
/**
* The user has been authenticated.
*
* #param \Illuminate\Http\Request $request
* #param mixed $user
* #return mixed
*/
protected function authenticated(Request $request, $user)
{
$this->setUserSession($user);
}
Then you can set your session up as:
protected function setUserSession($user)
{
session(
[
'last_invoiced_at' => $user->settings->last_invoiced_at,
'total_amount_due' => $user->settings->total_amount_due
]
);
}
If you want to be a bit more clever you can create a listener for the Login or Authenticated events and set up the session when one of those events* fires.
Create a listener such as SetUpUserSession:
<?php
namespace app\Listeners;
use Illuminate\Auth\Events\Login;
class SetUserSession
{
/**
* #param Login $event
* #return void
*/
public function handle(Login $event)
{
session(
[
'last_invoiced_at' => $event->user->settings->last_invoiced_at,
'total_amount_due' => $event->user->settings->total_amount_due
]
);
}
}
*Links go to 5.4 but this hasn't changed from 5.3.
I've used the Auth class to manage user data, like this:
public function index(){
$user_id = Auth::user()->id;
}
But you have to add 'use Auth;' before class declaration. Then you can add any data to session variable.
Laravel fires an event when a new login is made to the application.
When an event fires you may add a listener for it, then add a session .
This is the content of a listener I made.
<?php
namespace App\Listeners\Auth;
use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class UserLoggedIn
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
//
}
public function handle(Login $event)
{
if ($event->user->hasRole('subsidiary_admin')) {
\Session::put('subsidiary_admin', $event->user->subsidiaryBoUser->subsidiary_id);
\Session::put('subsidiary', $event->user->subsidiaryBoUser->subsidiary);
}
}
}
and I register it on the eventServiceProvider like this
'Illuminate\Auth\Events\Login' => [
'App\Listeners\Auth\UserLoggedIn',
],
You can store data in the session using two different methods either a Request instance or using the global helper/function provided.
Request Instance
public function methodA(Request $request) {
$request->session()->put('KEY', 'VALUE');
}
Global Helper
public function methodB() {
session(['key' => 'value']);
}
You can find more details on both methods in the documentation.
Here's what I am doing:
I have this on my helper file:
\App\Helpers\helpers.php:
function signedUser()
{
return [
'id' => Auth::id(),
'group_id' => Auth::user()->group_id,
'group_name' => Auth::user()->group->name,
'avatar' => Auth::user()->avatar,
'first_name' => Auth::user()->first_name,
'full_name' => Auth::user()->full_name,
];
}
On my User Model:
public function group()
{
return $this->belongsTo('App\Models\Group');
}
public function getFullNameAttribute()
{
$full_name = ucfirst($this->first_name) . ' ' . ucfirst($this->middle_name[0]) . '. ' . ucfirst($this->last_name);
return $full_name;
}
Then I can accessed the variables on both controllers and blade files like so:
dump(signedUser()['full_name']);
{{ signedUser()['full_name'] }}
I am not so into PHP and Laravel and I have the following problem, I came from Java.
I am following this tutorial to implement a custom user provider:
https://blog.georgebuckingham.com/laravel-52-auth-custom-user-providers-drivers/
I am using Laravel 5.3 version.
I briefly expain what I need: my Laravel application is only a front end application, all the business logic, included the user authentication, is performed by a Java back end application that exposes REST web services.
Performing a call to:
http://localhost:8080/Extranet/login
and passing username and password as basic authentication I obtain a JSON response like this that represent the logged user:
{
"userName": "Painkiller",
"email": "painkiller#gmail.com",
"enabled": true
}
So, in my Laravel application, I have to perform this call and then parse the previous returned JSON object to generate the authenticated object into the front end application session.
To do this I have implemented the previous tutorial (and it seems to works) implementing this custom user provider class named UserProvider that implements the Laravel IlluminateUserProvider interface:
<?php
namespace App\Authentication;
use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider as IlluminateUserProvider;
use GuzzleHttp\Client;
use function GuzzleHttp\json_encode;
use function GuzzleHttp\json_decode;
use Illuminate\Support\Facades\Log;
class UserProvider implements IlluminateUserProvider
{
public function retrieveById($identifier)
{
// TODO: Implement retrieveById() method.
\Log::info('retrieveById START');
// PERFORM THE CALL TO MY BACK END WB SERVICE AND CREATE A NEW GenericUser USING THESE INFORMATION:
$attributes = array(
'id' => 123,
'username' => 'nobili.andrea#gmail.com',
'password' => \Hash::make('SuperSecret'),
'name' => 'Dummy User',
);
$user = new GenericUser($attributes);
return $user;
}
public function retrieveByToken($identifier, $token)
{
// TODO: Implement retrieveByToken() method.
\Log::info('retrieveByToken START');
}
public function updateRememberToken(Authenticatable $user, $token)
{
// TODO: Implement updateRememberToken() method.
\Log::info('updateRememberToken START');
}
public function retrieveByCredentials(array $credentials) {
// TODO: Implement retrieveByCredentials() method.
\Log::info('retrieveByCredentials START');
\Log::info('INSERTED USER CREDENTIAL: '.$credentials['email'] . ' ' .$credentials['password']);
$client = new Client(); //GuzzleHttp\Client
$response = $client->get('http://localhost:8080/Extranet/login',
[
'auth' => [
'nobili.andrea#gmail.com',
'pswd'
]
]);
$dettagliLogin = json_decode($response->getBody());
\Log::info('response: '.(json_encode($dettagliLogin)));
//$user = new User('Pippo', 'pippo#google.com', true);
$attributes = array(
'id' => 123,
'username' => 'nobili.andrea#gmail.com',
'password' => \Hash::make('SuperSecret'),
'name' => 'Dummy User',
);
$user = new GenericUser($attributes);
\Log::info('USER: '.(json_encode($user)));
return $user;
}
public function validateCredentials(Authenticatable $user, array $credentials)
{
// TODO: Implement validateCredentials() method.
\Log::info('validateCredentials START');
return true;
}
}
This is only a preliminary test so the returned data are mocked.
It works in this way:
1) When the user insert his credential in the login page (http://localhost:8000/login) it is called the retrieveByCredentials() method:
public function retrieveByCredentials(array $credentials) {
// TODO: Implement retrieveByCredentials() method.
\Log::info('retrieveByCredentials START');
\Log::info('INSERTED USER CREDENTIAL: '.$credentials['email'] . ' ' .$credentials['password']);
$client = new Client(); //GuzzleHttp\Client
$response = $client->get('http://localhost:8080/Extranet/login',
[
'auth' => [
'nobili.andrea#gmail.com',
'pswd'
]
]);
$dettagliLogin = json_decode($response->getBody());
\Log::info('response: '.(json_encode($dettagliLogin)));
//$user = new User('Pippo', 'pippo#google.com', true);
$attributes = array(
'id' => 123,
'username' => 'nobili.andrea#gmail.com',
'password' => \Hash::make('SuperSecret'),
'name' => 'Dummy User',
);
$user = new GenericUser($attributes);
\Log::info('USER: '.(json_encode($user)));
return $user;
}
that performs a web service call to obtain the user information related to this user. Then these credential are verified by the validateCredentials() method (at this time it returns true every time). Finnaly it returns a GenericUser objct containing the information of the logged user (at this time are mocked because is a test and I have not yet paresed the JSON reeturned by my web service.
Then, when the user access to the next page (after the success login) it seems to me that is called the retrieveById($identifier) method, this:
public function retrieveById($identifier)
{
// TODO: Implement retrieveById() method.
\Log::info('retrieveById START');
// PERFORM THE CALL TO MY BACK END WB SERVICE AND CREATE A NEW GenericUser USING THESE INFORMATION:
$attributes = array(
'id' => 123,
'username' => 'nobili.andrea#gmail.com',
'password' => \Hash::make('SuperSecret'),
'name' => 'Dummy User',
);
$user = new GenericUser($attributes);
return $user;
}
At this time the logic is that it use the id of the previous logged user to perform a call to the back end web service, obtain again these information and create the same GenericUser object that will be returned to the next page that uses it. Now I have mock these userinformation.
Ok, this works but I can't do in this way for security reason.
So my idea is: when I retrieve the user information in the retrieveByCredentials(array $credentials), after have check that are correct, I will put this GenericUser object into session.
Then in the retrieveById() method I will retrieve these information from the session.
Can I do something like this? Could be a smart way? How can I put and retrieve an object\data into and from the session (I am not into PHP and front end).
Yes, you can.
When you log in a user using Laravel's authentication API, it is automatically stored in the application's session. So what you are doing is completely fine.
And you don't need to worry about methods like retrieveByCredentials fireing calls to your REST API when you get the user instance through Auth::user(). Every time this method is called, it checks if there is an user already loged in. If there is, it just returns the user, otherwise, it makes the necessary calls to find that user.
But I would suggest putting those calls to your REST API in another class. The UserProvider has only the responsibility of showing Laravel how to retrieve a user, so it should make use of delegation to call other objects to do the dirty work of finding something somewhere.
You can checkout more about how the session Auth class work in the source of laravel:
https://github.com/laravel/framework/blob/5.4/src/Illuminate/Auth/SessionGuard.php
This is how I solved it. It needs improvements yet but at least can be useful as a general guideline.
Notes: I'm using PHP 8 and taking advantage of constructor property promotion, so maybe you'll have to assign the instance variables. I'm also using Laravel 8.
Implement your API gateway that implements the method:
function authenticate(string $username, string $password): ?APIUser {}
Create an API user model:
<?php
namespace App\Auth;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Hash;
final class APIUser implements Authenticatable
{
function __construct(
private $id,
private $username,
private $password,
private $access_token,
private $remember_token,
private $expires_in,
private $platform_id,
) {
$this->password = Hash::make($this->password);
}
function getUsername()
{
return $this->username;
}
function getAccessToken()
{
return $this->access_token;
}
function getExpiresIn()
{
return $this->expires_in;
}
function getPlatformId()
{
return $this->platform_id;
}
/**
*
* Authenticatable section
*
*/
/**
* Get the name of the unique identifier for the user.
* #return string
*/
public function getAuthIdentifierName()
{
return 'id';
}
/**
* Get the unique identifier for the user.
* #return mixed
*/
public function getAuthIdentifier()
{
return $this->id;
}
/**
* Get the password for the user.
* #return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* Get the token value for the "remember me" session.
* #return string
*/
public function getRememberToken()
{
$this->remember_token;
}
/**
* Set the token value for the "remember me" session.
* #param string $value
* #return void
*/
public function setRememberToken($value)
{
$this->remember_token = $value;
}
/**
* Get the column name for the "remember me" token.
*
* #return string
*/
public function getRememberTokenName()
{
return 'remember_token';
}
}
Create the user provider:
<?php
namespace App\Auth;
use App\Ports\AppAPIGateway;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Session\Session;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
class APIUserProvider implements UserProvider
{
function __construct(
private AppAPIGateway $api,
private Session $session
) {
}
public function retrieveById($identifier)
{
if ($this->session->has('user')) {
return $this->session->get('user');
}
return;
}
public function retrieveByToken($identifier, $token)
{
Log::info('APIUserProvider: retrieveByToken (not implemented)');
return null;
}
public function updateRememberToken(Authenticatable $user, $token)
{
$user->setRememberToken($token);
}
public function retrieveByCredentials(array $credentials)
{
if (
!array_key_exists('username', $credentials)
||
!array_key_exists('password', $credentials)
) {
throw new AuthenticationException('Could not retrieve credentials: missing parameters');
}
$user = $this->api->authenticate(
$credentials['username'],
$credentials['password']
);
$this->session->put('user', $user);
$this->session->save();
return $user;
}
public function validateCredentials(Authenticatable $user, array $credentials)
{
if (
!array_key_exists('username', $credentials)
||
!array_key_exists('password', $credentials)
) {
throw new AuthorizationException('Could not validate credentials: missing parameters');
}
return
$user->getUsername() == $credentials['username']
&& Hash::check($credentials['password'], $user->getAuthPassword());
}
}
Modify app/Providers/AuthServiceProvider.php:
public function boot()
{
$this->registerPolicies();
Auth::provider('api_driver', function ($app, array $config) {
return new APIUserProvider(
$app->make('App\Ports\AppAPIGateway'),
$app->make('Illuminate\Contracts\Session\Session')
);
});
}
Enable this new provider in config/auth.php:
'defaults' => [
'guard' => 'api_guard',
'passwords' => 'api_provider',
],
...
'guards' => [
'api_guard' => [
'driver' => 'session',
'provider' => 'api_provider',
],
],
...
'providers' => [
'api_provider' => [
'driver' => 'api_driver',
],
],
When you did do
$user = new GenericUser($attributes);
You can follow with
Auth::login($user);
Now all your User attributes are stored in session and can be retrieved by calling
$user = Auth::user();
I have altered my authController.php file to do a few things needed for my project. It works great, just need to make one more change. Right now the controller looks like this:
<?php
namespace App\Http\Controllers\Auth;
use App\User;
use App\Role;
use Mail;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Foundation\Auth\ResetsPasswords;
class AuthController extends Controller
{
/*
|--------------------------------------------------------------------------
| Registration & Login Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users, as well as the
| authentication of existing users. By default, this controller uses
| a simple trait to add these behaviors. Why don't you explore it?
|
*/
use AuthenticatesAndRegistersUsers, ThrottlesLogins, ResetsPasswords;
/**
* Where to redirect users after login / registration.
*
* #var string
*/
protected $redirectTo = '/add';
/**
* Create a new authentication controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware($this->guestMiddleware(), ['except' => 'logout']);
}
/**
* Overwrite the Laravel 5.2 standard registration
* so user will not be logged in automatically.
*
* #param array $request
* #return Register
*/
public function register(Request $request)
{
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
$this->create($request->all());
return redirect($this->redirectPath());
}
/**
* Extend password reset email to user for when registering
*/
public function sendResetLinkEmail(Request $request)
{
$this->validateSendResetLinkEmail($request);
$broker = $this->getBroker();
$this->subject = "First Time User Setup";
$broker->emailView = "auth.emails.password";
$response = Password::broker($broker)->sendFirstTimeSetup(
$this->getSendResetLinkEmailCredentials($request),
$this->resetEmailBuilder()
);
switch ($response) {
case Password::RESET_LINK_SENT:
return $this->getSendResetLinkEmailSuccessResponse($response);
case Password::FIRST_TIME_SETUP:
return $this->getSendFirstTimeSetupEmailSuccessResponse($response);
case Password::INVALID_USER:
default:
return $this->getSendResetLinkEmailFailureResponse($response);
}
}
public function getSendFirstTimeSetupEmailSuccessResponse($response)
{
return redirect()->back()->with('status', trans($response));
}
/**
* Get a validator for an incoming registration request.
*
* #param array $data
* #return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'first-name' => 'required|max:255',
'last-name' => 'required|max:255',
'phone' => 'required|max:255',
'form' => 'max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6|confirmed',
]);
}
/**
* Create a new user instance after a valid registration.
*
* #param array $data
* #return User
*/
protected function create(Request $request, array $data )
{
//Create the user
$user = User::create([
'first_name' => $data['first-name'],
'last_name' => $data['last-name'],
'phone' => $data['phone'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
return $this->postEmail($request);
/*
Mail::send('auth.emails.registered', ['user' => $user], function ($m) use ($user)
{
$m->to($user->email, $user->first_name)->subject('You Have Been Added');
});*/
//Is it a User? Then give them that role
if ($data['user-role'] == 'user')
{
$role = Role::where('name', '=', 'user')->firstOrFail();
$user = User::find($user->id);
$user->roles()->attach($role->id);
}
//Is it an Admin? Then give them that role
if ($data['user-role'] == 'admin')
{
$role = Role::where('name', '=', 'owner')->firstOrFail();
$user = User::find($user->id);
$user->roles()->attach($role->id);
}
return $user;
}
}
It stops auto login on user creation, assigns a role to the user based on the form and sends a password reset email. The trouble is not I get the error Trait method guestMiddleware has not been applied, because there are collisions with other trait methods on App\Http\Controllers\Auth\AuthController
One of the tricks to this is understanding how the traits inside of the Auth factory work.
First, we'll need to use the use Illuminate\Foundation\Auth\ResetsPasswords; Trait as well as the AuthenticatesAndRegistersUsers and the ThrottlesLogins trait.
use AuthenticatesAndRegistersUsers, ThrottlesLogins, ResetsPassword;
Next, we need to make sure that we have a $request instance being passed to our create method:
protected function create(Illuminate\Http\Request $request, array $data)
Sidenote - I would recommend moving away from passing in a $data object as an argument and instead working with $request. You can get your data like this: $request->get('first-name'), etc.
Finally, we pass our $request to the postEmail() function:
return $this->postEmail($request);
This will pipe the $request to the ResetsPasswords\postEmail function:
public function postEmail(Request $request)
{
return $this->sendResetLinkEmail($request);
}
Which will inturn pipe it to the ResetsPasswords\sendResetLinkEmail function:
public function sendResetLinkEmail(Request $request)
{
$this->validateSendResetLinkEmail($request);
$broker = $this->getBroker();
$response = Password::broker($broker)->sendResetLink(
$this->getSendResetLinkEmailCredentials($request),
$this->resetEmailBuilder()
);
switch ($response) {
case Password::RESET_LINK_SENT:
return $this->getSendResetLinkEmailSuccessResponse($response);
case Password::INVALID_USER:
default:
return $this->getSendResetLinkEmailFailureResponse($response);
}
}
Which will ultimately send out an email.
The key to making this entire thing work is that the instance of Illuminate\Http\Request always contains an email field.
Note
The response that will be returned from the sendResetLinkEmail function may not be the same response that you sent out. Perhaps Users are confused by a Password Reset request when they've just created their account. Instead, maybe you want to send a First Time Setup. In order to do this, you will need to create your own Password Broker and Password Facade.
Next, lets make 2 new directories:
App\Brokers
App\Facades
Next, make a new file inside of App\Facades and call it Password.php. Namespace the file accordingly and extend the existing Password Facade. Also, we'll add another const as an observable response type for our FIRST_TIME_SETUP.
<?php
namespace App\Facades;
class Password extends \Illuminate\Support\Facades\Password {
/**
* Constant representing a successfully sent reminder.
*
* #var string
*/
const FIRST_TIME_SETUP = 'passwords.first_time_setup';
}
Now we have added another response type that we can switch on which will help dictate how we send out our email.
Next, we need to make our Password Broker and establish our sendFirstTimeSetup functionality.
namespace App\Brokers;
class PasswordBroker extends Illuminate\Auth\Passwords\PasswordBroker {
public function sendFirstTimeSetup(array $credentials, Closure $callback = null)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
if (is_null($user)) {
return static::INVALID_USER;
}
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$token = $this->tokens->create($user);
$this->emailResetLink($user, $token, $callback);
return static::FIRST_TIME_SETUP;
}
}
Now we need to copy the sendResetLinkEmail function that we saw earlier, and move it into our AuthController. This will allow us to modify the Password Facade that we use, and modify our switch statement to support our First Time Setup
public function sendResetLinkEmail(Request $request)
{
$this->validateSendResetLinkEmail($request);
$broker = $this->getBroker();
$response = Password::broker($broker)->sendFirstTimeSetup(
$this->getSendResetLinkEmailCredentials($request),
$this->resetEmailBuilder()
);
switch ($response) {
case Password::RESET_LINK_SENT:
return $this->getSendResetLinkEmailSuccessResponse($response);
case Password::FIRST_TIME_SETUP:
return $this->getSendFirstTimeSetupEmailSuccessResponse($response);
case Password::INVALID_USER:
default:
return $this->getSendResetLinkEmailFailureResponse($response);
}
}
Then, also in the AuthController, we'll make another function to return our response:
public function getSendFirstTimeSetupEmailSuccessResponse($response)
{
return redirect()->back()->with('status', trans($response));
}
Finally, if you want to override the view that is used by the function when being sent out, simply override the $broker->emailView property before invoking the ->sendFirstTimeSetup() function:
//...
$broker->emailView = "emails.user.first_time_setup";
$response = Password::broker($broker)->sendFirstTimeSetup(
$this->getSendResetLinkEmailCredentials($request),
$this->resetEmailBuilder()
);
//...
If you want to change the subject of the email, override the ->subject() property inside of your AuthController prior to firing your ->sendFirstTimeSetup() function:
//...
$this->subject = "First Time User Setup";
$broker->emailView = "emails.user.first_time_setup";
$response = Password::broker($broker)->sendFirstTimeSetup(
$this->getSendResetLinkEmailCredentials($request),
$this->resetEmailBuilder()
);
//...
I could go on and on, but I think this will get you going in the right direction. Hopefully it will help a few others as well.
You can fix your trait collision issue by changing the use block to the following:
use ThrottlesLogins,
ResetsPasswords,
AuthenticatesAndRegistersUsers {
AuthenticatesAndRegistersUsers::guestMiddleware insteadof ResetsPasswords;
AuthenticatesAndRegistersUsers::getGuard insteadof ResetsPasswords;
AuthenticatesAndRegistersUsers::redirectPath insteadof ResetsPasswords;
}
It's ugly, but it will fix all related collisions, thus allowing you to just use the pre-built Illuminate methods if you so choose.
Within Laravel you can easily define abilities and then hook into them later on a user request regarding to do different actions:
$gate->define('update-post', function ($user, $post) {
return $user->id === $post->user_id;
});
But almost all my defined abilities has this part $user->id === $model->user_id in it. I don't like it as it's a kind of repeating a condition over and over which I think could be more abstract.
Most of my defined abilities are according to updating/deleting records, so it would be better if I could make a global condition applied to all of them or if there could be a group ability defining which is like to what we do in routing.
Is there any workaround for it? I really like it DRY.
Everything in Laravel is extendable, that's the power of its service providers.
You can extend the Gate object to a MyCustomGate object and do whatever you want in that object. Here's an example:
MyCustomGate.php
class MyCustomGate extends \Illuminate\Auth\Access\Gate
{
protected $hasOwnershipVerification = [];
/**
* Define a new ability.
*
* #param string $ability
* #param callable|string $callback
* #return $this
*
* #throws \InvalidArgumentException
*/
public function defineWithOwnership($ability, $callback, $foreignUserIdKey = "user_id")
{
// We will add this
$this->hasOwnershipVerification[$ability] = $foreignUserIdKey;
return $this->define($ability, $callback);
}
/**
* Resolve and call the appropriate authorization callback.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $ability
* #param array $arguments
* #return bool
*/
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback(
$user, $ability, $arguments
);
// We will assume that the model is ALWAYS the first key
$model = is_array($arguments) ? $arguments[0] : $arguments;
return $this->checkDirectOwnership($ability, $user, $model) && call_user_func_array(
$callback, array_merge([$user], $arguments)
);
}
/**
* Check if the user owns a model.
*
* #param string $ability
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param \Illuminate\Database\Eloquent\Model $model
* #return bool
*/
protected function checkDirectOwnership($ability, $user, $model)
{
if(!isset($this->hasOwnershipVerification[$ability])) {
return true
}
$userIdKey = $this->hasOwnershipVerification[$ability];
// getAuthIdentifier() is just ->id, but it's better in case the pk of a user is different that id
return $user->getAuthIdentifier() == $model->{$userIdKey};
}
}
Then, you will have to tell Laravel to use your gate instead of the default one. You ca do that in your AuthServiceProvider (assuming that it's extending Illuminate\Auth\AuthServiceProvider, just add the following method.
AuthServiceProvider
/**
* Register the access gate service.
*
* #return void
*/
protected function registerAccessGate()
{
$this->app->singleton(\Illuminate\Contracts\Auth\Access\Gate::class, function ($app) {
return new MyCustomGate($app, function () use ($app) {
return $app['auth']->user();
});
});
}
And this way, you can define abilities using defineWithOwnership() method instead of define(). You can still use define() for abilities that don't require ownership verification. There's a third parameter defineWithOwnership() accepts which is $foreignUserIdKey; that's used for the case when a model has a different field for the user id.
Note: I wrote the code on the fly and did not try it, it may have errors, but you get the idea.
I checked your question quite a bit, but I've found no "easy" way to do it.
Instead, what I would probably do is this:
<?php
namespace App\Policies;
use App\User;
use App\Post;
trait CheckOwnership {
protected function checkOwnership($user, $model) {
$owned = $user->id === $model->user_id;
if ($owned === false)
throw new NotOwnedException;
}
}
class PostPolicy
{
use CheckOwnership;
public function update(User $user, Post $post)
{
try {
$this->checkOwnership($user, $post);
//continue other checks
} catch (NotOwnedException $ex) {
return false;
}
}
}
Add this function to your AuthServiceProvider
public function defineAbilities(array $abilities, $gate)
{
foreach($abilities as $name => $model){
$gate->define($name, function ($user, $model){
return $user->id === ${$model}->user_id;
});
}
}
and then inside boot method
$this->defineAbilities(['ability1' => 'model1', 'ability2' => 'model2'], $gate);
You can define another function and call it within the anonymous function. This will allow you to have commonly-used code in one central location while still allowing any resource-specific logic.
Add this function to your AuthServiceProvider class:
public function userCheck(User $user, $target)
{
// do the user id check
$result = isset($target->user_id) && isset($user) && $user->id === $target->user_id;
return $result;
}
Your code, modified:
$gate->define('update-post', function ($user, $post) {
// call the function
$result = $this->userCheck($user, $post);
// do some kind of 'update-post' specific check
return $result/* && some_bool_statement*/;
});
I think you can use middlewares.
Simply make a admin middleware and use it in your routes and routes group.
And there is no security bug on your project (delete, create & ... actions) because Laravel has csrf token!
You can use before() function, also.
And then an important note:
if you don't define a correspond function on Policy class and call it $this->authorize($post) on a controller an unauthorized Action error will be thrown unless before()methodreturnstrue.
for example call $this->authorize on Dashboard\PostsController:
public function edit($id)
{
$post = Post::find($id)->first();
$this->authorize($post);
return view('dashboard.post')->with(compact('post'));
}
and if we defined a PostPolicy Class:
class PostPolicy
{
use HandlesAuthorization;
public function before($user, $ability)
{
return $user->is_admin;
}
}
If user be admin he/she can edit post because we returned true in before() method despite of have not a method with same name (as edit method in PostsController).
In fact Laravel will check for before method mthod on Policy Class. if before return'snull will check for correspond method with same name on controller method and if this method not found user cannot perform action.
Thank you laravel for DRY us!♥