Skip to content

Commit

Permalink
Add an email registration policy feature - Closes #250
Browse files Browse the repository at this point in the history
  • Loading branch information
Bubka committed Feb 29, 2024
1 parent fd5520c commit 3eed7c8
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 10 deletions.
13 changes: 11 additions & 2 deletions app/Api/v1/Requests/SettingUpdateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Api\v1\Requests;

use App\Rules\IsValideEmailList;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;

Expand All @@ -24,8 +25,16 @@ public function authorize()
*/
public function rules()
{
return [
'value' => 'required',
$rule = [
'value' => [
'required',
]
];

if ($this->route()->parameter('settingName') == 'restrictList') {
$rule['value'][] = new IsValideEmailList;
}

return $rule;
}
}
12 changes: 10 additions & 2 deletions app/Http/Requests/UserStoreRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Requests;

use App\Rules\ComplyWithEmailRestrictionPolicy;
use Illuminate\Foundation\Http\FormRequest;

class UserStoreRequest extends FormRequest
Expand All @@ -24,8 +25,15 @@ public function authorize()
public function rules()
{
return [
'name' => 'unique:App\Models\User,name|required|string|max:191',
'email' => 'unique:App\Models\User,email|required|string|email|max:191',
'name' => 'unique:App\Models\User,name|required|string|max:191',
'email' => [
'unique:App\Models\User,email',
'required',
'string',
'email',
'max:191',
new ComplyWithEmailRestrictionPolicy,
],
'password' => 'required|string|min:8|confirmed',
];
}
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Requests/UserUpdateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Requests;

use App\Rules\ComplyWithEmailRestrictionPolicy;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
Expand Down Expand Up @@ -37,6 +38,7 @@ public function rules()
'email',
'max:191',
Rule::unique('users')->ignore($this->user()->id),
new ComplyWithEmailRestrictionPolicy,
],
'password' => 'required',
];
Expand Down
42 changes: 42 additions & 0 deletions app/Rules/ComplyWithEmailRestrictionPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Rules;

use App\Facades\Settings;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class ComplyWithEmailRestrictionPolicy implements ValidationRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$list = Settings::get('restrictList');
$regex = Settings::get('restrictRule');

$validatesFilter = true;
$validatesRegex = true;

if (Settings::get('restrictRegistration') == true) {
if ($list && ! in_array($value, explode('|', $list))) {
$validatesFilter = false;
}
if ($regex && ! preg_match('/' . $regex . '/', $value)) {
$validatesRegex = false;
}

if ($list && $regex) {
if (! $validatesFilter && ! $validatesRegex) {
$fail('validation.custom.email.ComplyWithEmailRestrictionPolicy')->translate();
}
}
else {
if (! $validatesFilter || ! $validatesRegex) {
$fail('validation.custom.email.ComplyWithEmailRestrictionPolicy')->translate();
}
}
}
}
}
29 changes: 29 additions & 0 deletions app/Rules/IsValideEmailList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Validator;

class IsValideEmailList implements ValidationRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$emails = explode('|', $value);

$pass = Validator::make(
$emails,
[
'*' => 'email',
]
)->passes();

if (! $pass) {
$fail('validation.custom.email.IsValidEmailList')->translate();
}
}
}
1 change: 1 addition & 0 deletions config/2fauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
'latestRelease' => false,
'disableRegistration' => false,
'enableSso' => true,
'restrictRegistration' => false,
],

/*
Expand Down
2 changes: 2 additions & 0 deletions resources/js/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
faFileLines,
faVideoSlash,
faChevronRight,
faSlash,
} from '@fortawesome/free-solid-svg-icons'

import {
Expand Down Expand Up @@ -107,6 +108,7 @@ library.add(
faChevronRight,
faOpenid,
faPaperPlane,
faSlash,
);

export default FontAwesomeIcon
15 changes: 15 additions & 0 deletions resources/js/services/appSettingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { httpClientFactory } from '@/services/httpClientFactory'
const apiClient = httpClientFactory('api')

export default {
/**
*
* @returns
*/
get(config = {}) {
return apiClient.get('/settings', { ...config })
},

/**
*
* @returns
Expand All @@ -11,4 +19,11 @@ export default {
return apiClient.put('/settings/' + name, { value: value })
},

/**
*
* @returns
*/
delete(name, config = {}) {
return apiClient.delete('/settings/' + name, { ...config })
},
}
82 changes: 77 additions & 5 deletions resources/js/views/admin/AppSetup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,66 @@
const infos = ref()
const listInfos = ref(null)
const isSendingTestEmail = ref(false)
const fieldErrors = ref({
restrictList: null,
restrictRule: null,
})
const _settings = ref({
checkForUpdate: appSettings.checkForUpdate,
useEncryption: appSettings.useEncryption,
restrictRegistration: appSettings.restrictRegistration,
restrictList: appSettings.restrictList,
restrictRule: appSettings.restrictRule,
disableRegistration: appSettings.disableRegistration,
enableSso: appSettings.enableSso,
})
/**
* Saves a setting on the backend
* @param {string} preference
* @param {any} value
*/
function saveSetting(setting, value) {
fieldErrors.value[setting] = null
appSettingService.update(setting, value).then(response => {
appSettings[setting] = value
useNotifyStore().success({ type: 'is-success', text: trans('settings.forms.setting_saved') })
})
.catch(error => {
if( error.response.status === 422 ) {
fieldErrors.value[setting] = error.response.data.message
}
else {
notify.error(error);
}
})
}
/**
* Saves a setting on the backend
* @param {string} preference
* @param {any} value
*/
function saveOrDeleteSetting(setting, value) {
if (value == '') {
fieldErrors.value[setting] = null
appSettingService.delete(setting, { returnError: true }).then(response => {
appSettings[setting] = ''
useNotifyStore().success({ type: 'is-success', text: trans('settings.forms.setting_saved') })
})
.catch(error => {
// appSettings[setting] = oldValue
if( error.response.status !== 404 ) {
notify.error(error);
}
})
}
else {
saveSetting(setting, value)
}
}
/**
Expand All @@ -47,7 +97,23 @@
}
})
onMounted(() => {
onMounted(async () => {
appSettingService.get({ returnError: true })
.then(response => {
// we reset those two because they are not registered on server side
// in order to be able to set them to blank
_settings.value.restrictList = ''
_settings.value.restrictRule = ''
response.data.forEach(setting => {
appSettings[setting.key] = setting.value
_settings.value[setting.key] = setting.value
})
})
.catch(error => {
notify.alert({ text: trans('errors.data_cannot_be_refreshed_from_server') })
})
systemService.getSystemInfos({returnError: true}).then(response => {
infos.value = response.data.common
})
Expand All @@ -66,7 +132,7 @@
<form>
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
<!-- Check for update -->
<FormCheckbox v-model="appSettings.checkForUpdate" @update:model-value="val => saveSetting('checkForUpdate', val)" fieldName="checkForUpdate" label="commons.check_for_update" help="commons.check_for_update_help" />
<FormCheckbox v-model="_settings.checkForUpdate" @update:model-value="val => saveSetting('checkForUpdate', val)" fieldName="checkForUpdate" label="commons.check_for_update" help="commons.check_for_update_help" />
<VersionChecker />
<div class="field">
<!-- <h5 class="title is-5">{{ $t('settings.security') }}</h5> -->
Expand All @@ -86,12 +152,18 @@
</div>
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
<!-- protect db -->
<FormCheckbox v-model="appSettings.useEncryption" @update:model-value="val => saveSetting('useEncryption', val)" fieldName="useEncryption" label="admin.forms.use_encryption.label" help="admin.forms.use_encryption.help" />
<FormCheckbox v-model="_settings.useEncryption" @update:model-value="val => saveSetting('useEncryption', val)" fieldName="useEncryption" label="admin.forms.use_encryption.label" help="admin.forms.use_encryption.help" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('admin.registrations') }}</h4>
<!-- restrict registration -->
<FormCheckbox v-model="_settings.restrictRegistration" @update:model-value="val => saveSetting('restrictRegistration', val)" fieldName="restrictRegistration" :isDisabled="appSettings.disableRegistration" label="admin.forms.restrict_registration.label" help="admin.forms.restrict_registration.help" />
<!-- restrict list -->
<FormField v-model="_settings.restrictList" @change:model-value="val => saveOrDeleteSetting('restrictList', val)" :fieldError="fieldErrors.restrictList" fieldName="restrictList" :isDisabled="!appSettings.restrictRegistration || appSettings.disableRegistration" label="admin.forms.restrict_list.label" help="admin.forms.restrict_list.help" :isIndented="true" />
<!-- restrict rule -->
<FormField v-model="_settings.restrictRule" @change:model-value="val => saveOrDeleteSetting('restrictRule', val)" :fieldError="fieldErrors.restrictRule" fieldName="restrictRule" :isDisabled="!appSettings.restrictRegistration || appSettings.disableRegistration" label="admin.forms.restrict_rule.label" help="admin.forms.restrict_rule.help" :isIndented="true" leftIcon="slash" rightIcon="slash" />
<!-- disable registration -->
<FormCheckbox v-model="appSettings.disableRegistration" @update:model-value="val => saveSetting('disableRegistration', val)" fieldName="disableRegistration" label="admin.forms.disable_registration.label" help="admin.forms.disable_registration.help" />
<FormCheckbox v-model="_settings.disableRegistration" @update:model-value="val => saveSetting('disableRegistration', val)" fieldName="disableRegistration" label="admin.forms.disable_registration.label" help="admin.forms.disable_registration.help" />
<!-- disable SSO registration -->
<FormCheckbox v-model="appSettings.enableSso" @update:model-value="val => saveSetting('enableSso', val)" fieldName="enableSso" label="admin.forms.enable_sso.label" help="admin.forms.enable_sso.help" />
<FormCheckbox v-model="_settings.enableSso" @update:model-value="val => saveSetting('enableSso', val)" fieldName="enableSso" label="admin.forms.enable_sso.label" help="admin.forms.enable_sso.help" />
</form>
<h4 class="title is-4 pt-5 has-text-grey-light">{{ $t('commons.environment') }}</h4>
<div v-if="infos" class="about-debug box is-family-monospace is-size-7">
Expand Down
14 changes: 13 additions & 1 deletion resources/lang/en/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,21 @@
'security_devices_succesfully_revoked' => 'User\'s security devices successfully revoked',
'forms' => [
'use_encryption' => [
'label' => 'Protect sensible data',
'label' => 'Protect sensitive data',
'help' => 'Sensitive data, the 2FA secrets and emails, are stored encrypted in database. Be sure to backup the APP_KEY value of your .env file (or the whole file) as it serves as key encryption. There is no way to decypher encrypted data without this key.',
],
'restrict_registration' => [
'label' => 'Restrict registration',
'help' => 'Make registration only available to a limited range of email addresses. Both rules can be used simultaneously.',
],
'restrict_list' => [
'label' => 'Filtering list',
'help' => 'Emails in this list will be allowed to register. Separate addresses with a pipe ("|")',
],
'restrict_rule' => [
'label' => 'Filtering rule',
'help' => 'Emails matching this regular expression will be allowed to register',
],
'disable_registration' => [
'label' => 'Disable registration',
'help' => 'Prevent new user registration. This affects SSO as well, so new SSO users won\'t be able to sign on',
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@
],
'email' => [
'exists' => 'No account found using this email.',
'ComplyWithEmailRestrictionPolicy' => 'This email address does not comply with the registration policy',
'IsValidEmailList' => 'All emails must be valid and separated with a pipe'
],
'secret' => [
'isBase32Encoded' => 'The :attribute must be a base32 encoded string.',
Expand Down
Loading

0 comments on commit 3eed7c8

Please sign in to comment.