diff --git a/app/Api/v1/Controllers/TwoFAccountController.php b/app/Api/v1/Controllers/TwoFAccountController.php
index f3aef246..b6bc812f 100644
--- a/app/Api/v1/Controllers/TwoFAccountController.php
+++ b/app/Api/v1/Controllers/TwoFAccountController.php
@@ -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);
diff --git a/app/Api/v1/Requests/TwoFAccountDynamicRequest.php b/app/Api/v1/Requests/TwoFAccountDynamicRequest.php
index c6914fa9..f5fbc425 100644
--- a/app/Api/v1/Requests/TwoFAccountDynamicRequest.php
+++ b/app/Api/v1/Requests/TwoFAccountDynamicRequest.php
@@ -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,
+ ]);
+ }
}
}
diff --git a/app/Api/v1/Requests/TwoFAccountStoreRequest.php b/app/Api/v1/Requests/TwoFAccountStoreRequest.php
index b6a3e9c6..279ecb32 100644
--- a/app/Api/v1/Requests/TwoFAccountStoreRequest.php
+++ b/app/Api/v1/Requests/TwoFAccountStoreRequest.php
@@ -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',
diff --git a/app/Api/v1/Requests/TwoFAccountUpdateRequest.php b/app/Api/v1/Requests/TwoFAccountUpdateRequest.php
index 473bd707..8ee4f425 100644
--- a/app/Api/v1/Requests/TwoFAccountUpdateRequest.php
+++ b/app/Api/v1/Requests/TwoFAccountUpdateRequest.php
@@ -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.
*
diff --git a/app/Services/GroupService.php b/app/Services/GroupService.php
index 5b121055..efde2d43 100644
--- a/app/Services/GroupService.php
+++ b/app/Services/GroupService.php
@@ -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);
}
diff --git a/resources/js/views/twofaccounts/CreateUpdate.vue b/resources/js/views/twofaccounts/CreateUpdate.vue
index fb92995a..cf5206f6 100644
--- a/resources/js/views/twofaccounts/CreateUpdate.vue
+++ b/resources/js/views/twofaccounts/CreateUpdate.vue
@@ -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 @@