Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Disable Signups for new users #7254

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ APP_DEBUG=true
# The URL of your application.
APP_URL=http://localhost:8000

# Ability to disable signups on your instance.
# Can be true or false. Default to false.
APP_DISABLE_SIGNUP=false

# Database to store information
# The documentation is here: https://laravel.com/docs/10.x/database
# You can also see the different values you can use in config/database.php
Expand Down
31 changes: 31 additions & 0 deletions app/Helpers/SignupHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace App\Helpers;

use App\Models\Account;
use Illuminate\Contracts\Config\Repository as Config;

class SignupHelper
{
public function __construct(
protected Config $config
) {
}

public function isEnabled(): bool
{
return ! ($this->isDisabledByConfig() && $this->hasAtLeastOneAccount());
}

protected function isDisabledByConfig(): bool
{
return (bool) $this->config->get('monica.disable_signup');
}

protected function hasAtLeastOneAccount(): bool
{
return ! empty(Account::first());
}
}
7 changes: 7 additions & 0 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers\Auth;

use App\Helpers\SignupHelper;
use App\Helpers\WallpaperHelper;
use App\Http\Controllers\Controller;
use App\Models\User;
Expand All @@ -14,6 +15,11 @@

class LoginController extends Controller
{
public function __construct(
protected SignupHelper $signupHelper
) {
}

Comment on lines +18 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't working actually

Suggested change
public function __construct(
protected SignupHelper $signupHelper
) {
}

/**
* Display the login view.
*/
Expand All @@ -40,6 +46,7 @@ public function __invoke(Request $request): Response
}

return Inertia::render('Auth/Login', $data + [
'isSignupEnabled' => $this->signupHelper->isEnabled(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should fix it

Suggested change
'isSignupEnabled' => $this->signupHelper->isEnabled(),
'isSignupEnabled' => app(SignupHelper::class)->isEnabled(),

'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
'wallpaperUrl' => WallpaperHelper::getRandomWallpaper(),
Expand Down
25 changes: 25 additions & 0 deletions app/Http/Middleware/EnsureSignupIsEnabled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use App\Helpers\SignupHelper;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureSignupIsEnabled
{
public function __construct(
protected SignupHelper $signupHelper,
) {
}

public function handle(Request $request, Closure $next): Response
{
abort_if(! $this->signupHelper->isEnabled(), 403, trans('Registration is currently disabled'));
Comment on lines +14 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function __construct(
protected SignupHelper $signupHelper,
) {
}
public function handle(Request $request, Closure $next): Response
{
abort_if(! $this->signupHelper->isEnabled(), 403, trans('Registration is currently disabled'));
public function handle(Request $request, Closure $next): Response
{
abort_if(! app(SignupHelper::class)->isEnabled(), 403, trans('Registration is currently disabled'));


return $next($request);
}
}
19 changes: 19 additions & 0 deletions app/Providers/FortifyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Http\Controllers\RegisteredUserController;

class FortifyServiceProvider extends ServiceProvider
{
Expand All @@ -34,6 +36,8 @@ public function register()
*/
public function boot()
{
$this->patchRoutes();

Fortify::loginView(fn ($request) => (new LoginController)($request));
Fortify::confirmPasswordsUsing(fn ($user, ?string $password = null) => $user->password
? app(StatefulGuard::class)->validate([
Expand All @@ -57,4 +61,19 @@ public function boot()

RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by($request->session()->get('login.id')));
}

protected function patchRoutes(): void
{
if ($this->app->routesAreCached()) {
return;
}

$router = $this->app->make(Router::class);
$routes = $router->getRoutes();
collect(['create', 'store'])->each(function ($method) use ($routes) {
if ($route = $routes->getByAction(RegisteredUserController::class.'@'.$method)) {
$route->middleware('monica.signup_is_enabled');
}
});
}
}
2 changes: 2 additions & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Http\Middleware\EnsureSignupIsEnabled;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
Expand All @@ -26,6 +27,7 @@
'abilities' => CheckAbilities::class,
'ability' => CheckForAnyAbility::class,
'webauthn' => WebauthnMiddleware::class,
'monica.signup_is_enabled' => EnsureSignupIsEnabled::class,
]);
$middleware->web(remove: [
\Illuminate\Routing\Middleware\SubstituteBindings::class,
Expand Down
10 changes: 10 additions & 0 deletions config/monica.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@

'app_version' => readVersion(__DIR__.'/.version', 'git describe --abbrev=0 --tags', '0.0.0'),

/*
|--------------------------------------------------------------------------
| Disable User registration
|--------------------------------------------------------------------------
|
| Disables registration of new users
|
*/
'disable_signup' => env('APP_DISABLE_SIGNUP', false),

/*
|--------------------------------------------------------------------------
| Commit hash of the application
Expand Down
3 changes: 2 additions & 1 deletion resources/js/Pages/Auth/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import WebauthnLogin from '@/Pages/Webauthn/WebauthnLogin.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';

const props = defineProps({
isSignupEnabled: Boolean,
canResetPassword: Boolean,
status: String,
wallpaperUrl: String,
Expand Down Expand Up @@ -195,7 +196,7 @@ const reload = () => {
</form>
</div>

<div class="px-6 py-6 text-sm dark:text-gray-50">
<div v-if="isSignupEnabled" class="px-6 py-6 text-sm dark:text-gray-50">
{{ $t('New to Monica?') }}
<Link :href="route('register')" class="text-blue-500 hover:underline">
{{ $t('Create an account') }}
Expand Down
51 changes: 36 additions & 15 deletions tests/Feature/Auth/RegistrationTest.php
Original file line number Diff line number Diff line change
@@ -1,47 +1,68 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Auth;

use App\Helpers\SignupHelper;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\Fortify\Features;
use Illuminate\Http\Response;
use Laravel\Jetstream\Jetstream;
use PHPUnit\Framework\Attributes\Test;
use Mockery;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use Mockery;
use PHPUnit\Framework\Attributes\Test;
use Mockery;

use Tests\TestCase;

class RegistrationTest extends TestCase
{
use DatabaseTransactions;

#[Test]
public function registration_screen_can_be_rendered()
public function testAccessToRegistrationPage(): void
davpsh marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the new syntax

Suggested change
public function testAccessToRegistrationPage(): void
#[Test]
public function registration_screen_can_be_rendered(): void

{
$this->withoutVite();

if (! Features::enabled(Features::registration())) {
return $this->markTestSkipped('Registration support is not enabled.');
}
$isSignupEnabled = null;
$this->app->bind(SignupHelper::class, function () use (&$isSignupEnabled) {
$mock = Mockery::mock(SignupHelper::class)->makePartial();
$mock->shouldReceive('isEnabled')->andReturn($isSignupEnabled);

return $mock;
});

$isSignupEnabled = true;
$response = $this->get('/register');
$response->assertStatus(Response::HTTP_OK);

$response->assertStatus(200);
$isSignupEnabled = false;
$response = $this->get('/register');
$response->assertStatus(Response::HTTP_FORBIDDEN);
$response->assertSeeText('Registration is currently disabled');
}

#[Test]
public function new_users_can_register()
public function testRegistration(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function testRegistration(): void
#[Test]
public function new_users_can_register()

{
if (! Features::enabled(Features::registration())) {
return $this->markTestSkipped('Registration support is not enabled.');
}
$isSignupEnabled = null;
$this->app->bind(SignupHelper::class, function () use (&$isSignupEnabled) {
$mock = Mockery::mock(SignupHelper::class)->makePartial();
$mock->shouldReceive('isEnabled')->andReturn($isSignupEnabled);

$response = $this->post('/register', [
return $mock;
});

$data = [
'first_name' => 'Test',
'last_name' => 'User',
'email' => '[email protected]',
'password' => 'Password$123',
'password_confirmation' => 'Password$123',
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
]);
];

$isSignupEnabled = false;
$response = $this->post('/register', $data);
$response->assertStatus(Response::HTTP_FORBIDDEN);
$response->assertSeeText('Registration is currently disabled');

$isSignupEnabled = true;
$response = $this->post('/register', $data);
$this->assertAuthenticated();
$response->assertRedirect('/vaults');
}
Expand Down
50 changes: 50 additions & 0 deletions tests/Unit/Helpers/SignupHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Tests\Unit\Helpers;

use App\Helpers\SignupHelper;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;

#[CoversClass(SignupHelper::class)]
class SignupHelperTest extends TestCase
{
#[DataProvider('isEnabledDataProvider')]
public function testIsEnabled(bool $isSignupDisabled, bool $hasAtLeastOneAccount, bool $expectedResult): void
{
$helper = Mockery::mock(SignupHelper::class)->shouldAllowMockingProtectedMethods()->makePartial();
$helper->shouldReceive('isDisabledByConfig')->andReturn($isSignupDisabled);
$helper->shouldReceive('hasAtLeastOneAccount')->andReturn($hasAtLeastOneAccount);

$this->assertEquals($expectedResult, $helper->isEnabled());
}

public function isEnabledDataProvider(): iterable
{
// $isSignupDisabled, $hasAtLeastOneAccount, $expectedResult
return [
[true, true, false],
[true, false, true],
[false, true, true],
[false, false, true],
];
}

public function testIsDisabledByConfig(): void
{
$configRepository = Mockery::mock(ConfigRepository::class)->makePartial();
$configRepository->shouldReceive('get')
->once()
->withArgs(function ($name) {
return $name === 'monica.disable_signup';
})
->andReturnTrue();

$helper = Mockery::mock(SignupHelper::class, [$configRepository])->makePartial();
$helper->isDisabledByConfig();
}
}