Skip to content

Commit 3eed7c8

Browse files
committed
Add an email registration policy feature - Closes #250
1 parent fd5520c commit 3eed7c8

12 files changed

+320
-10
lines changed

app/Api/v1/Requests/SettingUpdateRequest.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Api\v1\Requests;
44

5+
use App\Rules\IsValideEmailList;
56
use Illuminate\Foundation\Http\FormRequest;
67
use Illuminate\Support\Facades\Auth;
78

@@ -24,8 +25,16 @@ public function authorize()
2425
*/
2526
public function rules()
2627
{
27-
return [
28-
'value' => 'required',
28+
$rule = [
29+
'value' => [
30+
'required',
31+
]
2932
];
33+
34+
if ($this->route()->parameter('settingName') == 'restrictList') {
35+
$rule['value'][] = new IsValideEmailList;
36+
}
37+
38+
return $rule;
3039
}
3140
}

app/Http/Requests/UserStoreRequest.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Http\Requests;
44

5+
use App\Rules\ComplyWithEmailRestrictionPolicy;
56
use Illuminate\Foundation\Http\FormRequest;
67

78
class UserStoreRequest extends FormRequest
@@ -24,8 +25,15 @@ public function authorize()
2425
public function rules()
2526
{
2627
return [
27-
'name' => 'unique:App\Models\User,name|required|string|max:191',
28-
'email' => 'unique:App\Models\User,email|required|string|email|max:191',
28+
'name' => 'unique:App\Models\User,name|required|string|max:191',
29+
'email' => [
30+
'unique:App\Models\User,email',
31+
'required',
32+
'string',
33+
'email',
34+
'max:191',
35+
new ComplyWithEmailRestrictionPolicy,
36+
],
2937
'password' => 'required|string|min:8|confirmed',
3038
];
3139
}

app/Http/Requests/UserUpdateRequest.php

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Http\Requests;
44

5+
use App\Rules\ComplyWithEmailRestrictionPolicy;
56
use Illuminate\Foundation\Http\FormRequest;
67
use Illuminate\Support\Facades\Auth;
78
use Illuminate\Validation\Rule;
@@ -37,6 +38,7 @@ public function rules()
3738
'email',
3839
'max:191',
3940
Rule::unique('users')->ignore($this->user()->id),
41+
new ComplyWithEmailRestrictionPolicy,
4042
],
4143
'password' => 'required',
4244
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Rules;
4+
5+
use App\Facades\Settings;
6+
use Closure;
7+
use Illuminate\Contracts\Validation\ValidationRule;
8+
9+
class ComplyWithEmailRestrictionPolicy implements ValidationRule
10+
{
11+
/**
12+
* Run the validation rule.
13+
*/
14+
public function validate(string $attribute, mixed $value, Closure $fail): void
15+
{
16+
$list = Settings::get('restrictList');
17+
$regex = Settings::get('restrictRule');
18+
19+
$validatesFilter = true;
20+
$validatesRegex = true;
21+
22+
if (Settings::get('restrictRegistration') == true) {
23+
if ($list && ! in_array($value, explode('|', $list))) {
24+
$validatesFilter = false;
25+
}
26+
if ($regex && ! preg_match('/' . $regex . '/', $value)) {
27+
$validatesRegex = false;
28+
}
29+
30+
if ($list && $regex) {
31+
if (! $validatesFilter && ! $validatesRegex) {
32+
$fail('validation.custom.email.ComplyWithEmailRestrictionPolicy')->translate();
33+
}
34+
}
35+
else {
36+
if (! $validatesFilter || ! $validatesRegex) {
37+
$fail('validation.custom.email.ComplyWithEmailRestrictionPolicy')->translate();
38+
}
39+
}
40+
}
41+
}
42+
}

app/Rules/IsValideEmailList.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Rules;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
use Illuminate\Support\Facades\Validator;
8+
9+
class IsValideEmailList implements ValidationRule
10+
{
11+
/**
12+
* Run the validation rule.
13+
*/
14+
public function validate(string $attribute, mixed $value, Closure $fail): void
15+
{
16+
$emails = explode('|', $value);
17+
18+
$pass = Validator::make(
19+
$emails,
20+
[
21+
'*' => 'email',
22+
]
23+
)->passes();
24+
25+
if (! $pass) {
26+
$fail('validation.custom.email.IsValidEmailList')->translate();
27+
}
28+
}
29+
}

config/2fauth.php

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
'latestRelease' => false,
7575
'disableRegistration' => false,
7676
'enableSso' => true,
77+
'restrictRegistration' => false,
7778
],
7879

7980
/*

resources/js/icons.js

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
faFileLines,
4747
faVideoSlash,
4848
faChevronRight,
49+
faSlash,
4950
} from '@fortawesome/free-solid-svg-icons'
5051

5152
import {
@@ -107,6 +108,7 @@ library.add(
107108
faChevronRight,
108109
faOpenid,
109110
faPaperPlane,
111+
faSlash,
110112
);
111113

112114
export default FontAwesomeIcon

resources/js/services/appSettingService.js

+15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { httpClientFactory } from '@/services/httpClientFactory'
33
const apiClient = httpClientFactory('api')
44

55
export default {
6+
/**
7+
*
8+
* @returns
9+
*/
10+
get(config = {}) {
11+
return apiClient.get('/settings', { ...config })
12+
},
13+
614
/**
715
*
816
* @returns
@@ -11,4 +19,11 @@ export default {
1119
return apiClient.put('/settings/' + name, { value: value })
1220
},
1321

22+
/**
23+
*
24+
* @returns
25+
*/
26+
delete(name, config = {}) {
27+
return apiClient.delete('/settings/' + name, { ...config })
28+
},
1429
}

resources/js/views/admin/AppSetup.vue

+77-5
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,66 @@
1717
const infos = ref()
1818
const listInfos = ref(null)
1919
const isSendingTestEmail = ref(false)
20+
const fieldErrors = ref({
21+
restrictList: null,
22+
restrictRule: null,
23+
})
24+
const _settings = ref({
25+
checkForUpdate: appSettings.checkForUpdate,
26+
useEncryption: appSettings.useEncryption,
27+
restrictRegistration: appSettings.restrictRegistration,
28+
restrictList: appSettings.restrictList,
29+
restrictRule: appSettings.restrictRule,
30+
disableRegistration: appSettings.disableRegistration,
31+
enableSso: appSettings.enableSso,
32+
})
2033
2134
/**
2235
* Saves a setting on the backend
2336
* @param {string} preference
2437
* @param {any} value
2538
*/
2639
function saveSetting(setting, value) {
40+
fieldErrors.value[setting] = null
41+
2742
appSettingService.update(setting, value).then(response => {
43+
appSettings[setting] = value
2844
useNotifyStore().success({ type: 'is-success', text: trans('settings.forms.setting_saved') })
2945
})
46+
.catch(error => {
47+
if( error.response.status === 422 ) {
48+
fieldErrors.value[setting] = error.response.data.message
49+
}
50+
else {
51+
notify.error(error);
52+
}
53+
})
54+
}
55+
56+
/**
57+
* Saves a setting on the backend
58+
* @param {string} preference
59+
* @param {any} value
60+
*/
61+
function saveOrDeleteSetting(setting, value) {
62+
if (value == '') {
63+
fieldErrors.value[setting] = null
64+
65+
appSettingService.delete(setting, { returnError: true }).then(response => {
66+
appSettings[setting] = ''
67+
useNotifyStore().success({ type: 'is-success', text: trans('settings.forms.setting_saved') })
68+
})
69+
.catch(error => {
70+
// appSettings[setting] = oldValue
71+
72+
if( error.response.status !== 404 ) {
73+
notify.error(error);
74+
}
75+
})
76+
}
77+
else {
78+
saveSetting(setting, value)
79+
}
3080
}
3181
3282
/**
@@ -47,7 +97,23 @@
4797
}
4898
})
4999
50-
onMounted(() => {
100+
onMounted(async () => {
101+
appSettingService.get({ returnError: true })
102+
.then(response => {
103+
// we reset those two because they are not registered on server side
104+
// in order to be able to set them to blank
105+
_settings.value.restrictList = ''
106+
_settings.value.restrictRule = ''
107+
108+
response.data.forEach(setting => {
109+
appSettings[setting.key] = setting.value
110+
_settings.value[setting.key] = setting.value
111+
})
112+
})
113+
.catch(error => {
114+
notify.alert({ text: trans('errors.data_cannot_be_refreshed_from_server') })
115+
})
116+
51117
systemService.getSystemInfos({returnError: true}).then(response => {
52118
infos.value = response.data.common
53119
})
@@ -66,7 +132,7 @@
66132
<form>
67133
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
68134
<!-- Check for update -->
69-
<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" />
135+
<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" />
70136
<VersionChecker />
71137
<div class="field">
72138
<!-- <h5 class="title is-5">{{ $t('settings.security') }}</h5> -->
@@ -86,12 +152,18 @@
86152
</div>
87153
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
88154
<!-- protect db -->
89-
<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" />
155+
<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" />
90156
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('admin.registrations') }}</h4>
157+
<!-- restrict registration -->
158+
<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" />
159+
<!-- restrict list -->
160+
<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" />
161+
<!-- restrict rule -->
162+
<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" />
91163
<!-- disable registration -->
92-
<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" />
164+
<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" />
93165
<!-- disable SSO registration -->
94-
<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" />
166+
<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" />
95167
</form>
96168
<h4 class="title is-4 pt-5 has-text-grey-light">{{ $t('commons.environment') }}</h4>
97169
<div v-if="infos" class="about-debug box is-family-monospace is-size-7">

resources/lang/en/admin.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,21 @@
6565
'security_devices_succesfully_revoked' => 'User\'s security devices successfully revoked',
6666
'forms' => [
6767
'use_encryption' => [
68-
'label' => 'Protect sensible data',
68+
'label' => 'Protect sensitive data',
6969
'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.',
7070
],
71+
'restrict_registration' => [
72+
'label' => 'Restrict registration',
73+
'help' => 'Make registration only available to a limited range of email addresses. Both rules can be used simultaneously.',
74+
],
75+
'restrict_list' => [
76+
'label' => 'Filtering list',
77+
'help' => 'Emails in this list will be allowed to register. Separate addresses with a pipe ("|")',
78+
],
79+
'restrict_rule' => [
80+
'label' => 'Filtering rule',
81+
'help' => 'Emails matching this regular expression will be allowed to register',
82+
],
7183
'disable_registration' => [
7284
'label' => 'Disable registration',
7385
'help' => 'Prevent new user registration. This affects SSO as well, so new SSO users won\'t be able to sign on',

resources/lang/en/validation.php

+2
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@
170170
],
171171
'email' => [
172172
'exists' => 'No account found using this email.',
173+
'ComplyWithEmailRestrictionPolicy' => 'This email address does not comply with the registration policy',
174+
'IsValidEmailList' => 'All emails must be valid and separated with a pipe'
173175
],
174176
'secret' => [
175177
'isBase32Encoded' => 'The :attribute must be a base32 encoded string.',

0 commit comments

Comments
 (0)