I am using Laravel Cashier with Stripe, and I am trying to cover the the following lines in my tests, so far I tried mocking but that did not work, I was wondering what is the best practice to test them
use function Illuminate\Events\queueable;
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::updated(queueable(function ($customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}
The code is taken from https://laravel.com/docs/9.x/billing#syncing-customer-data-with-stripe
You could achieve this by using a partial mock, Laravel has a helper for that method. Since you are actually using correct dependency injection, this would be my approach. It requires the queue setting in phpunit to be sync.
public function test_update_customer()
{
$account = Account::factory()->create(['stripe_id' => 'fake']);
$this->partialMock(Account::class, function ($mock) {
$mock->shouldReceive('syncStripeCustomerDetails')
->once();
});
$response = $this->actingAs($someUser)
->put(route('billing.update', $account), [
'name' => 'Some name',
'email' => 'Another email',
]);
$response->assertOk();
}
I am working in two apps: accounts.domain.com (Laravel app) and dash.domain.com (Not laravel, but php). I want dash users to login through accounts to use the app, so I figured I could use OAuth to achieve this.
I installed Laravel Passport and everything worked fine when getting an authorization code:
$query = http_build_query([
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'response_type' => 'code',
'scope' => '*',
'state' => $state,
]);
return redirect('https://accounts.domain.com/oauth/authorize?'.$query);
But then I tried to get the access token:
$response = $http->post('https://accounts.domain.com/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'redirect_uri' => $redirectUri,
'code' => $code,
],
]);
And I got this error:
{
"error": "invalid_client",
"error_description": "Client authentication failed",
"message": "Client authentication failed"
}
So I googled the error, and I found that maybe there was an error with my credentials, so I check them, tried to recreate them, and nothing.
Finally I got to this file vendor/laravel/passport/src/Bridge/ClientRepository.php and I found something really interesting in the handlesGrant method that is used to verify a client:
protected function handlesGrant($record, $grantType)
{
// ...
switch ($grantType) {
case 'authorization_code':
return ! $record->firstParty();
// ...
default:
return true;
}
}
I changed this line
return ! $record->firstParty();
To this:
return $record->firstParty();
And everything worked. So, what I can see is that, using 'grant_type' => 'authorization_code' is only valid for third party clients.
My question is: ¿Why can't first party clients use 'authorization_code' as grant type? And if they can, ¿how can I implement this without changing Laravel Passport files?
I stumbled across the same problem, don't know why this is the default behavior. You can easily extend the ClientRepository and rebind it to the service container:
Create a file app\Passport\ClientRepository.php and put the following content:
<?php
namespace App\Passport;
use Laravel\Passport\Bridge\ClientRepository as BaseClientRepository;
class ClientRepository extends BaseClientRepository
{
/**
* Determine if the given client can handle the given grant type.
*
* #param \Laravel\Passport\Client $record
* #param string $grantType
* #return bool
*/
protected function handlesGrant($record, $grantType)
{
if (is_array($record->grant_types) && ! in_array($grantType, $record->grant_types)) {
return false;
}
switch ($grantType) {
case 'personal_access':
return $record->personal_access_client && $record->confidential();
case 'password':
return $record->password_client;
case 'client_credentials':
return $record->confidential();
default:
return true;
}
}
}
Register your ClientRepository, to rebind it to the service container:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Passport\ClientRepository;
class AuthServiceProvider extends ServiceProvider
{
// Other code
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->bindClientRepository();
}
/**
* Register the client repository.
*
* #return void
*/
protected function bindClientRepository()
{
$this->app->bind(\Laravel\Passport\Bridge\ClientRepository::class, ClientRepository::class);
}
}
I want to create an invite when a member request is approved, using Events and Listeners in Laravel 7.
Invite controller:
class InviteController extends Controller
{
private InviteRepositoryInterface $invite_repository;
public function __construct(InviteRepositoryInterface $invite_repository)
{
$this->invite_repository = $invite_repository;
}
public function __invoke(InviteRequest $request)
{
$member_request = MemberRequest::find($request->member_request_id);
if ($this->_MemberRequestIsValid($member_request))
return response()->json(['error' => ['message' => 'Please, enter an existing member request.', 'object' => $member_request]]);
$this->invite_repository->createByMemberRequestId($member_request->id);
return response()->json(['success' => true]);
}
What I tried: redirect()->route('invite', ['member_request_id' => $event->member_request->id]);
and: app(InviteController::class)->__invoke(['member_request_id' => $event->member_request->id]);
Both didn't work because of constructor.
UPDATE:
Listener:
namespace App\Listeners\MemberRequest\Approved;
use App\Events\MemberRequest\Approved;
use App\Http\Controllers\Auth\Registration\Invite\InviteController;
class CreateInvite
{
/**
* Handle the event.
*
* #param Approved $event
* #return void
*/
public function handle(Approved $event)
{
redirect()->route('invite', ['member_request_id' => $event->member_request->id]);
// app(InviteController::class)->__invoke(['member_request_id' => $event->member_request->id]);
}
}
When I fire the event, the invite is not created but the InviteController invoke method does work according to my tests.
Testresults:
I fixed it by using repositories.
(new InviteRepository(new MemberRequestRepository()))->createByMemberRequestId($event->member_request->id);
I have a trait that uses accessors and mutators to encrypt model values:
trait Encryptable
{
public function getAttribute($key)
{
$value = parent::getAttribute($key);
if (in_array($key, $this->encryptable)) {
$value = Crypt::decrypt($value);
return $value;
} else {
return $value;
}
}
public function setAttribute($key, $value)
{
if (in_array($key, $this->encryptable)) {
$value = Crypt::encrypt($value);
}
return parent::setAttribute($key, $value);
}
}
Comments Model
protected $fillable = ['content','user_id','commentable_id', 'commentable_type'];
protected $encryptable = [
'content'
];
CommentController
public function storePostComment(Request $request, Post $Post)
{
$this->validate($request, [
'content' => 'required',
]);
$comment = $post->comments()->create([
'user_id' => auth()->user()->id,
'content' => $request->content
]);
dd($comment->content);
//return new CommentResource($comment);
}
What's happening is that when I pass the return new CommentResource($comment); gives me the comments content encrypted, but dd($comment->content); decrypts the comments content. How do I decrypt the entire comment object so I can output it in a resource?
Edit For CommentResource
class CommentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'content' => $this->content,
'owner' => $this->owner,
];
}
}
Edit 2 for answer
Here's my attempt:
use App\Comment;
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CommentResource extends JsonResource
{
public function __construct(Comment $resource)
{
$this->resource = $resource;
}
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'content' => $this->content,
'owner' => $this->owner,
];
}
}
Error:
Argument 1 passed to App\Http\Resources\CommentResource::__construct() must be an instance of App\Http\Resources\Comment, instance of App\Comment given, called in /Applications/MAMP/htdocs/my-app/app/Http/Controllers/Api/CommentController.php on line 31
Edit 3 (final edit)
Here's what I figured out:
I tried a bunch of various combinations along with #Edwin Krause answer. I have another model using this encryptable trait and outputting in a resource that works fine.
To give a bit more context to this question I found out there was a problem using assertJsonFragment in a test:
CommentsTest
/* #test **/
public function a_user_can_comment_on_a_post()
{
$decryptedComment = ['content'=>'A new content']
$response = $this->json('POST', '/api/comment/' . $post->id, $decryptedComment);
$response->assertStatus(201);
$response->assertJsonStructure([
'data' => [
'owner',
'content'
]
])
->assertJsonFragment(['content' => $decryptedContent['content']]);
}
assertJsonFragment was returning the encrypted content and therefore failing because it was being tested against the decrypted comments content.
I used dd(new CommentResource($comment)); in the controller to check to see if it the content was decrypting, it wasn't.
I tried various different things trouble shooting with dd() in the controller method and even testing in the browser. Still nothing. I added #Edwin Krause code and still nothing on dd()
I finally got lucky and got rid of dd() with #Edwin Krause and changing my controller to:
Working code combined with #Edwin Krause answer in my CommentResource
$comment = Comment::create([
'user_id' => auth()->user()->id,
'content' => $request->content,
'commentable_type' => 'App\Post',
'commentable_id' => $post->id,
]);
return new CommentResource($comment);
The tests went green. I tried dd(new CommentResource($comment)); and the content was encrypted still. The content output on the broweser and assertJsonFragment worked. I must've tried so many combinations to try and figure this out and I kind of just got lucky.
I'm unsure as to why this is the way it is, but I've already spent hours on this, so I can't troubleshoot why it's breaking. Maybe someone else can.
Just a suggestion to try and override the constructor of the JsonResource and typecast the $resource parameter to your Modelclass.
It work's for other things, not sure if it fixes your issue, that needs to be tested
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Comment;
class CommentResource extends JsonResource
{
public function __construct(Comment $resource)
{
$this->resource = $resource;
$this->resource->content = $resource->content;
}
....
Edit:
I Played around a bit more with the constructor and the modified version should actually work. I don't have any encrypted data to play with, but logically this should work.
How can you authenticate multiple types of users in Laravel 5.1 e.g. Jobseeker, Recruiter, Admin etc.
Some of you have suggested using a single users table to store only the password and email, creating profile tables to store user specific information (jobseeker_profile, recruiter_profile) and using roles to differentiate between the different types of users (i.e having a roles and role_user) table.
This is all very well but then what if the different types of users have different registration and login forms. How do you customize the default auth controller out of the box to display the correct view?
So if I have the following routes:
// Jobseeker Authentication routes...
Route::get('auth/login', 'Auth\AuthController#getLogin');
Route::post('auth/login', 'Auth\AuthController#postLogin');
Route::get('auth/logout', 'Auth\AuthController#getLogout');
// Jobseeker Registration routes...
Route::get('auth/register', 'Auth\AuthController#getRegister');
Route::post('auth/register', 'Auth\AuthController#postRegister');
// Recruiter Authentication routes...
Route::get('recruiter/auth/login', 'Auth\AuthController#getLogin');
Route::post('recruiter/auth/login', 'Auth\AuthController#postLogin');
Route::get('recruiter/auth/logout', 'Auth\AuthController#getLogout');
// Recruiter Registration routes...
Route::get('recruiter/auth/register', 'Auth\AuthController#getRegister');
Route::post('recruiter/auth/register', 'Auth\AuthController#postRegister');
This is the default auth controller out of the box:
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers;
public function __construct()
{
$this->middleware('guest', ['except' => 'getLogout']);
}
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed|min:6',
]);
}
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
}
traits used by the default out of the box auth controller:
trait AuthenticatesUsers
{
use RedirectsUsers;
public function getLogin()
{
return view('auth.login');
}
public function postLogin(Request $request)
{
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
$credentials = $this->getCredentials($request);
if (Auth::attempt($credentials, $request->has('remember'))) {
return redirect()->intended($this->redirectPath());
}
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => $this->getFailedLoginMessage(),
]);
}
public function loginPath()
{
return property_exists($this, 'loginPath') ? $this->loginPath : '/auth/login';
}
}
trait RegistersUsers
{
use RedirectsUsers;
public function getRegister()
{
return view('auth.register');
}
public function postRegister(Request $request)
{
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
Auth::login($this->create($request->all()));
return redirect($this->redirectPath());
}
}
I'm sure this is a very common requirement for many web applications but I can't find any helpful tutorials for Laravel specific implementations. All the tutorial simply focus on the out of the box implementation for some odd reason.
Any help on the above would be much appreciated.
This is not a solution to your question directly, but alternative way to solve your question problem with.
In stead of creating different username and password for different groups, make a central authentication that has roles. It called user and roles.
You can define groups with different roles, and each roles has specific access to respective area.
Regarding registration process you can make two differnet views but using the same controller, and for each view you can create a hidden field to indicate if it is jobseekers group or recruiter group.
Both will receive two different confirmation emails where they should fill the rest of the profile information, like recruiter should put company name and jobseeker should put his name etc. they might have two different tables for profile information, but still using the same login system.
By adding condition to middleware and correct route, if jobseeker tries to access recruiter area even if jobseeker is logged in the system, the jobseeker won't be able to access that area or the opposite way.
Since Laravel 5.1 has build in user login system, so you have few choices, build your own roles or use 3rd party.
I suggest you to build your own so you have control over your code and can further develop it as you wish with time. It might take you half day to get it run and understand how it works, but it is worth spending that time with the right approach in stead of the way you go in your Question OR using 3rd party is fine too, there is a lot of packages around you can search for. I have personally used Entrust (https://github.com/Zizaco/entrust) it is easy and nice way to provide roles and permissions to your project.
Here is also a link to video developed by Jeffrey Way at Laracast, it builds user and roles system from scratch for Laravel 4. but since you have user part, just follow roles part and with small modifications you will have a roles system to your Laravel 5.1, I have tried it and it works.
Regarding your question in the comments, when you follow the video you will understand the concept.
Link to the video: https://laracasts.com/lessons/users-and-roles
You might need to create account to see the video, most of videos are free.
Good practice
It is always also a good practice to illustrate what you want to achieve that makes things easier, I have just made an example for your project, but that is only example for learning:
I encourage you to read some of the topics regarding roles, here you will also find some inspiration to 3rd party acl systems to Laravel, there might be more articles but here is some:
Reading:
https://laracasts.com/discuss/channels/laravel/which-package-is-best-for-roles-permissions/?page=2
https://laracasts.com/discuss/channels/general-discussion/laravel-5-user-groups-management
https://laracasts.com/discuss/channels/general-discussion/roles-and-permissions-in-laravel-5
EDIT
Important Note
Laravel 5.1 has introduced Authorization, I have not found much documentation online yet but it is worth to spend some time learning it:
http://laravel.com/docs/5.1/authorization#policies
NEW UPDATE
There are some great videos solution for what you asking, follow ACL parts here
https://laracasts.com/series/whats-new-in-laravel-5-1
This might be very interesting too:
https://laracasts.com/lessons/email-verification-in-laravel
This will give you a complete own developed solution.
You can achieve multiple authentication easily by pulling up the sarav/laravel-multiauth package
composer require sarav/laravel-multiauth
I assume you have separate tables for Jobseeker, Recruiter, Admin.
Step 1 : Open app.php and replace
Illuminate\Auth\AuthServiceProvider::class
with
Sarav\Multiauth\MultiauthServiceProvider::class
Then, open up auth.php file and remove
<?php
return [
'driver' => 'eloquent',
'model' => 'App\User::class',
'table' => 'users',
'password' => [
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
],
];
and add the following code
return [
'multi' => [
'jobseeker' => [
'driver' => 'eloquent',
'model' => App\Jobseeker::class, // Model Class
'table' => 'jobseeker' // jobseeker table
],
'recruiter' => [
'driver' => 'eloquent',
'model' => App\Recruiter::class, // Model Class
'table' => 'recruiter' //recruiter table
],
'admin' => [
'driver' => 'eloquent',
'model' => App\Admin::class, // Model Class
'table' => 'admin' //admin table
],
],
'password' => [
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
]
];
Thats it!
Now you can try login attempt by calling
\Auth::attempt('jobseeker', ['email'=> 'johndoe#example.com', 'password' => 'secret']);
\Auth::attempt('recruiter', ['email'=> 'johndoe#example.com', 'password' => 'secret']);
\Auth::attempt('admin', ['email'=> 'johndoe#example.com', 'password' => 'secret']);
Always remember first paramter should be your user parameter. Here I have given jobseeker for jobseeker login attempt, recruiter for recruiter attempt and admin for admin login attempt. Without the proper first parameter system will throw exception.
For more detailed information checkout this article
http://sarav.co/blog/multiple-authentication-in-laravel-continued/
Short Answer: Add user types to your users table with specific number.
TL;DR answer.
Long Answer:
If you have migrated your table, just run php artisan migrate:rollback.
Add following line to your migration table for users:
$table->integer("user_type")->default(0);
Here I am considering that user type zero is just a simple JobSeeker.
And in your form, you can add option with value zero and one such that people will be selecting what they want to be like recruiter. There is no need of other
As another solution, i can suggest you to use a polymorphic relation between User and Account, like
class User extends Eloquent {
...
public function account() {
return $this->morphTo();
}
}
class Account extends Eloquent {
...
public function user() {
return $this->morphOne(App\User::class, 'account');
}
}
class JobSeeker extends Account { ... }
class Recruiter extends Account { ... }
For different types of Account, you can use route prefixes and different auth controllers, specially for registration who differs for each account instances :
// Recruiter Authentication routes...
Route::group(['prefix' => 'recruiter'], function() {
Route::controller('auth', 'Auth\RecruiterAuthController');
});
At last, you can access the authenticated account directly from auth()->user()->account. it will return any instance of Account (Recruiter, Admin, ....)
hope it helps you ;)
I will try to explain how authentication is managed in Laravel 5.1
On application start AuthServiceProvider is called, which calls registerAuthenticator() function in which new AuthManager is created.
AuthServiceProvider -> registerAuthenticator() -> new AuthManager()
On manager create createNameDriver function will be called in which new nameProvider will be created, where name is your auth driver selected in auth config file. Then in that function new Guard will be created and nameProivder will be passed to its contractor. All auth functions in that Guard will use functions from that provider to manage auth. Provider implements UserProvider which has
retrieveById($identifier),
retrieveByToken($identifier, $token),
updateRememberToken(Authenticatable $user, $token),
retrieveByCredentials(array $credentials),
validateCredentials(Authenticatable $user, array $credentials)
functions.
Main idea of managing multi auth in Laravel 5.1 is to create new AutServiceProvider and on its boot pass app auth new AuthModelProvider which functions then will be used in same Guard. In AuthModelProvider you can manage all retrieve functions the way you need.
Here is all changed I've made to manage multi auth. My project name is APC, that's why I use it everywhere.
Add this function to your models
public function getAuthIdentifier()
{
return [self::MODULE_NAME => $this->getKey()];
}
Create AuthServiceProvider in Provider/YourProjectName directory. In boot function we extend auth from our new provider AuthModelProvider.
<?php
namespace App\Providers\Apc;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Hashing\BcryptHasher;
class AuthServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
self::getAuthModels();
$this->app['auth']->extend('apc', function() {
return new AuthModelProvider(self::getAuthModels(), new BcryptHasher());
});
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
}
public static function getAuthModels()
{
$configModels = config('auth.models');
$authModels = [];
foreach ($configModels as $key => $class) {
$authModel = new $class();
$authModels [$key]= $authModel;
}
return $authModels;
}
}
Create AuthModelProvider in same directory. Diff in my models is existence of login field in company table. But you can be more specific if you want. In retrieveByCridentials function I just look for existence of login and choose my model accordingly.
<?php
namespace App\Providers\Apc;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Support\Str;
class AuthModelProvider implements UserProvider
{
protected $users;
protected $hasher;
public function __construct($usersModels, HasherContract $hasher)
{
$this->users = $usersModels;
$this->hasher = $hasher;
}
/**
* Retrieve a user by their unique identifier.
*
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifiers)
{
foreach ($identifiers as $key => $id) {
if (isset($this->users[$key])) {
return $this->users[$key]->where('id', $id)->active()->base()->first();
}
}
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifiers, $token)
{
return null;
$user = $this->getUserByIdentifier($identifiers);
if ($user) {
return $user->where($user->getRememberTokenName(), $token)->active()->first();
}
}
/**
* Update the "remember me" token for the given user in storage.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
$user->setRememberToken($token);
$user->save();
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials)) {
return null;
}
if (isset($credentials['login'])) {
$userModel = $this->users['company'];
} else {
$userModel = $this->users['user'];
}
$query = $userModel->newQuery();
foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
return $query->first();
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
private function getUserByIdentifier($identifiers)
{
if (!$identifiers) {
}
foreach ($identifiers as $namespace => $id) {
if (isset($this->users[$namespace])) {
return $this->users[$namespace];
}
}
return null;
}
}
Add AuthServiceProvider to app conf file.
\App\Providers\Apc\AuthServiceProvider::class,
Make this changes to auth conf file.
'driver' => 'apc',
'models' => [
\App\Apc\User\User::MODULE_NAME => \App\Apc\User\User::class,
\App\Apc\Company\Company::MODULE_NAME => \App\Apc\Company\Company::class
],
That's all. Hope it was helpful.