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 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
33 changes: 33 additions & 0 deletions app/Helpers/SignupHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Helpers;

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

class SignupHelper
{
protected ConfigRepository $configRepository;

public function __construct(ConfigRepository $configRepository)
{
$this->configRepository = $configRepository;
}

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

protected function isDisabledByConfig(): bool
{
return (bool) $this->configRepository->get('monica.disable_signup');
}
davpsh marked this conversation as resolved.
Show resolved Hide resolved

protected function hasAtLeastOneAccount(): bool
{
return !empty(Account::first());
}
}
9 changes: 9 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,13 @@

class LoginController extends Controller
{
protected SignupHelper $signupHelper;

public function __construct(SignupHelper $signupHelper)
{
$this->signupHelper = $signupHelper;
}
davpsh marked this conversation as resolved.
Show resolved Hide resolved

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

return Inertia::render('Auth/Login', $data + [
'isSignupEnabled' => $this->signupHelper->isEnabled(),
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
'wallpaperUrl' => WallpaperHelper::getRandomWallpaper(),
Expand Down
32 changes: 32 additions & 0 deletions app/Http/Middleware/EnsureSignupIsEnabled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace App\Http\Middleware;

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

class EnsureSignupIsEnabled
{
protected SignupHelper $signupHelper;
protected Translator $translator;

public function __construct(SignupHelper $signupHelper, Translator $translator)
{
$this->signupHelper = $signupHelper;
$this->translator = $translator;
}
davpsh marked this conversation as resolved.
Show resolved Hide resolved

public function handle(Request $request, Closure $next): Response
{
if (!$this->signupHelper->isEnabled()) {
abort(Response::HTTP_FORBIDDEN, $this->translator->get('auth.signup_disabled'));
davpsh marked this conversation as resolved.
Show resolved Hide resolved
}
davpsh marked this conversation as resolved.
Show resolved Hide resolved

return $next($request);
}
}
23 changes: 22 additions & 1 deletion 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,7 +36,11 @@ public function register()
*/
public function boot()
{
Fortify::loginView(fn ($request) => (new LoginController())($request));
$this->patchRoutes();

$loginController = $this->app->make(LoginController::class);

Fortify::loginView(fn ($request) => $loginController($request));
Fortify::confirmPasswordsUsing(fn ($user, ?string $password = null) => $user->password
? app(StatefulGuard::class)->validate([
'email' => $user->email,
Expand All @@ -57,4 +63,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');
}
});
}
davpsh marked this conversation as resolved.
Show resolved Hide resolved
}
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
1 change: 1 addition & 0 deletions lang/en/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

return [
'failed' => 'These credentials do not match our records.',
'signup_disabled' => 'Registration is currently disabled',
davpsh marked this conversation as resolved.
Show resolved Hide resolved
'lang' => 'English',
'login_provider_azure' => 'Microsoft',
'login_provider_facebook' => 'Facebook',
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Feature</directory>
<directory suffix="Test.php">./tests/Unit</directory>
<directory suffix="Test.php">./tests/PHPUnit</directory>
davpsh marked this conversation as resolved.
Show resolved Hide resolved
</testsuite>
</testsuites>
<coverage/>
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
50 changes: 35 additions & 15 deletions tests/Feature/Auth/RegistrationTest.php
Original file line number Diff line number Diff line change
@@ -1,47 +1,67 @@
<?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;
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
{
$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
{
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;
});

$response = $this->post('/register', [
$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);
$response->assertStatus(Response::HTTP_OK);
$this->assertAuthenticated();
$response->assertRedirect('/vaults');
}
Expand Down
53 changes: 53 additions & 0 deletions tests/PHPUnit/Helpers/SignupHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Tests\PHPUnit\Helpers;

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

#[CoversClass(SignupHelper::class)]
class SignupHelperTest extends TestCase
{
use MockeryPHPUnitIntegration;

#[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();
}
}
Loading