As I am interested in getting the gender info form social network when user register, I've noticed that gender mapping is missing in the native Laravel socialite provider, for ex:
GoogleProvider.php
protected function mapUserToObject(array $user)
{
return (new User)->setRaw($user)->map([
'id' => $user['id'], 'nickname' => array_get($user, 'nickname'), 'name' => $user['displayName'],
'email' => $user['emails'][0]['value'], 'avatar' => array_get($user, 'image')['url'],
]);
}
So, I can't just edit the above method and map the gender field, because GoogleProvider.php is located in the vendor folder.
The question is, how would I override mapUserToObject in different social providers afforded by Laravel?
Or, what is the recommended approach in such case?
Providing Google passes a gender field you should be able to access it like...
$user = Socialite::driver('google')->user();
echo $user['gender'];
Related
I am trying to follow best practice for REST api on CRUD.
GET: users/ -> all
GET: users/:id -> one specific user.
POST: users/ -> add one
PUT: users/:id -> update specific user.
DELETE: users/:id -> delete one user.
On laravel 8 I want to validate the url :id using the validator, so I have like this on delete user:
$validator = Validator::make(['id' => $request->id], [
'id' => 'exists:users,id,deleted_at,NULL',
]);
And this way to update a user:
$validator = Validator::make(array_merge($request->all(), ['id' => $request->id]), [
'id' => 'required|exists:users,id,deleted_at,NULL',
'name' => 'required',
'surname' => 'required',
'email' => 'required|email:rfc,dns'
]);
As you can see I have to put the id on an array and/or merge with the $request->all().
There is any way in laravel to do this with the request?
I have found 3 ways by Laravel:
$request->add(['variable' => 'value']);
$request->merge(["key"=>"value"]);
$request->request->set(key, value);
But a solution for adding route params to the request before hitting the controller method would be even great.
You can update the request object on the fly and add the ID field, before you validate it, something like
$request['id'] = $id;
// note: the $id is your function's parameter name
$validator = Validator::make(array_merge($request->all()), [
'id' => 'required|exists:users,id,deleted_at,NULL',
'name' => 'required',
'surname' => 'required',
'email' => 'required|email:rfc,dns'
]);
You can do it like you are doing, but doing it with route model binding would be the way to go.
Now when you want to update a user by sending a PUT to /users/:id, and the user does not exist you will get a 422. But what you really want would be a 404.
With route model binding, Laravel will check if the model exists for you and abort with a 404 when it does not.
If route model binding is not an option, you can also just not validate the id with the validator and retrieve the user with User::findOrFail($request->input('id')), the framework will then still abort with a 404 if it can't be found.
I have a Model called Type with a title field and a pretty_slug field.
I have a test that is checking that a user can not update a Type instance:
<?
public function test_user_cannot_put_update_page() {
$type = Type::factory()->make([
'title' => 'Original type',
]);
$type->save();
$response = $this->put(route('types.update', [
'pretty_slug' => $type->pretty_slug,
'title' => 'New type',
]));
$response->assertForbidden();
$this->assertDatabaseHas('types', [
'title' => 'Original type'
]);
}
If I do dd($response->getContent()); I can see that a redirect is happening:
Now the weird thing is that I have the exact same for another Model called Level:
<?
public function test_user_cannot_put_update_page() {
$level = Level::factory()->make([
'title' => 'Original level',
]);
$level->save();
$response = $this->put(route('levels.update', [
'pretty_slug' => $level->pretty_slug,
'title' => 'New level',
]));
$response->assertForbidden();
$this->assertDatabaseHas('levels', [
'title' => 'Original level'
]);
}
The Model Level is exactly the same as Type: the same Controller, the same Trait shared, same Policy, same Tests, same routes… I have other Models called Idea, Concept and Episode that have the exact same behavior.
All tests pass, except for my Type Model:
I have no idea why this particular types.update route is not working. It should return a 302 but is instead redirecting.
It is working when I use the webform: the Type instance updates correctly. But the test is failing.
How can I debug this test? Where do I look for an issue in my code?
Thanks for any help.
EDIT 1: added controller and routes
I think the problem is the 'levels.update' route might be protected by the auth middleware.
Since there is no user logged in, the auth middleware will attempt to redirect to the login page instead.
I like IGP's answer. But if that's not it, you might want to check your host configs. Apache or Nginx, or whatever you're using. It could be the route is being called via http and your server is redirecting to https, or visa versa.
Thanks to #Aless55, I found the issue: it was the validation of my Type model that was preventing me from updating the instance.
I looked into the StoreType file, in which I had:
'order' => 'required|numeric',
This means the order field is required. But when I tried calling the types.update route, I wasn't including that field.
One solution would have been to make that field optional. But I ended up including the order field in my test:
$response = $this->put(route('types.update', [
'pretty_slug' => $type->pretty_slug,
'title' => 'Alex new type',
'order' => 1,
]));
I'm authenticating my users on my web service and then creating Firebase custom token via php-jwt:
// Requires: composer require firebase/php-jwt
use Firebase\JWT\JWT;
// Get your service account's email address and private key from the JSON key file
$service_account_email = ...;
$private_key = ...;
function create_custom_token($uid, $is_premium_account) {
global $service_account_email, $private_key;
$now_seconds = time();
$payload = array(
"iss" => $service_account_email,
"sub" => $service_account_email,
"aud" => "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
"iat" => $now_seconds,
"exp" => $now_seconds+(60*60), // Maximum expiration time is one hour
"uid" => $uid,
"claims" => array(
"premium_account" => $is_premium_account
)
);
return JWT::encode($payload, $private_key, "RS256");
}
But the users that I authenticate this way, don't show the administrator-friendly "Identifier" and "Providers" fields in the "Authentication" panel in the Firebase Console:
The first two are users that I authenticated via this custom authentication process, and the last one is a user that I authenticated directly via Google.
How can I populate the "Identifier" and the "Providers" fields for users created via custom authentication?
The "Providers" column will only display an icon if the information attached to a user matches one or more of the the given providers in the "Sign-In Methods" section (https://console.firebase.google.com/project/_/authentication/providers).
Custom providers don't have a distinct icon, and Firebase wouldn't know what to display in the "Identifier" column (the UID is already in its own column at the end).
However, you do have some control for the display of the columns by creating them in advance (meaning: before signing them in for the first time), or by updating the user information after the user entry has been created.
I prepared an example showing which combination of fields leads to which display:
Please note:
The display name has no effect: if it is the only data provided, the user is considered anonymous.
Email + Password match the "Email/Password" Provider
Phone Numbers will alway match the "Phone" provider
The icons for a matched provider will be displayed in the column, even if a provider has been disabled.
Emails and Phone numbers have to be unique. If your application allows multiple users with the same email address/phone number, you will get into trouble, if you just want to see more information about the users of your Firebase project.
You can create and update users via the Firebase Auth REST API, but I would suggest to use one of the official Firebase Admin SDKs SDK to do it - in case you want to stick to PHP, I happen to know an unofficial one: kreait/firebase-php (Documentation) (Disclaimer: I'm the maintainer of the PHP SDK :) ).
On a non-technical note: I wouldn't bother too much with the user list in the Firebase Web Console: use the Firebase CLI tool or one of the official (or unofficial ;) ) Admin SDKs to create an overview that meets your needs.
You mentioned in the Bounty Annotation that you asked this in the Firebase Slack Community without an answer - you can find me and other PHP developers in the #php channel. I enabled notifications for the channel, so please feel free to join if you have further questions.
FYI, this is the code I wrote with the PHP SDK to create the data for the screenshot above:
<?php
declare(strict_types=1);
use Kreait\Firebase;
use Kreait\Firebase\Util\JSON;
require_once __DIR__.'/vendor/autoload.php';
$serviceAccount = Firebase\ServiceAccount::fromJsonFile(__DIR__.'/service_account.json');
$firebase = (new Firebase\Factory())
->withServiceAccount($serviceAccount)
->create();
$auth = $firebase->getAuth();
// Remove all users
foreach ($auth->listUsers() as $user) {
$auth->deleteUser($user->uid);
}
// Simulate custom auth
$ct = $auth->createCustomToken('a-custom-auth');
$r = $auth->getApiClient()->exchangeCustomTokenForIdAndRefreshToken($ct);
echo JSON::prettyPrint($auth->getUser('a-custom-auth'));
echo JSON::prettyPrint($auth->createUser([
'uid' => 'displayname-only',
'displayName' => 'Jérôme Gamez',
]));
echo JSON::prettyPrint($auth->createUser([
'uid' => 'email-only',
'email' => 'jerome#example.org',
]));
echo JSON::prettyPrint($auth->createUser([
'uid' => 'email-and-password',
'email' => 'jerome#example.com',
'password' => 'password'
]));
echo JSON::prettyPrint($auth->createUser([
'uid' => 'phone-only',
'phoneNumber' => '+49-123-1234567',
]));
echo JSON::prettyPrint($auth->createUser([
'uid' => 'email+name+phone',
'email' => 'jerome#example.net',
'displayName' => 'Jérôme Gamez',
'phoneNumber' => '+49-123-7654321',
]));
echo JSON::prettyPrint($auth->createUser([
'uid' => 'email+name+password+phone',
'email' => 'jerome#example.de',
'displayName' => 'Jérôme Gamez',
'password' => 'example123',
'phoneNumber' => '+49-321-7654321',
]));
I have created a complete code which allows Facebook Login by getting Name, Email and Avatar of the user in the database by using Laravel Socialite.
I did it by writing this:
$user = User::create([
'email' => $providerUser->getEmail(),
'name' => $providerUser->getName(),
'avatar' => $providerUser->getAvatar(),
'password' => md5(rand(1,10000)),
]);
But for putting Gender, BirthDate and Address in the database, I tried this.
$user = User::create([
'email' => $providerUser->getEmail(),
'name' => $providerUser->getName(),
'avatar' => $providerUser->getAvatar(),
'gender' => $providerUser->getGender(),
'bio' => $providerUser->getBio(),
'password' => md5(rand(1,10000)),
]);
But, there are no functions such as getGender() or getBio(). Do you know what they are?
I have also asked necessary permissions to Facebook Driver in SocialController.php
public function redirect()
{
return Socialite::driver('facebook')->fields([
'first_name', 'last_name', 'email', 'gender', 'birthday'
])->scopes([
'email', 'user_birthday'
])->redirect();
}
All necessary stuffs like Database Tables, Migrations, Models and a working project without Gender and Bio is ready. But I need to add more info in database. How can I do so?
I search such questions already on StackOverflow but didn't find a satisfying answer. I hope someone answers this.
Thank you in advance.
Default socialite provider for Facebook does not have the method to get gender & bio.
For more info, You can check the available methods here:
https://github.com/laravel/socialite/blob/3.0/src/Contracts/User.php
https://github.com/laravel/socialite/blob/3.0/src/AbstractUser.php
This StackOverflow might help you:
How to get birthday gender and religion information using laravel socialite implementing facebook Api
Apart from getting the scope in redirect you also need to pass them in call back
public function handleFacebookCallback()
{
$facebook_user = Socialite::driver('facebook')->fields([
'first_name', 'last_name', 'email', 'gender', 'birthday'
])->user();
}
Check available helper methods
https://github.com/laravel/socialite/blob/3.0/src/AbstractUser.php
offsetExists('gender')
offsetGet('gender')
These methods can help get extra info from users array
These are my rules in my class:
class AppointmentsController extends Controller
{
protected $rules = [
'appointment' => ['required', 'min:5'],
'slug' => ['required', 'unique:appointments'],
'description' => ['required'],
'date' => ['required', 'date_format:"Y-m-d H:i"'],
];
This is in the laravel official docs:
Sometimes, you may wish to ignore a given ID during the unique check.
For example, consider an "update profile" screen that includes the
user's name, e-mail address, and location. Of course, you will want to
verify that the e-mail address is unique. However, if the user only
changes the name field and not the e-mail field, you do not want a
validation error to be thrown because the user is already the owner of
the e-mail address. You only want to throw a validation error if the
user provides an e-mail address that is already used by a different
user. To tell the unique rule to ignore the user's ID, you may pass
the ID as the third parameter:
'email' => 'unique:users,email_address,'.$user->id.',user_id'
I tried using this in my rules:
'slug' => ['required', 'unique:appointments,id,:id'],
This indeed ignores the current row BUT it ignores it completely. What I want to accomplish is, I want it to ignore the current row only if the slug is unchanged. When it is changed to something that is already unique in another row, I want it to throw an error.
The Unique validator works like that
unique:table,column,except,idColumn
So in your case, you can do it like that:
Get the id you want to validate against, you can get it from the route or with any other way that works for you; something like that
$id = $this->route('id');
'slug' => ['required','unique:appointments,slug,'.$id],
For example we need to update contact info into Users table.
In my model User I created this static method:
static function getContactDataValidationRules( $idUserToExcept ) {
return [
'email' => 'required|email|max:255|unique:users,email,' . $idUserToExcept,
'pec' => 'required|email|max:255',
'phone' => 'required|regex:/^([0-9\s\-\+\(\)]*)$/|min:8|max:20',
'mobile' => 'required|regex:/^([0-9\s\-\+\(\)]*)$/|min:8|max:20',
'phone2' => 'required|regex:/^([0-9\s\-\+\(\)]*)$/|min:8|max:20',
'recovery_email' => 'required|email|max:255',
];
}
and in my UsersController, into the method that update User I've:
$id = $request->input('id');
$request->validate(User::getContactDataValidationRules( $id ));
:-)