feat: add registration control setting, invites (WIP)

Signed-off-by: Miguel Nogueira <me@nogueira.codes>
This commit is contained in:
2025-08-06 13:35:37 +01:00
parent 2062cd247e
commit 22cffaffca
8 changed files with 134 additions and 80 deletions

View File

@@ -55,6 +55,9 @@ class OptionsController extends Controller
'requiresPMC' => Options::getOption('requireGameLicense'),
'enforce2fa' => Options::getOption('force2fa'),
],
'features' => [
'enableRegistrations' => Options::getOption('enable_registrations')
],
'currentGame' => Options::getOption('currentGame'),
]);
}

View File

@@ -21,6 +21,7 @@ class SecuritySettingsController extends Controller
'pwExpiry' => $request->pwExpiry,
'enforce2fa' => $request->enforce2fa,
'requirePMC' => $request->requirePMC,
'enable_registrations' => $request->input('enable_registrations')
]);
return redirect()

View File

@@ -40,6 +40,7 @@ class SecuritySettingsService
Options::changeOption('password_expiry', $options['pwExpiry']);
Options::changeOption('force2fa', $options['enforce2fa']);
Options::changeOption('requireGameLicense', $options['requirePMC']);
Options::changeOption('enable_registrations', $options['enable_registrations']);
return true;
}

View File

@@ -54,5 +54,6 @@ class DefaultOptionsSeeder extends Seeder
Options::setOption('enable_slack_notifications', true, 'Enable slack notifications', 'notifications');
Options::setOption('enable_email_notifications', true, 'Enable e-mail notifications', 'notifications');
Options::setOption('enable_registrations', true, 'Enable sign-uos', 'app_features');
}
}

View File

@@ -13,98 +13,134 @@
<div class="brand-wrapper">
<img src="{{ asset(config('adminlte.logo_img_xl')) }}" alt="logo" class="logo rounded mr-2">
</div> <!-- main content start -->
<p class="login-card-description">{{__('Sign up for an account')}}</p>
@if(\App\Facades\Options::getOption('pw_security_policy') !== 'off')
@if(\App\Facades\Options::getOption('enable_registrations') == true)
<p class="login-card-description">{{__('Sign up for an account')}}</p>
<div class="alert alert-warning alert-dismissible">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
<p><b>{{__('Basic password security')}}</b></p>
<p>{{__("For your security, we implement strict password policies. It's also advisable to let your password manager or browser generate and save passwords for you (if it's a private device).")}}</p>
@if(\App\Facades\Options::getOption('pw_security_policy') !== 'off')
<p>{{__('Passwords must be a combination of:')}} </p>
<ul>
@switch(\App\Facades\Options::getOption('pw_security_policy'))
<div class="alert alert-warning alert-dismissible">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
<p><b>{{__('Basic password security')}}</b></p>
<p>{{__("For your security, we implement strict password policies. It's also advisable to let your password manager or browser generate and save passwords for you (if it's a private device).")}}</p>
@case('low')
<li>{{ __('A minimum of 10 characters') }}</li>
@break
<p>{{__('Passwords must be a combination of:')}} </p>
<ul>
@switch(\App\Facades\Options::getOption('pw_security_policy'))
@case('medium')
<li>{{ __('A minimum of 12 characters;') }}</li>
<li>{{ __('At least one special character;') }}</li>
<li>{{ __('Lower case and upper case characters') }}</li>
@break
@case('low')
<li>{{ __('A minimum of 10 characters') }}</li>
@break
@case('high')
<li>{{ __('A minimum of 20 characters;') }}</li>
<li>{{ __('At least one special character;') }}</li>
<li>{{ __('Lower case and upper case characters') }}</li>
<li>{{ __('At least one numerical character') }}</li>
@break
@case('medium')
<li>{{ __('A minimum of 12 characters;') }}</li>
<li>{{ __('At least one special character;') }}</li>
<li>{{ __('Lower case and upper case characters') }}</li>
@break
@endswitch
</ul>
</div>
@case('high')
<li>{{ __('A minimum of 20 characters;') }}</li>
<li>{{ __('At least one special character;') }}</li>
<li>{{ __('Lower case and upper case characters') }}</li>
<li>{{ __('At least one numerical character') }}</li>
@break
@endif
@endswitch
</ul>
</div>
@if($demoActive)
<div class="alert alert-warning">
<p class="font-weight-bold"><i class="fas fa-exclamation-triangle"></i>{{ __('Warning') }}</p>
<p>{{ __('Do not use real credentials here. The application is in demo mode. Additionally, the database is wiped every day.') }}</p>
@endif
<p>{{ __("Also note: If a game license is required to sign up, you may find valid MC usernames at NameMC. No special validation is performed other than contacting Mojang's authentication servers to verify the username's existence, therefore, you can use any username for testing purposes.") }}</p>
@if($demoActive)
<div class="alert alert-warning">
<p class="font-weight-bold"><i class="fas fa-exclamation-triangle"></i>{{ __('Warning') }}</p>
<p>{{ __('Do not use real credentials here. The application is in demo mode. Additionally, the database is wiped every day.') }}</p>
<p>{{ __("Also note: If a game license is required to sign up, you may find valid MC usernames at NameMC. No special validation is performed other than contacting Mojang's authentication servers to verify the username's existence, therefore, you can use any username for testing purposes.") }}</p>
</div>
@endif
<form action="{{ route('register') }}" method="POST" id="registerForm">
@csrf
<div class="form-group">
<label for="name" class="sr-only">{{__('Name')}}</label>
<input type="text" name="name" id="name" class="form-control" placeholder="{{__('Name')}}">
</div>
<div class="form-group mb-4">
<label for="email" class="sr-only">{{__('Email address')}}</label>
<input type="email" name="email" id="email" class="form-control" placeholder="{{__('Email address')}}">
</div>
<div class="form-group mb-4">
<label for="password" class="sr-only">{{__('Password')}}</label>
<input type="password" name="password" id="password" class="form-control" placeholder="{{__('Password')}}">
</div>
<div class="form-group mb-2">
<label for="passwordc" class="sr-only">{{__('Confirm Password')}}</label>
<input type="password" id="passwordc" name="password_confirmation" class="form-control" placeholder="{{__('Confirm Password')}}" />
</div>
<div class="form-group mb-4 mt-5">
<label for="dob" class="sr-only">{{__('Date of birth')}}</label>
<input type="text" class="form-control" name="dob" id="dob" placeholder="Date of birth">
<span class="text-muted text-sm"><i class="fas fa-info-circle"></i> {!! __("<b>Why do we need this?</b> We use your age information to make sure you meet age requirements for certain positions, and to make sure that everyone is compliant with our terms of service.") !!} </span>
</div>
@if(\App\Facades\Options::getOption('requireGameLicense') && \App\Facades\Options::getOption('currentGame') == 'MINECRAFT')
<div class="form-group mt-5">
<label for="mcusername" class="sr-only">{{__('Minecraft Username (Premium)')}}</label>
<input type="text" name="uuid" class="form-control" id="mcusername" placeholder="{{__('Minecraft Username (Premium)')}}" />
</div>
@endif
<div class="form-group mt-3">
<label for="legal"><input type="checkbox" name="acceptTerms"> {!! __('I have read and agree with the :appName <a href=":communityGuidelinesUrlConfigValue" target="_blank">Community Guidelines</a>, <a href=":privacyPolicyUrlConfigValue" target="_blank">Privacy Policy</a> and <a href=":termsUrlConfigValue" target="_blank">Terms of Service</a>.', ['communityGuidelinesUrlConfigValue' => config('app.guidelines_url'), 'privacyPolicyUrlConfigValue' => config('app.privacy_url'), 'termsUrlConfigValue' => config('app.terms_url'), 'appName' => config('app.name')]) !!}</label>
</div>
<input name="register" id="register" class="btn btn-block login-btn mb-4" type="submit" value="{{__('Sign up')}}">
</form>
<script>
flatpickr('#dob', {
altInput: true,
altFormat: "F j, Y",
dateFormat: "Y-m-d",
});
</script>
@else
<div class="alert alert-danger">
<b><i class="fa fa-exclamation-triangle"></i> {{ __('Sorry, but new signups are currently closed.') }}</b>
<p>{{ __('Due to an internal policy, new accounts cannot be created, and so you will not be able to sign up for an account. If you already have an account with us, you can still sign-in as you usually would.') }}</p>
</div>
<div class="alert alert-warning">
<b><i class="fas fa-question-circle"></i> {{ __('I\'m trying to sign up so I can apply for a vacancy here. What does this restriction mean for me?') }}</b>
<p>{{ __('Effectively, this means that you will not be able to apply for any of our vacancies unless you already have an account. However, if you\'ve received an invitation to create an account or apply for a vacancy, you still can, but you must use the sign-up invitation link sent to your email address.') }}</p>
<p>{!! __('Alternatively, if you don\'t have an invitation, feel free to :requestInviteLink.', ['requestInviteLink' => '<a href="#" id="toggleInviteRequestForm">' . __('request an invite') . '</a>']) !!}</p>
</div>
<div id="inviteRequestFormContainer" style="display:none;">
<form action="{{ route('invitations.request') }}" method="POST" id="inviteRequestForm">
@csrf
<div class="form-group">
<label for="inviteEmail" class="sr-only">{{ __('Email address') }}</label>
<input type="email" name="email" id="inviteEmail" class="form-control" placeholder="{{ __('Enter your email address') }}" required>
</div>
<input type="submit" class="btn btn-primary btn-block" value="{{ __('Request Invite') }}">
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var toggleLink = document.getElementById('toggleInviteRequestForm');
var formContainer = document.getElementById('inviteRequestFormContainer');
toggleLink.addEventListener('click', function(e) {
e.preventDefault();
formContainer.style.display = (formContainer.style.display === 'none') ? 'block' : 'none';
});
});
</script>
@endif
<form action="{{ route('register') }}" method="POST" id="registerForm">
@csrf
<div class="form-group">
<label for="name" class="sr-only">{{__('Name')}}</label>
<input type="text" name="name" id="name" class="form-control" placeholder="{{__('Name')}}">
</div>
<div class="form-group mb-4">
<label for="email" class="sr-only">{{__('Email address')}}</label>
<input type="email" name="email" id="email" class="form-control" placeholder="{{__('Email address')}}">
</div>
<div class="form-group mb-4">
<label for="password" class="sr-only">{{__('Password')}}</label>
<input type="password" name="password" id="password" class="form-control" placeholder="{{__('Password')}}">
</div>
<div class="form-group mb-2">
<label for="passwordc" class="sr-only">{{__('Confirm Password')}}</label>
<input type="password" id="passwordc" name="password_confirmation" class="form-control" placeholder="{{__('Confirm Password')}}" />
</div>
<div class="form-group mb-4 mt-5">
<label for="dob" class="sr-only">{{__('Date of birth')}}</label>
<input type="text" class="form-control" name="dob" id="dob" placeholder="Date of birth">
<span class="text-muted text-sm"><i class="fas fa-info-circle"></i> {!! __("<b>Why do we need this?</b> We use your age information to make sure you meet age requirements for certain positions, and to make sure that everyone is compliant with our terms of service.") !!} </span>
</div>
@if(\App\Facades\Options::getOption('requireGameLicense') && \App\Facades\Options::getOption('currentGame') == 'MINECRAFT')
<div class="form-group mt-5">
<label for="mcusername" class="sr-only">{{__('Minecraft Username (Premium)')}}</label>
<input type="text" name="uuid" class="form-control" id="mcusername" placeholder="{{__('Minecraft Username (Premium)')}}" />
</div>
@endif
<div class="form-group mt-3">
<label for="legal"><input type="checkbox" name="acceptTerms"> {!! __('I have read and agree with the :appName <a href=":communityGuidelinesUrlConfigValue" target="_blank">Community Guidelines</a>, <a href=":privacyPolicyUrlConfigValue" target="_blank">Privacy Policy</a> and <a href=":termsUrlConfigValue" target="_blank">Terms of Service</a>.', ['communityGuidelinesUrlConfigValue' => config('app.guidelines_url'), 'privacyPolicyUrlConfigValue' => config('app.privacy_url'), 'termsUrlConfigValue' => config('app.terms_url'), 'appName' => config('app.name')]) !!}</label>
</div>
<input name="register" id="register" class="btn btn-block login-btn mb-4" type="submit" value="{{__('Sign up')}}">
</form>
<script>
flatpickr('#dob', {
altInput: true,
altFormat: "F j, Y",
dateFormat: "Y-m-d",
});
</script>
<p class="login-card-footer-text">{{__('Have an account with us?')}} <a href="{{ route('login') }}" class="text-reset">{{__('Sign in here')}}</a></p>
<nav class="login-card-footer-nav">
<a href="{{ config('app.terms_url') }}">{{__('Terms of Service')}}</a>

View File

@@ -9,7 +9,7 @@
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/4.8.95/css/materialdesignicons.min.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" />
<script src="https://kit.fontawesome.com/2d0b1aecfa.js" crossorigin="anonymous"></script>
<script src="https://kit.fontawesome.com/2f2450d84f.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/css/login.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>

View File

@@ -200,6 +200,14 @@
<p class="text-muted text-sm"><i class="fas fa-info-circle"></i> {{ __('Choose a game in the section below, if applicable.') }}</p>
</div>
<div class="form-group form-check">
<input type="hidden" name="enable_registrations" value="0">
<input type="checkbox" name="enable_registrations" value="1" id="enable_registrations" class="form-check-input" {{ $features['enableRegistrations'] == true ? 'checked' : '' }}>
<label for="enable_registrations">{{ __('Allow people to make new accounts?') }}</label>
<p class="text-muted text-sm"><i class="fas fa-info-circle"></i> {{ __('Unchecking this box will stop people from signing up to :appName and disable the associated form. However, you will still be able to invite users and create new accounts from the administrative UI if you so choose.', ['appName' => config('app.name')]) }}</p>
</div>
</form>
</div>

View File

@@ -29,6 +29,7 @@ use App\Http\Controllers\DashboardController;
use App\Http\Controllers\DevToolsController;
use App\Http\Controllers\FormController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\InvitationController;
use App\Http\Controllers\OptionsController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\SecuritySettingsController;
@@ -57,6 +58,9 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
'verify' => true,
]);
Route::post('/invitations/request', [InvitationController::class, 'requestInvite'])
->name('invitations.request');
Route::post('/twofa/authenticate', [TwofaController::class, 'verify2FA'])
->name('verify2FA');