<?php namespace Common\Auth;
use App\Models\User;
use Carbon\Carbon;
use Common\Auth\Actions\CreateUser;
use Common\Auth\Events\SocialConnected;
use Common\Auth\Events\SocialLogin;
use Common\Core\Bootstrap\BootstrapData;
use Common\Core\Bootstrap\MobileBootstrapData;
use Illuminate\Contracts\View\View as ViewContract;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\View;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\One\User as OneUser;
use Laravel\Socialite\Two\User as TwoUser;
class Oauth
{
const OAUTH_CALLBACK_HANDLER_KEY = 'oauthCallbackHandler';
const RETRIEVE_PROFILE_ONLY_KEY = 'retrieveProfileOnly';
private array $validProviders = ['google', 'facebook', 'twitter', 'envato'];
public function loginWith(string $provider)
{
if (Auth::user()) {
return View::make('common::oauth/popup')->with(
'status',
'ALREADY_LOGGED_IN',
);
}
return $this->redirect($provider);
}
public function redirect(string $providerName)
{
$this->validateProvider($providerName);
return Socialite::driver($providerName)->redirect();
}
/**
* Retrieve user details from specified social account without logging user in or connecting accounts.
*/
public function retrieveProfileOnly(string $providerName)
{
$this->validateProvider($providerName);
Session::put([Oauth::RETRIEVE_PROFILE_ONLY_KEY => true]);
$driver = Socialite::driver($providerName);
// get user profile url from facebook
if ($providerName === 'facebook') {
$driver->scopes(['user_link']);
}
return $driver->redirect();
}
/**
* Disconnect specified social account from currently logged-in user.
*/
public function disconnect(string $provider): void
{
$this->validateProvider($provider);
Auth::user()
->social_profiles()
->where('service_name', $provider)
->delete();
}
/**
* Get user profile from specified social provider or throw 404 if it's invalid.
*/
public function socializeWith(
string $provider,
?string $token,
?string $secret,
) {
$this->validateProvider($provider);
if ($token && $secret) {
$user = Socialite::driver($provider)->userFromTokenAndSecret(
$token,
$secret,
);
} elseif ($token) {
$user = Socialite::driver($provider)->userFromToken($token);
} else {
$user = Socialite::with($provider)->user();
}
return $user;
}
/**
* Return existing social profile from database for specified external social profile.
*/
public function getExistingProfile(mixed $profile): ?SocialProfile
{
if (!$profile) {
return null;
}
return SocialProfile::where(
'user_service_id',
$this->getUsersIdentifierOnService($profile),
)
->with('user')
->first();
}
/**
* Create a new user from given social profile and log him in.
*/
public function createUserFromOAuthData(array $data)
{
$profile = $data['profile'];
$service = $data['service'];
$user = User::where('email', $profile->email)->first();
//create a new user if one does not exist with specified email
if (!$user) {
$img = str_replace('http://', 'https://', $profile->avatar);
$user = (new CreateUser())->execute([
'email' => $profile->email,
'avatar' => $img,
'email_verified_at' => now(),
]);
}
//save this social profile data, so we can log in the user easily next time
$user
->social_profiles()
->create(
$this->transformSocialProfileData(
$service,
$profile,
$user->id,
),
);
return $this->logUserIn($user, $service);
}
public function updateSocialProfileData(
SocialProfile $profile,
string $service,
$data,
User|null $user = null,
): void {
$data = $this->transformSocialProfileData(
$service,
$data,
$user->id ?? $profile->user_id,
);
$profile->fill($data)->save();
}
public function attachProfileToExistingUser(
User $user,
mixed $profile,
string $service,
) {
$payload = $this->transformSocialProfileData(
$service,
$profile,
$user->id,
);
//if this social account is already attached to some user
//we will re-attach it to specified user
if ($existing = $this->getExistingProfile($profile)) {
$this->updateSocialProfileData(
$existing,
$service,
$profile,
$user,
);
//if social account is not attached to any user, we will
//create a model for it and attach it to specified user
} else {
$user->social_profiles()->create($payload);
}
$response = [
'bootstrapData' => app(BootstrapData::class)
->init()
->getEncoded(),
];
event(new SocialConnected($user, $service));
return request()->expectsJson()
? $response
: $this->getPopupResponse('SUCCESS', $response);
}
private function transformSocialProfileData(
string $service,
TwoUser|OneUser $data,
int $userId,
): array {
return [
'service_name' => $service,
'user_service_id' => $this->getUsersIdentifierOnService($data),
'user_id' => $userId,
'username' => $data->name,
'access_token' => $data->token ?? null,
'refresh_token' => $data->refreshToken ?? null,
'access_expires_at' =>
isset($data->expiresIn) && $data->expiresIn
? Carbon::now()->addSeconds($data->expiresIn)
: null,
];
}
public function returnProfileData($externalProfile)
{
$normalizedProfile = [
'id' => $externalProfile->id,
'name' => $externalProfile->name,
'email' => $externalProfile->email,
'avatar' => $externalProfile->avatar,
'profileUrl' => $externalProfile->profileUrl,
];
if (request()->expectsJson()) {
return ['profile' => $normalizedProfile];
} else {
return $this->getPopupResponse('SUCCESS_PROFILE_RETRIEVE', [
'profile' => $normalizedProfile,
]);
}
}
/**
* Log given user into the app and return
* a view to close popup in front end.
*/
public function logUserIn(User $user, string $serviceName)
{
Auth::loginUsingId($user->id, true);
if (request('tokenForDevice')) {
$response = app(MobileBootstrapData::class)
->init()
->refreshToken(request('tokenForDevice'))
->get();
} else {
$response = [
'bootstrapData' => app(BootstrapData::class)
->init()
->getEncoded(),
];
}
event(new SocialLogin($user, $serviceName));
if (request()->expectsJson()) {
return $response;
} else {
return $this->getPopupResponse('SUCCESS', $response);
}
}
public function getErrorResponse(string $message)
{
if (request()->wantsJson()) {
return response()->json(['errorMessage' => $message], 500);
} else {
return $this->getPopupResponse('ERROR', [
'errorMessage' => $message,
]);
}
}
/**
* Get oauth data persisted in current session.
*
* @param string $key
* @return mixed
*/
public function getPersistedData($key = null)
{
//test session when not logged, what if multiple users log in at same time etc
$data = Session::get('social_profile');
if (!$key) {
return $data;
}
if ($key && isset($data[$key])) {
return $data[$key];
}
}
/**
* Store specified social profile information in the session
* for use in subsequent social login process steps.
*/
public function persistSocialProfileData(array $data): void
{
foreach ($data as $key => $value) {
Session::put("social_profile.$key", $value);
}
}
/**
* Check if provider user want to login with is valid, if not throw 404
*/
private function validateProvider(string $provider): void
{
if (!in_array($provider, $this->validProviders)) {
abort(404);
}
}
/**
* Get users unique identifier on social service from given profile.
*/
private function getUsersIdentifierOnService(mixed $profile): int|string
{
return $profile->id ?? $profile->email;
}
public function getPopupResponse(string $status, $data = null): ViewContract
{
$view = View::make('common::oauth/popup')->with('status', $status);
if ($data) {
$view->with('data', json_encode($data));
}
return $view;
}
}