diff --git a/app/Http/Middleware/Range.php b/app/Http/Middleware/Range.php index 3b75b080d1..11e85836f2 100644 --- a/app/Http/Middleware/Range.php +++ b/app/Http/Middleware/Range.php @@ -89,6 +89,9 @@ class Range View::share('listLength', $pref); } + /** + * + */ private function configureView() { $pref = Preferences::get('language', config('firefly.default_language', 'en_US')); @@ -103,7 +106,7 @@ class Range // send error to view if could not set money format if (false === $moneyResult) { - View::share('invalidMonetaryLocale', true); + View::share('invalidMonetaryLocale', true); // @codeCoverageIgnore } // save some formats: diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index ad5d2a13db..e1b9676e9d 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -43,7 +43,7 @@ class RedirectIfAuthenticated public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->check()) { - return redirect('/home'); + return redirect(route('index')); } return $next($request); diff --git a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php index 94819959b3..1827455932 100644 --- a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php +++ b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php @@ -44,13 +44,14 @@ class RedirectIfTwoFactorAuthenticated { if (Auth::guard($guard)->check()) { $is2faEnabled = Preferences::get('twoFactorAuthEnabled', false)->data; + $has2faSecret = null !== Preferences::get('twoFactorAuthSecret'); // grab 2auth information from cookie. $is2faAuthed = 'true' === $request->cookie('twoFactorAuthenticated'); if ($is2faEnabled && $has2faSecret && $is2faAuthed) { - return redirect('/'); + return redirect(route('index')); } } diff --git a/app/Http/Middleware/Sandstorm.php b/app/Http/Middleware/Sandstorm.php index 00236f44a4..c119efcc97 100644 --- a/app/Http/Middleware/Sandstorm.php +++ b/app/Http/Middleware/Sandstorm.php @@ -25,10 +25,10 @@ namespace FireflyIII\Http\Middleware; use Auth; use Closure; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Models\Role; use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Http\Request; +use Log; use View; /** @@ -63,7 +63,8 @@ class Sandstorm /** @var UserRepositoryInterface $repository */ $repository = app(UserRepositoryInterface::class); $userId = strval($request->header('X-Sandstorm-User-Id')); - $count = $repository->count(); + Log::debug(sprintf('Sandstorm user ID is "%s"', $userId)); + $count = $repository->count(); // if there already is one user in this instance, we assume this is // the "main" user. Firefly's nature does not allow other users to @@ -72,7 +73,7 @@ class Sandstorm // and any other differences there may be between these users. if (1 === $count && strlen($userId) > 0) { // login as first user user. - $user = User::first(); + $user = $repository->first(); Auth::guard($guard)->login($user); View::share('SANDSTORM_ANON', false); @@ -92,7 +93,7 @@ class Sandstorm // create new user. $email = $userId . '@firefly'; /** @var User $user */ - $user = User::create( + $user = $repository->store( [ 'email' => $email, 'password' => str_random(16), @@ -101,9 +102,10 @@ class Sandstorm Auth::guard($guard)->login($user); // also make the user an admin - $admin = Role::where('name', 'owner')->first(); - $user->attachRole($admin); - $user->save(); + $repository->attachRole($user, 'owner'); + + // share value. + View::share('SANDSTORM_ANON', false); return $next($request); } @@ -116,6 +118,14 @@ class Sandstorm throw new FireflyException('Your Firefly III installation has more than one user, which is weird.'); } } + // if in Sandstorm, user logged in, still must check if user is anon. + $userId = strval($request->header('X-Sandstorm-User-Id')); + if (strlen($userId) === 0) { + View::share('SANDSTORM_ANON', true); + + return $next($request); + } + View::share('SANDSTORM_ANON', false); return $next($request); } diff --git a/app/Repositories/User/UserRepository.php b/app/Repositories/User/UserRepository.php index 200649bc44..5f7c793f15 100644 --- a/app/Repositories/User/UserRepository.php +++ b/app/Repositories/User/UserRepository.php @@ -168,6 +168,16 @@ class UserRepository implements UserRepositoryInterface return User::where('email', $email)->first(); } + /** + * Returns the first user in the DB. Generally only works when there is just one. + * + * @return null|User + */ + public function first(): ?User + { + return User::first(); + } + /** * Return basic user information. * @@ -225,6 +235,23 @@ class UserRepository implements UserRepositoryInterface return $user->hasRole($role); } + /** + * @param array $data + * + * @return User + */ + public function store(array $data): User + { + $password = bcrypt($data['password'] ?? app('str')->random(16)); + + return User::create( + [ + 'email' => $data['email'], + 'password' => $password, + ] + ); + } + /** * @param User $user */ diff --git a/app/Repositories/User/UserRepositoryInterface.php b/app/Repositories/User/UserRepositoryInterface.php index 802d898b97..1588696677 100644 --- a/app/Repositories/User/UserRepositoryInterface.php +++ b/app/Repositories/User/UserRepositoryInterface.php @@ -37,6 +37,20 @@ interface UserRepositoryInterface */ public function all(): Collection; + /** + * Returns the first user in the DB. Generally only works when there is just one. + * + * @return null|User + */ + public function first(): ?User; + + /** + * @param array $data + * + * @return User + */ + public function store(array $data): User; + /** * Gives a user a role. * diff --git a/resources/views/test/test.twig b/resources/views/test/test.twig new file mode 100644 index 0000000000..d9563d62ce --- /dev/null +++ b/resources/views/test/test.twig @@ -0,0 +1,2 @@ +list-length: {{ listLength }} +sandstorm-anon: {% if SANDSTORM_ANON %}true{% else %}false{% endif %} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 4b08982156..86b8f2ade6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -88,7 +88,17 @@ abstract class TestCase extends BaseTestCase /** * @return User */ - public function emptyUser() + public function demoUser(): User + { + $user = User::find(4); + + return $user; + } + + /** + * @return User + */ + public function emptyUser(): User { $user = User::find(2); @@ -98,7 +108,7 @@ abstract class TestCase extends BaseTestCase /** * @return User */ - public function user() + public function user(): User { $user = User::find(1); diff --git a/tests/Unit/Middleware/IsAdminTest.php b/tests/Unit/Middleware/IsAdminTest.php new file mode 100644 index 0000000000..3d65c0481a --- /dev/null +++ b/tests/Unit/Middleware/IsAdminTest.php @@ -0,0 +1,94 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Helpers; + +use FireflyIII\Http\Middleware\IsAdmin; +use Route; +use Symfony\Component\HttpFoundation\Response; +use Tests\TestCase; + +/** + * Class IsAdminTest + */ +class IsAdminTest extends TestCase +{ + /** + * @covers \FireflyIII\Http\Middleware\IsAdmin::handle + */ + public function testMiddleware() + { + $this->withoutExceptionHandling(); + $response = $this->get('/_test/is-admin'); + $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); + $response->assertRedirect(route('login')); + } + + /** + * @covers \FireflyIII\Http\Middleware\IsAdmin::handle + */ + public function testMiddlewareAjax() + { + $server = ['HTTP_X-Requested-With' => 'XMLHttpRequest']; + $this->withoutExceptionHandling(); + $response = $this->get('/_test/is-admin', $server); + $this->assertEquals(Response::HTTP_UNAUTHORIZED, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\IsAdmin::handle + */ + public function testMiddlewareOwner() + { + $this->be($this->user()); + $this->withoutExceptionHandling(); + $response = $this->get('/_test/is-admin'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\IsAdmin::handle + */ + public function testMiddlewareNotOwner() + { + $this->withoutExceptionHandling(); + $this->be($this->emptyUser()); + $response = $this->get('/_test/is-admin'); + $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); + $response->assertRedirect(route('home')); + } + + /** + * Set up test + */ + protected function setUp() + { + parent::setUp(); + + Route::middleware(IsAdmin::class)->any( + '/_test/is-admin', function () { + return 'OK'; + } + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Middleware/IsDemoUserTest.php b/tests/Unit/Middleware/IsDemoUserTest.php new file mode 100644 index 0000000000..839904ff23 --- /dev/null +++ b/tests/Unit/Middleware/IsDemoUserTest.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Helpers; + +use FireflyIII\Http\Middleware\IsDemoUser; +use Route; +use Symfony\Component\HttpFoundation\Response; +use Tests\TestCase; + +/** + * Class IsDemoUserTest + */ +class IsDemoUserTest extends TestCase +{ + /** + * @covers \FireflyIII\Http\Middleware\IsDemoUser::handle + */ + public function testMiddlewareNotAuthenticated() + { + $this->withoutExceptionHandling(); + $response = $this->get('/_test/is-demo'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\IsDemoUser::handle + */ + public function testMiddlewareAuthenticated() + { + $this->withoutExceptionHandling(); + $this->be($this->user()); + $response = $this->get('/_test/is-demo'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\IsDemoUser::handle + */ + public function testMiddlewareDemoUser() + { + $this->withoutExceptionHandling(); + $this->be($this->demoUser()); + $response = $this->get('/_test/is-demo'); + + $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); + $response->assertSessionHas('warning', strval(trans('firefly.not_available_demo_user'))); + $response->assertRedirect(route('index')); + } + + /** + * Set up test + */ + protected function setUp() + { + parent::setUp(); + + Route::middleware(IsDemoUser::class)->any( + '/_test/is-demo', function () { + return 'OK'; + } + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Middleware/IsSandstormUserTest.php b/tests/Unit/Middleware/IsSandstormUserTest.php new file mode 100644 index 0000000000..c22bf2795b --- /dev/null +++ b/tests/Unit/Middleware/IsSandstormUserTest.php @@ -0,0 +1,87 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Helpers; + +use FireflyIII\Http\Middleware\IsDemoUser; +use FireflyIII\Http\Middleware\IsSandStormUser; +use Route; +use Symfony\Component\HttpFoundation\Response; +use Tests\TestCase; + +/** + * Class IsSandstormUserTest + */ +class IsSandstormUserTest extends TestCase +{ + /** + * @covers \FireflyIII\Http\Middleware\IsSandStormUser::handle + */ + public function testMiddlewareNotAuthenticated() + { + $this->withoutExceptionHandling(); + $response = $this->get('/_test/is-sandstorm'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\IsSandStormUser::handle + */ + public function testMiddlewareNotSandStorm() + { + $this->withoutExceptionHandling(); + $this->be($this->user()); + $response = $this->get('/_test/is-sandstorm'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\IsSandStormUser::handle + */ + public function testMiddlewareSandstorm() + { + putenv('SANDSTORM=1'); + $this->withoutExceptionHandling(); + $this->be($this->user()); + $response = $this->get('/_test/is-sandstorm'); + + $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); + $response->assertSessionHas('warning', strval(trans('firefly.sandstorm_not_available'))); + $response->assertRedirect(route('index')); + putenv('SANDSTORM=0'); + } + + /** + * Set up test + */ + protected function setUp() + { + parent::setUp(); + + Route::middleware(IsSandStormUser::class)->any( + '/_test/is-sandstorm', function () { + return 'OK'; + } + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Middleware/RangeTest.php b/tests/Unit/Middleware/RangeTest.php new file mode 100644 index 0000000000..c064524551 --- /dev/null +++ b/tests/Unit/Middleware/RangeTest.php @@ -0,0 +1,80 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Helpers; + +use FireflyIII\Http\Middleware\Range; +use Route; +use Symfony\Component\HttpFoundation\Response; +use Tests\TestCase; + +/** + * Class RangeTest + */ +class RangeTest extends TestCase +{ + /** + * @covers \FireflyIII\Http\Middleware\Range::handle + * @covers \FireflyIII\Http\Middleware\Range::__construct + * @covers \FireflyIII\Http\Middleware\Range::configureList + * @covers \FireflyIII\Http\Middleware\Range::configureView + * @covers \FireflyIII\Http\Middleware\Range::setRange + */ + public function testMiddlewareAuthenticated() + { + $this->withoutExceptionHandling(); + $this->be($this->user()); + $response = $this->get('/_test/range'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + + // view has list length + $response->assertSeeText('list-length: 10'); + + // assert some session stuff? + } + + /** + * @covers \FireflyIII\Http\Middleware\Range::handle + * @covers \FireflyIII\Http\Middleware\Range::__construct + */ + public function testMiddlewareNotAuthenticated() + { + $this->withoutExceptionHandling(); + $response = $this->get('/_test/range'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * Set up test + */ + protected function setUp() + { + parent::setUp(); + + Route::middleware(Range::class)->any( + '/_test/range', function () { + return view('test.test'); + } + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Middleware/RedirectIf2FAAuthenticatedTest.php b/tests/Unit/Middleware/RedirectIf2FAAuthenticatedTest.php new file mode 100644 index 0000000000..d7c0fdc05c --- /dev/null +++ b/tests/Unit/Middleware/RedirectIf2FAAuthenticatedTest.php @@ -0,0 +1,94 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Helpers; + +use FireflyIII\Http\Middleware\RedirectIfTwoFactorAuthenticated; +use FireflyIII\Models\Preference; +use Preferences; +use Route; +use Symfony\Component\HttpFoundation\Response; +use Tests\TestCase; + +/** + * Class RedirectIf2FAAuthenticatedTest + */ +class RedirectIf2FAAuthenticatedTest extends TestCase +{ + /** + * @covers \FireflyIII\Http\Middleware\RedirectIfTwoFactorAuthenticated::handle + */ + public function testMiddleware() + { + $response = $this->get('/_test/authenticate'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\RedirectIfTwoFactorAuthenticated::handle + */ + public function testMiddlewareAuthenticated() + { + // pref for has 2fa is true + $preference = new Preference; + $preference->data = true; + Preferences::shouldReceive('get')->withArgs(['twoFactorAuthEnabled', false])->once()->andReturn($preference); + + // pref for twoFactorAuthSecret + $secret = new Preference; + $secret->data = 'SomeSecret'; + Preferences::shouldReceive('get')->withArgs(['twoFactorAuthSecret'])->once()->andReturn($secret); + + // no cookie + $cookie = ['twoFactorAuthenticated' => 'true']; + + $this->be($this->user()); + $response = $this->call('GET', '/_test/authenticate', [], $cookie); + $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); + $response->assertRedirect(route('index')); + } + + /** + * @covers \FireflyIII\Http\Middleware\RedirectIfTwoFactorAuthenticated::handle + */ + public function testMiddlewareLightAuth() + { + $this->be($this->user()); + $response = $this->get('/_test/authenticate'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * Set up test + */ + protected function setUp() + { + parent::setUp(); + + Route::middleware(RedirectIfTwoFactorAuthenticated::class)->any( + '/_test/authenticate', function () { + return 'OK'; + } + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Middleware/RedirectIfAuthenticatedTest.php b/tests/Unit/Middleware/RedirectIfAuthenticatedTest.php new file mode 100644 index 0000000000..e6b7cd6684 --- /dev/null +++ b/tests/Unit/Middleware/RedirectIfAuthenticatedTest.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Helpers; + +use FireflyIII\Http\Middleware\RedirectIfAuthenticated; +use Route; +use Symfony\Component\HttpFoundation\Response; +use Tests\TestCase; + +/** + * Class RedirectIfAuthenticatedTest + */ +class RedirectIfAuthenticatedTest extends TestCase +{ + /** + * @covers \FireflyIII\Http\Middleware\RedirectIfAuthenticated::handle + */ + public function testMiddleware() + { + $response = $this->get('/_test/authenticate'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\RedirectIfAuthenticated::handle + */ + public function testMiddlewareAuthenticated() + { + $this->be($this->user()); + $response = $this->get('/_test/authenticate'); + $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); + $response->assertRedirect(route('index')); + } + + /** + * Set up test + */ + protected function setUp() + { + parent::setUp(); + + Route::middleware(RedirectIfAuthenticated::class)->any( + '/_test/authenticate', function () { + return 'OK'; + } + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Middleware/SandstormTest.php b/tests/Unit/Middleware/SandstormTest.php new file mode 100644 index 0000000000..878aaca991 --- /dev/null +++ b/tests/Unit/Middleware/SandstormTest.php @@ -0,0 +1,178 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Helpers; + +use FireflyIII\Http\Middleware\Sandstorm; +use FireflyIII\Repositories\User\UserRepositoryInterface; +use Route; +use Symfony\Component\HttpFoundation\Response; +use Tests\TestCase; + +/** + * Class RangeTest + */ +class SandstormTest extends TestCase +{ + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareAnonEmpty() + { + putenv('SANDSTORM=1'); + + $repository = $this->mock(UserRepositoryInterface::class); + $repository->shouldReceive('count')->once()->andReturn(0); + + $response = $this->get('/_test/sandstorm'); + $this->assertEquals(Response::HTTP_INTERNAL_SERVER_ERROR, $response->getStatusCode()); + $response->assertSee('The first visit to a new Firefly III administration cannot be by a guest user.'); + + putenv('SANDSTORM=0'); + } + + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareAnonUser() + { + putenv('SANDSTORM=1'); + + $repository = $this->mock(UserRepositoryInterface::class); + $repository->shouldReceive('count')->once()->andReturn(1); + + $response = $this->get('/_test/sandstorm'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $response->assertSee('sandstorm-anon: true'); + + putenv('SANDSTORM=0'); + } + + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareLoggedIn() + { + putenv('SANDSTORM=1'); + + $this->be($this->user()); + $response = $this->get('/_test/sandstorm', ['X-Sandstorm-User-Id' => 'abcd']); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $response->assertSee('sandstorm-anon: false'); + + putenv('SANDSTORM=0'); + } + + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareAnonLoggedIn() + { + putenv('SANDSTORM=1'); + + $this->be($this->user()); + $response = $this->get('/_test/sandstorm'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $response->assertSee('sandstorm-anon: true'); + + putenv('SANDSTORM=0'); + } + + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareMultiUser() + { + putenv('SANDSTORM=1'); + + $repository = $this->mock(UserRepositoryInterface::class); + $repository->shouldReceive('count')->once()->andReturn(2); + + $response = $this->get('/_test/sandstorm'); + $this->assertEquals(Response::HTTP_INTERNAL_SERVER_ERROR, $response->getStatusCode()); + $response->assertSee('Your Firefly III installation has more than one user, which is weird.'); + + putenv('SANDSTORM=0'); + } + + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareNoUser() + { + putenv('SANDSTORM=1'); + + $repository = $this->mock(UserRepositoryInterface::class); + $repository->shouldReceive('count')->once()->andReturn(0); + $repository->shouldReceive('store')->once()->andReturn($this->user()); + $repository->shouldReceive('attachRole')->once()->andReturn(true); + + $response = $this->get('/_test/sandstorm', ['X-Sandstorm-User-Id' => 'abcd']); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $response->assertSee('sandstorm-anon: false'); + + putenv('SANDSTORM=0'); + } + + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareNotSandstorm() + { + $this->withoutExceptionHandling(); + $response = $this->get('/_test/sandstorm'); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * @covers \FireflyIII\Http\Middleware\Sandstorm::handle + */ + public function testMiddlewareOneUser() + { + putenv('SANDSTORM=1'); + + $repository = $this->mock(UserRepositoryInterface::class); + $repository->shouldReceive('count')->once()->andReturn(1); + $repository->shouldReceive('first')->once()->andReturn($this->user()); + + $response = $this->get('/_test/sandstorm', ['X-Sandstorm-User-Id' => 'abcd']); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $response->assertSee('sandstorm-anon: false'); + + putenv('SANDSTORM=0'); + } + + /** + * Set up test + */ + protected function setUp() + { + parent::setUp(); + + Route::middleware(Sandstorm::class)->any( + '/_test/sandstorm', function () { + return view('test.test'); + } + ); + } +} \ No newline at end of file