Add Group selector to the Advanced form - Closes #372

This commit is contained in:
Bubka 2024-08-06 11:21:08 +02:00
parent 5d3a1be38f
commit ba4e1edffa
8 changed files with 253 additions and 5 deletions

View File

@ -85,7 +85,7 @@ public function store(TwoFAccountDynamicRequest $request)
$request->user()->twofaccounts()->save($twofaccount);
// Possible group association
Groups::assign($twofaccount->id, $request->user());
Groups::assign($twofaccount->id, $request->user(), Arr::get($validated, 'group_id', null));
return (new TwoFAccountReadResource($twofaccount->refresh()))
->response()
@ -106,6 +106,16 @@ public function update(TwoFAccountUpdateRequest $request, TwoFAccount $twofaccou
$twofaccount->fillWithOtpParameters($validated);
$request->user()->twofaccounts()->save($twofaccount);
// Possible group change
$groupId = Arr::get($validated, 'group_id', null);
if ($twofaccount->group_id != $groupId) {
if ((int) $groupId === 0) {
TwoFAccounts::withdraw($twofaccount->id);
}
else Groups::assign($twofaccount->id, $request->user(), $groupId);
$twofaccount->refresh();
}
return (new TwoFAccountReadResource($twofaccount))
->response()
->setStatusCode(200);

View File

@ -5,6 +5,8 @@
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Fluent;
use Illuminate\Validation\Validator;
class TwoFAccountDynamicRequest extends FormRequest
{
@ -32,6 +34,18 @@ public function rules()
return $rules;
}
/**
* Get the "withValidator" validation callables for the request.
*/
public function withValidator(Validator $validator) : void
{
// The account may have to be assign to a specific group.
// If so, we check if the provided group exists.
$validator->sometimes('group_id', 'exists:groups,id', function (Fluent $input) {
return $input['group_id'] > 0;
});
}
/**
* Prepare the data for validation.
*
@ -45,5 +59,11 @@ protected function prepareForValidation()
'otp_type' => strtolower($this->otp_type),
'algorithm' => strtolower($this->algorithm),
]);
if ($this->has('group_id') && $this->group_id === '') {
$this->merge([
'group_id' => null,
]);
}
}
}

View File

@ -28,6 +28,7 @@ public function rules()
'service' => 'nullable|string|regex:/^[^:]+$/i',
'account' => 'required|string|regex:/^[^:]+$/i',
'icon' => 'nullable|string',
'group_id' => 'sometimes|nullable|integer|min:0',
'otp_type' => 'required|string|in:totp,hotp,steamtotp',
'secret' => ['string', 'bail', new \App\Rules\IsBase32Encoded],
'digits' => 'nullable|integer|between:5,10',

View File

@ -4,6 +4,8 @@
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Fluent;
use Illuminate\Validation\Validator;
class TwoFAccountUpdateRequest extends FormRequest
{
@ -28,6 +30,7 @@ public function rules()
'service' => 'present|nullable|string|regex:/^[^:]+$/i',
'account' => 'required|string|regex:/^[^:]+$/i',
'icon' => 'present|nullable|string',
'group_id' => 'sometimes|nullable|integer|min:0',
'otp_type' => 'required|string|in:totp,hotp,steamtotp',
'secret' => ['present', 'string', 'bail', new \App\Rules\IsBase32Encoded],
'digits' => 'present|integer|between:5,10',
@ -37,6 +40,18 @@ public function rules()
];
}
/**
* Get the "withValidator" validation callables for the request.
*/
public function withValidator(Validator $validator) : void
{
// The account may have to be assign to a specific group.
// If so, we check if the provided group exists.
$validator->sometimes('group_id', 'exists:groups,id', function (Fluent $input) {
return $input['group_id'] > 0;
});
}
/**
* Prepare the data for validation.
*

View File

@ -15,12 +15,39 @@ class GroupService
* Assign one or more accounts to a group
*
* @param array|int $ids accounts ids to assign
* @param \App\Models\Group|null $group The group the accounts will be assigned to
* @param mixed $targetGroup The group the accounts should be assigned to
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public static function assign($ids, User $user, ?Group $group = null) : void
public static function assign($ids, User $user, mixed $targetGroup = null) : void
{
// targetGroup == 0 == The pseudo group named 'All' == No group
// It means we do not want the accounts to be associated to a group, either a
// specific group or the default group from user preferences.
// If you need to release the accounts from an existing association, use the
// TwoFAccountService::withdraw() method.
if ($targetGroup === 0 || $targetGroup === '0') {
Log::info('Group assignment skipped, no group explicitly requested');
return;
}
// Two main cases :
// - A group (or group id) is passed as parameter => It has priority for use, if the group is valid
// - No group is passed => We try to identify a destination group through user preferences
$group = null;
if(! is_null($targetGroup)) {
if ($targetGroup instanceof Group && $targetGroup->exists && $targetGroup->user_id == $user->id) {
$group = $targetGroup;
}
else {
$group = Group::where('id', (int) $targetGroup)
->where('user_id', $user->id)
->first();
}
}
if (! $group) {
$group = self::defaultGroup($user);
}

View File

@ -6,6 +6,7 @@
import twofaccountService from '@/services/twofaccountService'
import { useUserStore } from '@/stores/user'
import { useTwofaccounts } from '@/stores/twofaccounts'
import { useGroups } from '@/stores/groups'
import { useBusStore } from '@/stores/bus'
import { useNotifyStore } from '@/stores/notify'
import { UseColorMode } from '@vueuse/components'
@ -22,6 +23,7 @@
account: '',
otp_type: '',
icon: '',
group_id: user.preferences.defaultGroup == -1 ? user.preferences.activeGroup : user.preferences.defaultGroup,
secret: '',
algorithm: '',
digits: null,
@ -85,10 +87,19 @@
return props.twofaccountId != undefined
})
const groups = computed(() => {
return useGroups().items.map((item) => {
return { text: item.id > 0 ? item.name : '- ' + trans('groups.no_group') + ' -', value: item.id }
})
})
onMounted(() => {
if (route.name == 'editAccount') {
twofaccountService.get(props.twofaccountId).then(response => {
form.fill(response.data)
if (form.group_id == null) {
form.group_id = 0
}
form.setOriginal()
// set account icon as temp icon
tempIcon.value = form.icon
@ -513,6 +524,8 @@
<FieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" />
<p v-if="user.preferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
</div>
<!-- group -->
<FormSelect v-if="groups.length > 0" v-model="form.group_id" :options="groups" fieldName="group_id" label="twofaccounts.forms.group.label" help="twofaccounts.forms.group.help" />
<!-- otp type -->
<FormToggle v-model="form.otp_type" :isDisabled="isEditMode" :choices="otp_types" fieldName="otp_type" :fieldError="form.errors.get('otp_type')" label="twofaccounts.forms.otp_type.label" help="twofaccounts.forms.otp_type.help" :hasOffset="true" />
<div v-if="form.otp_type != ''">

View File

@ -60,6 +60,10 @@
'i_m_lucky' => 'Try my luck',
'i_m_lucky_legend' => 'The "Try my luck" button try to get the official icon of the given service. Enter actual service name without ".xyz" extension and try to avoid typo. (beta feature)',
'test' => 'Test',
'group' => [
'label' => 'Group',
'help' => 'The group to which the account is to be assigned'
],
'secret' => [
'label' => 'Secret',
'help' => 'The key used to generate your security codes'

View File

@ -517,6 +517,102 @@ public function test_store_with_invalid_uri_returns_validation_error()
->assertStatus(422);
}
#[Test]
public function test_store_assigns_created_account_to_provided_groupid()
{
// Set the default group to No group
$this->user['preferences->defaultGroup'] = 0;
$this->user->save();
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => $this->userGroupA->id]
))
->assertJsonFragment([
'group_id' => $this->userGroupA->id,
]);
}
#[Test]
public function test_store_with_assignement_to_missing_groupid_returns_validation_error()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => 9999999]
))
->assertJsonValidationErrorFor('group_id');
}
#[Test]
public function test_store_with_assignement_to_null_groupid_does_not_assign_account_to_group()
{
// Set the default group to No group
$this->user['preferences->defaultGroup'] = 0;
$this->user->save();
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => null]
))
->assertJsonFragment([
'group_id' => null,
]);
}
#[Test]
public function test_store_with_assignement_to_null_groupid_is_overriden_by_specific_default_group()
{
// Set the default group to a specific group
$this->user['preferences->defaultGroup'] = $this->userGroupA->id;
$this->user->save();
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => null]
))
->assertJsonFragment([
'group_id' => $this->user->preferences['defaultGroup'],
]);
}
#[Test]
public function test_store_with_assignement_to_zero_groupid_overrides_specific_default_group()
{
// Set the default group to a specific group
$this->user['preferences->defaultGroup'] = $this->userGroupA->id;
$this->user->save();
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => 0]
))
->assertJsonFragment([
'group_id' => null,
]);
}
#[Test]
public function test_store_with_assignement_to_provided_groupid_overrides_specific_default_group()
{
// Set the default group to a specific group
$this->user['preferences->defaultGroup'] = $this->userGroupA->id;
$this->user->save();
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => $this->userGroupB->id]
))
->assertJsonFragment([
'group_id' => $this->userGroupB->id,
]);
}
#[Test]
public function test_store_assigns_created_account_when_default_group_is_a_specific_one()
{
@ -529,7 +625,7 @@ public function test_store_assigns_created_account_when_default_group_is_a_speci
'uri' => OtpTestData::TOTP_SHORT_URI,
])
->assertJsonFragment([
'group_id' => $this->userGroupA->id,
'group_id' => $this->user->preferences['defaultGroup'],
]);
}
@ -547,7 +643,7 @@ public function test_store_assigns_created_account_when_default_group_is_the_act
'uri' => OtpTestData::TOTP_SHORT_URI,
])
->assertJsonFragment([
'group_id' => $this->userGroupA->id,
'group_id' => $this->user->preferences['activeGroup'],
]);
}
@ -609,6 +705,68 @@ public function test_update_missing_twofaccount_returns_not_found()
->assertNotFound();
}
#[Test]
public function test_update_with_assignement_to_null_group_returns_success_with_updated_resource()
{
$this->assertNotEquals(null, $this->twofaccountA->group_id);
$response = $this->actingAs($this->user, 'api-guard')
->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => null]
))
->assertOk()
->assertJsonFragment([
'group_id' => null
])
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
}
#[Test]
public function test_update_with_assignement_to_zero_group_returns_success_with_updated_resource()
{
$this->assertNotEquals(null, $this->twofaccountA->group_id);
$response = $this->actingAs($this->user, 'api-guard')
->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => 0]
))
->assertOk()
->assertJsonFragment([
'group_id' => null
])
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
}
#[Test]
public function test_update_with_assignement_to_new_groupid_returns_success_with_updated_resource()
{
$this->assertEquals($this->userGroupA->id, $this->twofaccountA->group_id);
$response = $this->actingAs($this->user, 'api-guard')
->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => $this->userGroupB->id]
))
->assertOk()
->assertJsonFragment([
'group_id' => $this->userGroupB->id
])
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
}
#[Test]
public function test_update_with_assignement_to_missing_groupid_returns_validation_error()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
['group_id' => 9999999]
))
->assertJsonValidationErrorFor('group_id');
}
#[Test]
public function test_update_twofaccount_with_invalid_data_returns_validation_error()
{