diff --git a/src/Flarum/Api/Actions/Auth/Login.php b/src/Flarum/Api/Actions/Auth/Login.php new file mode 100644 index 000000000..3473b670d --- /dev/null +++ b/src/Flarum/Api/Actions/Auth/Login.php @@ -0,0 +1,32 @@ +input('identifier'); + $password = $this->input('password'); + $field = filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; + $credentials = [$field => $identifier, 'password' => $password]; + + if (! Auth::attempt($credentials)) { + return $this->respondWithError('invalidLogin', 401); + } + + $token = Auth::user()->getRememberToken(); + + return Response::json(compact('token')); + } +} diff --git a/src/Flarum/Core/CoreServiceProvider.php b/src/Flarum/Core/CoreServiceProvider.php index fb8a0e64e..e7364524e 100644 --- a/src/Flarum/Core/CoreServiceProvider.php +++ b/src/Flarum/Core/CoreServiceProvider.php @@ -24,6 +24,8 @@ class CoreServiceProvider extends ServiceProvider $this->app->make('validator')->extend('username', 'Flarum\Core\Users\UsernameValidator@validate'); + $this->app['config']->set('auth.model', 'Flarum\Core\Users\User'); + Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\DiscussionMetadataUpdater'); Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\UserMetadataUpdater'); Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\PostFormatter'); diff --git a/src/Flarum/Core/Discussions/Discussion.php b/src/Flarum/Core/Discussions/Discussion.php index 8c17bd1c9..70ee378cb 100755 --- a/src/Flarum/Core/Discussions/Discussion.php +++ b/src/Flarum/Core/Discussions/Discussion.php @@ -44,7 +44,7 @@ class Discussion extends Entity // Allow a user to edit their own discussion. static::grant('edit', function ($grant, $user) { if (app('flarum.permissions')->granted($user, 'editOwn', 'discussion')) { - $grant->where('user_id', $user->id); + $grant->where('start_user_id', $user->id); } }); diff --git a/src/Flarum/Core/Users/User.php b/src/Flarum/Core/Users/User.php index a317670bd..37759a396 100755 --- a/src/Flarum/Core/Users/User.php +++ b/src/Flarum/Core/Users/User.php @@ -1,7 +1,9 @@ 'required|username|unique', @@ -206,34 +210,4 @@ class User extends Entity /*implements UserInterface, RemindableInterface*/ { return $this->hasMany('Flarum\Core\Activity\Activity'); } - - /** - * Get the unique identifier for the user. - * - * @return mixed - */ - public function getAuthIdentifier() - { - return $this->getKey(); - } - - /** - * Get the password for the user. - * - * @return string - */ - public function getAuthPassword() - { - return $this->password; - } - - /** - * Get the e-mail address where password reminders are sent. - * - * @return string - */ - public function getReminderEmail() - { - return $this->email; - } } diff --git a/src/migrations/2014_01_14_231404_create_users_table.php b/src/migrations/2014_01_14_231404_create_users_table.php index 8ce3cb25e..6a3bda13f 100644 --- a/src/migrations/2014_01_14_231404_create_users_table.php +++ b/src/migrations/2014_01_14_231404_create_users_table.php @@ -18,6 +18,7 @@ class CreateUsersTable extends Migration { $table->string('username'); $table->string('email'); $table->string('password'); + $table->rememberToken(); $table->dateTime('join_time'); $table->string('time_zone'); $table->dateTime('last_seen_time')->nullable(); diff --git a/src/routes.api.php b/src/routes.api.php index 3c57d4c5a..56983da8d 100644 --- a/src/routes.api.php +++ b/src/routes.api.php @@ -3,14 +3,35 @@ $action = function($class) { return function () use ($class) { - $action = \App::make($class); + $action = App::make($class); $request = app('request'); $parameters = Route::current()->parameters(); return $action->handle($request, $parameters); }; }; -Route::group(['prefix' => 'api'], function () use ($action) { +// @todo refactor into a unit-testable class +Route::filter('attemptLogin', function($route, $request) { + $prefix = 'Token '; + if (starts_with($request->headers->get('authorization'), $prefix)) { + $token = substr($request->headers->get('authorization'), strlen($prefix)); + Auth::once(['remember_token' => $token]); + } +}); + +Route::group(['prefix' => 'api', 'before' => 'attemptLogin'], function () use ($action) { + + /* + |-------------------------------------------------------------------------- + | Auth + |-------------------------------------------------------------------------- + */ + + // Login + Route::post('auth/login', [ + 'as' => 'flarum.api.auth.login', + 'uses' => $action('Flarum\Api\Actions\Auth\Login') + ]); /* |-------------------------------------------------------------------------- diff --git a/tests/_support/ApiHelper.php b/tests/_support/ApiHelper.php index 5fcdbe2c7..722db46ef 100644 --- a/tests/_support/ApiHelper.php +++ b/tests/_support/ApiHelper.php @@ -1,10 +1,35 @@ getModule('REST')->sendPOST('/api/auth/login', ['identifier' => $identifier, 'password' => $password]); + + $response = json_decode($this->getModule('REST')->grabResponse(), true); + if ($response && is_array($response) && isset($response['token'])) { + return $response['token']; + } + + return false; + } + + public function amAuthenticated() + { + $user = $this->haveAnAccount(); + $user->groups()->attach(3); // Add member group + Auth::onceUsingId($user->id); + + return $user; + } } diff --git a/tests/api/ApiTester.php b/tests/api/ApiTester.php index 214b13fb8..61058172a 100644 --- a/tests/api/ApiTester.php +++ b/tests/api/ApiTester.php @@ -1,4 +1,4 @@ -scenario->runStep(new \Codeception\Step\Action('fail', func_get_args())); } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\ApiHelper::haveAnAccount() + */ + public function haveAnAccount($data = null) { + return $this->scenario->runStep(new \Codeception\Step\Action('haveAnAccount', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\ApiHelper::login() + */ + public function login($identifier, $password) { + return $this->scenario->runStep(new \Codeception\Step\Action('login', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\ApiHelper::amAuthenticated() + */ + public function amAuthenticated() { + return $this->scenario->runStep(new \Codeception\Step\Condition('amAuthenticated', func_get_args())); + } } diff --git a/tests/api/AuthCest.php b/tests/api/AuthCest.php new file mode 100644 index 000000000..c9179239f --- /dev/null +++ b/tests/api/AuthCest.php @@ -0,0 +1,55 @@ +wantTo('login via API with email'); + + $user = $I->haveAnAccount([ + 'email' => 'foo@bar.com', + 'password' => 'pass7word' + ]); + + $token = $I->login('foo@bar.com', 'pass7word'); + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $loggedIn = User::where('remember_token', $token)->first(); + $I->assertEquals($user->id, $loggedIn->id); + } + + public function loginWithUsername(ApiTester $I) + { + $I->wantTo('login via API with username'); + + $user = $I->haveAnAccount([ + 'username' => 'tobscure', + 'password' => 'pass7word' + ]); + + $token = $I->login('tobscure', 'pass7word'); + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $loggedIn = User::where('remember_token', $token)->first(); + $I->assertEquals($user->id, $loggedIn->id); + } + + public function invalidLogin(ApiTester $I) + { + $user = $I->haveAnAccount([ + 'email' => 'foo@bar.com', + 'password' => 'pass7word' + ]); + + $I->login('foo@bar.com', 'incorrect'); + $I->seeResponseCodeIs(401); + $I->seeResponseIsJson(); + } +} \ No newline at end of file diff --git a/tests/api/DiscussionsResourceCest.php b/tests/api/DiscussionsResourceCest.php index 8961a6106..e0ca66cf1 100644 --- a/tests/api/DiscussionsResourceCest.php +++ b/tests/api/DiscussionsResourceCest.php @@ -42,7 +42,7 @@ class DiscussionsResourceCest { { $I->wantTo('create a discussion via API'); - $I->haveHttpHeader('Authorization', 'Token 123456'); + $I->amAuthenticated(); $I->sendPOST($this->endpoint, ['discussions' => ['title' => 'foo', 'content' => 'bar']]); $I->seeResponseCodeIs(200); @@ -58,9 +58,9 @@ class DiscussionsResourceCest { { $I->wantTo('update a discussion via API'); - $I->haveHttpHeader('Authorization', 'Token 123456'); + $user = $I->amAuthenticated(); - $discussion = Factory::create('Flarum\Core\Discussions\Discussion'); + $discussion = Factory::create('Flarum\Core\Discussions\Discussion', ['start_user_id' => $user->id]); $I->sendPUT($this->endpoint.'/'.$discussion->id, ['discussions' => ['title' => 'foo']]); $I->seeResponseCodeIs(200); @@ -75,9 +75,10 @@ class DiscussionsResourceCest { { $I->wantTo('delete a discussion via API'); - $I->haveHttpHeader('Authorization', 'Token 123456'); + $user = $I->amAuthenticated(); + $user->groups()->attach(4); - $discussion = Factory::create('Flarum\Core\Discussions\Discussion'); + $discussion = Factory::create('Flarum\Core\Discussions\Discussion', ['start_user_id' => $user->id]); $I->sendDELETE($this->endpoint.'/'.$discussion->id); $I->seeResponseCodeIs(204); diff --git a/tests/factories/factories.php b/tests/factories/factories.php index 66ad9ba04..9d9d37a40 100644 --- a/tests/factories/factories.php +++ b/tests/factories/factories.php @@ -1,9 +1,15 @@ $faker->sentence + 'title' => $faker->sentence, + 'start_time' => $faker->dateTimeThisYear, + 'start_user_id' => 'factory:Flarum\Core\Users\User' ]); $factory('Flarum\Core\Users\User', [ - 'username' => $faker->sentence + 'username' => $faker->userName, + 'email' => $faker->safeEmail, + 'password' => 'password', + 'join_time' => $faker->dateTimeThisYear, + 'time_zone' => $faker->timezone ]); \ No newline at end of file