diff --git a/app/Exceptions/SocialSignInException.php b/app/Exceptions/SocialSignInException.php new file mode 100644 index 000000000..774a2a3ba --- /dev/null +++ b/app/Exceptions/SocialSignInException.php @@ -0,0 +1,7 @@ +middleware('guest', ['except' => 'getLogout']); + $this->middleware('guest', ['only' => ['getLogin', 'postLogin']]); $this->socialAuthService = $socialAuthService; } @@ -95,7 +95,7 @@ class AuthController extends Controller */ public function getSocialLogin($socialDriver) { - return $this->socialAuthService->logIn($socialDriver); + return $this->socialAuthService->startLogIn($socialDriver); } /** @@ -103,13 +103,21 @@ class AuthController extends Controller * * @param $socialDriver * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * @throws UserNotFound + * @throws SocialSignInException */ public function socialCallback($socialDriver) { - $user = $this->socialAuthService->getUserFromCallback($socialDriver); - \Auth::login($user, true); - return redirect($this->redirectPath); + return $this->socialAuthService->handleCallback($socialDriver); + } + + /** + * Detach a social account from a user. + * @param $socialDriver + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function detachSocialAccount($socialDriver) + { + return $this->socialAuthService->detachSocialAccount($socialDriver); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 63c27fa0b..b04dedf16 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Oxbow\Http\Requests; +use Oxbow\Services\SocialAuthService; use Oxbow\User; class UserController extends Controller @@ -74,16 +75,19 @@ class UserController extends Controller /** * Show the form for editing the specified user. * - * @param int $id + * @param int $id + * @param SocialAuthService $socialAuthService * @return Response */ - public function edit($id) + public function edit($id, SocialAuthService $socialAuthService) { $this->checkPermissionOr('user-update', function () use ($id) { return $this->currentUser->id == $id; }); + $user = $this->user->findOrFail($id); - return view('users/edit', ['user' => $user]); + $activeSocialDrivers = $socialAuthService->getActiveDrivers(); + return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]); } /** @@ -107,13 +111,14 @@ class UserController extends Controller ]); $user = $this->user->findOrFail($id); - $user->fill($request->all()); + $user->fill($request->except('password')); if ($this->currentUser->can('user-update') && $request->has('role')) { $user->attachRoleId($request->get('role')); } if ($request->has('password') && $request->get('password') != '') { + //dd('cat'); $password = $request->get('password'); $user->password = Hash::make($password); } diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index f0c6f803f..69b16cd07 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -17,7 +17,7 @@ class RedirectIfAuthenticated /** * Create a new filter instance. * - * @param Guard $auth + * @param Guard $auth * @return void */ public function __construct(Guard $auth) @@ -28,14 +28,14 @@ class RedirectIfAuthenticated /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if ($this->auth->check()) { - return redirect('/home'); + return redirect('/'); } return $next($request); diff --git a/app/Http/routes.php b/app/Http/routes.php index 075f49851..b50cd6fab 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -78,13 +78,15 @@ Route::group(['middleware' => 'auth'], function () { }); +// Login using social authentication +Route::get('/login/service/{socialDriver}', 'Auth\AuthController@getSocialLogin'); +Route::get('/login/service/{socialDriver}/callback', 'Auth\AuthController@socialCallback'); +Route::get('/login/service/{socialDriver}/detach', 'Auth\AuthController@detachSocialAccount'); + // Login/Logout routes Route::get('/login', 'Auth\AuthController@getLogin'); Route::post('/login', 'Auth\AuthController@postLogin'); Route::get('/logout', 'Auth\AuthController@getLogout'); -// Login using social authentication -Route::get('/login/service/{socialService}', 'Auth\AuthController@getSocialLogin'); -Route::get('/login/service/{socialService}/callback', 'Auth\AuthController@socialCallback'); // Password reset link request routes... Route::get('/password/email', 'Auth\PasswordController@getEmail'); diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php index c8f3278c5..814f96af6 100644 --- a/app/Services/SocialAuthService.php +++ b/app/Services/SocialAuthService.php @@ -2,29 +2,40 @@ use Laravel\Socialite\Contracts\Factory as Socialite; use Oxbow\Exceptions\SocialDriverNotConfigured; -use Oxbow\Exceptions\UserNotFound; +use Oxbow\Exceptions\SocialSignInException; use Oxbow\Repos\UserRepo; +use Oxbow\SocialAccount; +use Oxbow\User; class SocialAuthService { protected $userRepo; protected $socialite; + protected $socialAccount; protected $validSocialDrivers = ['google', 'github']; /** * SocialAuthService constructor. - * @param $userRepo - * @param $socialite + * @param UserRepo $userRepo + * @param Socialite $socialite + * @param SocialAccount $socialAccount */ - public function __construct(UserRepo $userRepo, Socialite $socialite) + public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount) { $this->userRepo = $userRepo; $this->socialite = $socialite; + $this->socialAccount = $socialAccount; } - public function logIn($socialDriver) + /** + * Start the social login path. + * @param $socialDriver + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * @throws SocialDriverNotConfigured + */ + public function startLogIn($socialDriver) { $driver = $this->validateDriver($socialDriver); return $this->socialite->driver($driver)->redirect(); @@ -34,23 +45,78 @@ class SocialAuthService * Get a user from socialite after a oAuth callback. * * @param $socialDriver - * @return mixed + * @return User * @throws SocialDriverNotConfigured - * @throws UserNotFound + * @throws SocialSignInException */ - public function getUserFromCallback($socialDriver) + public function handleCallback($socialDriver) { $driver = $this->validateDriver($socialDriver); + // Get user details from social driver $socialUser = $this->socialite->driver($driver)->user(); - $user = $this->userRepo->getByEmail($socialUser->getEmail()); + $socialId = $socialUser->getId(); - // Redirect if the email is not a current user. - if ($user === null) { - throw new UserNotFound('A user with the email ' . $socialUser->getEmail() . ' was not found.', '/login'); + // Get any attached social accounts or users + $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first(); + $user = $this->userRepo->getByEmail($socialUser->getEmail()); + $isLoggedIn = \Auth::check(); + $currentUser = \Auth::user(); + + // When a user is not logged in but a matching SocialAccount exists, + // Log the user found on the SocialAccount into the application. + if (!$isLoggedIn && $socialAccount !== null) { + return $this->logUserIn($socialAccount->user); } - return $user; + // When a user is logged in but the social account does not exist, + // Create the social account and attach it to the user & redirect to the profile page. + if ($isLoggedIn && $socialAccount === null) { + $this->fillSocialAccount($socialDriver, $socialUser); + $currentUser->socialAccounts()->save($this->socialAccount); + \Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.'); + return redirect($currentUser->getEditUrl()); + } + + // When a user is logged in and the social account exists and is already linked to the current user. + if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) { + \Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.'); + return redirect($currentUser->getEditUrl()); + } + + // When a user is logged in, A social account exists but the users do not match. + // Change the user that the social account is assigned to. + if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { + $socialAccount->user_id = $currentUser->id; + $socialAccount->save(); + \Session::flash('success', 'This ' . title_case($socialDriver) . ' account is now attached to your profile.'); + } + + if ($user === null) { + throw new SocialSignInException('A system user with the email ' . $socialUser->getEmail() . + ' was not found and this ' . $socialDriver . ' account is not linked to any users.', '/login'); + } + return $this->authenticateUserWithNewSocialAccount($user, $socialUser, $socialUser); + } + + /** + * Logs a user in and creates a new social account entry for future usage. + * @param User $user + * @param string $socialDriver + * @param \Laravel\Socialite\Contracts\User $socialUser + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + private function authenticateUserWithNewSocialAccount($user, $socialDriver, $socialUser) + { + $this->fillSocialAccount($socialDriver, $socialUser); + $user->socialAccounts()->save($this->socialAccount); + return $this->logUserIn($user); + } + + private function logUserIn($user) + { + \Auth::login($user); + return redirect('/'); } /** @@ -65,7 +131,7 @@ class SocialAuthService $driver = trim(strtolower($socialDriver)); if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found'); - if (!$this->checklDriverConfigured($driver)) throw new SocialDriverNotConfigured; + if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured; return $driver; } @@ -75,7 +141,7 @@ class SocialAuthService * @param $driver * @return bool */ - private function checklDriverConfigured($driver) + private function checkDriverConfigured($driver) { $upperName = strtoupper($driver); $config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)]; @@ -90,12 +156,36 @@ class SocialAuthService { $activeDrivers = []; foreach ($this->validSocialDrivers as $driverName) { - if ($this->checklDriverConfigured($driverName)) { + if ($this->checkDriverConfigured($driverName)) { $activeDrivers[$driverName] = true; } } return $activeDrivers; } + /** + * @param $socialDriver + * @param $socialUser + */ + private function fillSocialAccount($socialDriver, $socialUser) + { + $this->socialAccount->fill([ + 'driver' => $socialDriver, + 'driver_id' => $socialUser->getId(), + 'avatar' => $socialUser->getAvatar() + ]); + } + + /** + * Detach a social account from a user. + * @param $socialDriver + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function detachSocialAccount($socialDriver) + { + \Auth::user()->socialAccounts()->where('driver', '=', $socialDriver)->delete(); + \Session::flash('success', $socialDriver . ' account successfully detached'); + return redirect(\Auth::user()->getEditUrl()); + } } \ No newline at end of file diff --git a/app/SocialAccount.php b/app/SocialAccount.php new file mode 100644 index 000000000..2e526ecc4 --- /dev/null +++ b/app/SocialAccount.php @@ -0,0 +1,16 @@ +belongsTo('Oxbow\User'); + } +} diff --git a/app/User.php b/app/User.php index cfc20453f..b2fa5cd1b 100644 --- a/app/User.php +++ b/app/User.php @@ -96,6 +96,31 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon $this->roles()->sync([$id]); } + /** + * Get the social account associated with this user. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function socialAccounts() + { + return $this->hasMany('Oxbow\SocialAccount'); + } + + /** + * Check if the user has a social account, + * If a driver is passed it checks for that single account type. + * @param bool|string $socialDriver + * @return bool + */ + public function hasSocialAccount($socialDriver = false) + { + if($socialDriver === false) { + return $this->socialAccounts()->count() > 0; + } + + return $this->socialAccounts()->where('driver', '=', $socialDriver)->exists(); + } + /** * Returns the user's avatar, * Uses Gravatar as the avatar service. @@ -108,4 +133,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon $emailHash = md5(strtolower(trim($this->email))); return '//www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; } + + public function getEditUrl() + { + return '/users/' . $this->id; + } } diff --git a/database/migrations/2015_09_04_165821_create_social_accounts_table.php b/database/migrations/2015_09_04_165821_create_social_accounts_table.php new file mode 100644 index 000000000..a4f627390 --- /dev/null +++ b/database/migrations/2015_09_04_165821_create_social_accounts_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->integer('user_id')->indexed(); + $table->string('driver')->indexed(); + $table->string('driver_id'); + $table->string('avatar'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('social_accounts'); + } +} diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index 80277b9a5..095a0e0dc 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -37,7 +37,7 @@ $primary: #0288D1; $primary-dark: #0288D1; $secondary: #e27b41; $positive: #52A256; -$negative: #D32F2F; +$negative: #E84F4F; // Item Colors $color-book: #009688; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 2c545f281..ee5164cd7 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -30,7 +30,7 @@ @endif @if(isset($socialDrivers['github'])) - + @endif @endif diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 4dc3251ca..968f9ea9a 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -55,6 +55,9 @@ @if($currentUser->can('settings-update')) Settings @endif + @if(!$signedIn) + Sign In + @endif @if($signedIn) diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 36dee5086..b981c64d8 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -42,22 +42,43 @@
User Role: {{$user->role->display_name}}.
-
+ Here you can connect your other accounts for quicker and easier login.
+ Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.
+