-<?php
+<?php namespace BookStack\Http\Controllers\Auth;
-namespace Oxbow\Http\Controllers\Auth;
-
-use Oxbow\Exceptions\SocialDriverNotConfigured;
-use Oxbow\Exceptions\UserNotFound;
-use Oxbow\Repos\UserRepo;
-use Oxbow\User;
+use BookStack\Exceptions\AuthException;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Http\Request;
+use BookStack\Exceptions\SocialSignInException;
+use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Repos\UserRepo;
+use BookStack\Services\EmailConfirmationService;
+use BookStack\Services\SocialAuthService;
+use BookStack\SocialAccount;
use Validator;
-use Oxbow\Http\Controllers\Controller;
+use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
-use Laravel\Socialite\Contracts\Factory as Socialite;
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
- protected $loginPath = '/login';
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
+ protected $username = 'email';
- protected $validSocialDrivers = ['google', 'github'];
-
- protected $socialite;
+ protected $socialAuthService;
+ protected $emailConfirmationService;
protected $userRepo;
/**
* Create a new authentication controller instance.
- * @param Socialite $socialite
- * @param UserRepo $userRepo
+ * @param SocialAuthService $socialAuthService
+ * @param EmailConfirmationService $emailConfirmationService
+ * @param UserRepo $userRepo
*/
- public function __construct(Socialite $socialite, UserRepo $userRepo)
+ public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
- $this->middleware('guest', ['except' => 'getLogout']);
- $this->socialite = $socialite;
+ $this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]);
+ $this->socialAuthService = $socialAuthService;
+ $this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
+ $this->redirectPath = baseUrl('/');
+ $this->redirectAfterLogout = baseUrl('/login');
+ $this->username = config('auth.method') === 'standard' ? 'email' : 'username';
+ parent::__construct();
}
/**
* 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, [
- 'name' => 'required|max:255',
- 'email' => 'required|email|max:255|unique:users',
- 'password' => 'required|confirmed|min:6',
+ 'name' => 'required|max:255',
+ 'email' => 'required|email|max:255|unique:users',
+ 'password' => 'required|min:6',
]);
}
+ protected function checkRegistrationAllowed()
+ {
+ if (!setting('registration-enabled')) {
+ throw new UserRegistrationException('Registrations are currently disabled.', '/login');
+ }
+ }
+
/**
- * Create a new user instance after a valid registration.
- *
- * @param array $data
- * @return User
+ * Show the application registration form.
+ * @return \Illuminate\Http\Response
*/
- protected function create(array $data)
+ public function getRegister()
{
- return User::create([
- 'name' => $data['name'],
- 'email' => $data['email'],
- 'password' => bcrypt($data['password']),
- ]);
+ $this->checkRegistrationAllowed();
+ $socialDrivers = $this->socialAuthService->getActiveDrivers();
+ return view('auth.register', ['socialDrivers' => $socialDrivers]);
}
/**
- * Show the application login form.
- *
+ * Handle a registration request for the application.
+ * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
+ * @throws UserRegistrationException
*/
- public function getLogin()
+ public function postRegister(Request $request)
{
+ $this->checkRegistrationAllowed();
+ $validator = $this->validator($request->all());
- if (view()->exists('auth.authenticate')) {
- return view('auth.authenticate');
+ if ($validator->fails()) {
+ $this->throwValidationException(
+ $request, $validator
+ );
}
- $socialDrivers = $this->getActiveSocialDrivers();
+ $userData = $request->all();
+ return $this->registerUser($userData);
+ }
- return view('auth.login', ['socialDrivers' => $socialDrivers]);
+
+ /**
+ * Overrides the action when a user is authenticated.
+ * If the user authenticated but does not exist in the user table we create them.
+ * @param Request $request
+ * @param Authenticatable $user
+ * @return \Illuminate\Http\RedirectResponse
+ * @throws AuthException
+ */
+ protected function authenticated(Request $request, Authenticatable $user)
+ {
+ // Explicitly log them out for now if they do no exist.
+ if (!$user->exists) auth()->logout($user);
+
+ if (!$user->exists && $user->email === null && !$request->has('email')) {
+ $request->flash();
+ session()->flash('request-email', true);
+ return redirect('/login');
+ }
+
+ if (!$user->exists && $user->email === null && $request->has('email')) {
+ $user->email = $request->get('email');
+ }
+
+ if (!$user->exists) {
+
+ // Check for users with same email already
+ $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
+ if ($alreadyUser) {
+ throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
+ }
+
+ $user->save();
+ $this->userRepo->attachDefaultRole($user);
+ auth()->login($user);
+ }
+
+ return redirect()->intended($this->redirectPath());
}
/**
- * Redirect to the relevant social site.
+ * Register a new user after a registration callback.
* @param $socialDriver
- * @return \Symfony\Component\HttpFoundation\RedirectResponse
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws UserRegistrationException
*/
- public function getSocialLogin($socialDriver)
+ protected function socialRegisterCallback($socialDriver)
{
- $driver = $this->validateSocialDriver($socialDriver);
- return $this->socialite->driver($driver)->redirect();
+ $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
+ $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
+
+ // Create an array of the user data to create a new user instance
+ $userData = [
+ 'name' => $socialUser->getName(),
+ 'email' => $socialUser->getEmail(),
+ 'password' => str_random(30)
+ ];
+ return $this->registerUser($userData, $socialAccount);
}
/**
- * The callback for social login services.
- *
- * @param $socialDriver
+ * The registrations flow for all users.
+ * @param array $userData
+ * @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws UserNotFound
+ * @throws UserRegistrationException
+ * @throws \BookStack\Exceptions\ConfirmationEmailException
*/
- public function socialCallback($socialDriver)
+ protected function registerUser(array $userData, $socialAccount = false)
{
- $driver = $this->validateSocialDriver($socialDriver);
- // Get user details from social driver
- $socialUser = $this->socialite->driver($driver)->user();
- $user = $this->userRepo->getByEmail($socialUser->getEmail());
+ if (setting('registration-restrict')) {
+ $restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
+ $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
+ if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
+ throw new UserRegistrationException('That email domain does not have access to this application', '/register');
+ }
+ }
+
+ $newUser = $this->userRepo->registerNew($userData);
+ if ($socialAccount) {
+ $newUser->socialAccounts()->save($socialAccount);
+ }
- // 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');
+ if (setting('registration-confirmation') || setting('registration-restrict')) {
+ $newUser->save();
+ $this->emailConfirmationService->sendConfirmation($newUser);
+ return redirect('/register/confirm');
}
- \Auth::login($user, true);
+ auth()->login($newUser);
+ session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
+ return redirect($this->redirectPath());
+ }
+
+ /**
+ * Show the page to tell the user to check their email
+ * and confirm their address.
+ */
+ public function getRegisterConfirmation()
+ {
+ return view('auth/register-confirm');
+ }
+
+ /**
+ * View the confirmation email as a standard web page.
+ * @param $token
+ * @return \Illuminate\View\View
+ * @throws UserRegistrationException
+ */
+ public function viewConfirmEmail($token)
+ {
+ $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
+ return view('emails/email-confirmation', ['token' => $confirmation->token]);
+ }
+
+ /**
+ * Confirms an email via a token and logs the user into the system.
+ * @param $token
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws UserRegistrationException
+ */
+ public function confirmEmail($token)
+ {
+ $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
+ $user = $confirmation->user;
+ $user->email_confirmed = true;
+ $user->save();
+ auth()->login($confirmation->user);
+ session()->flash('success', 'Your email has been confirmed!');
+ $this->emailConfirmationService->deleteConfirmationsByUser($user);
return redirect($this->redirectPath);
}
/**
- * Ensure the social driver is correct and supported.
- *
- * @param $socialDriver
- * @return string
- * @throws SocialDriverNotConfigured
+ * Shows a notice that a user's email address has not been confirmed,
+ * Also has the option to re-send the confirmation email.
+ * @return \Illuminate\View\View
+ */
+ public function showAwaitingConfirmation()
+ {
+ return view('auth/user-unconfirmed');
+ }
+
+ /**
+ * Resend the confirmation email
+ * @param Request $request
+ * @return \Illuminate\View\View
*/
- protected function validateSocialDriver($socialDriver)
+ public function resendConfirmation(Request $request)
{
- $driver = trim(strtolower($socialDriver));
+ $this->validate($request, [
+ 'email' => 'required|email|exists:users,email'
+ ]);
+ $user = $this->userRepo->getByEmail($request->get('email'));
+ $this->emailConfirmationService->sendConfirmation($user);
+ session()->flash('success', 'Confirmation email resent, Please check your inbox.');
+ return redirect('/register/confirm');
+ }
- if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
- if(!$this->checkSocialDriverConfigured($driver)) throw new SocialDriverNotConfigured;
+ /**
+ * Show the application login form.
+ * @return \Illuminate\Http\Response
+ */
+ public function getLogin()
+ {
+ $socialDrivers = $this->socialAuthService->getActiveDrivers();
+ $authMethod = config('auth.method');
+ return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
+ }
- return $driver;
+ /**
+ * Redirect to the relevant social site.
+ * @param $socialDriver
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse
+ */
+ public function getSocialLogin($socialDriver)
+ {
+ session()->put('social-callback', 'login');
+ return $this->socialAuthService->startLogIn($socialDriver);
}
/**
- * Check a social driver has been configured correctly.
- * @param $driver
- * @return bool
+ * Redirect to the social site for authentication intended to register.
+ * @param $socialDriver
+ * @return mixed
*/
- protected function checkSocialDriverConfigured($driver)
+ public function socialRegister($socialDriver)
{
- $upperName = strtoupper($driver);
- $config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)];
- return (!in_array(false, $config) && !in_array(null, $config));
+ $this->checkRegistrationAllowed();
+ session()->put('social-callback', 'register');
+ return $this->socialAuthService->startRegister($socialDriver);
}
/**
- * Gets the names of the active social drivers.
- * @return array
+ * The callback for social login services.
+ * @param $socialDriver
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws SocialSignInException
*/
- protected function getActiveSocialDrivers()
+ public function socialCallback($socialDriver)
{
- $activeDrivers = [];
- foreach($this->validSocialDrivers as $driverName) {
- if($this->checkSocialDriverConfigured($driverName)) {
- $activeDrivers[$driverName] = true;
+ if (session()->has('social-callback')) {
+ $action = session()->pull('social-callback');
+ if ($action == 'login') {
+ return $this->socialAuthService->handleLoginCallback($socialDriver);
+ } elseif ($action == 'register') {
+ return $this->socialRegisterCallback($socialDriver);
}
+ } else {
+ throw new SocialSignInException('No action defined', '/login');
}
- return $activeDrivers;
+ return redirect()->back();
}
+
+ /**
+ * 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);
+ }
+
}