diff --git a/app/Api/v1/Controllers/TwoFAccountController.php b/app/Api/v1/Controllers/TwoFAccountController.php index b1c2de16..c402bdc6 100644 --- a/app/Api/v1/Controllers/TwoFAccountController.php +++ b/app/Api/v1/Controllers/TwoFAccountController.php @@ -10,6 +10,7 @@ use App\Api\v1\Requests\TwoFAccountUpdateRequest; use App\Api\v1\Requests\TwoFAccountUriRequest; use App\Api\v1\Resources\TwoFAccountCollection; +use App\Api\v1\Resources\TwoFAccountExportCollection; use App\Api\v1\Resources\TwoFAccountReadResource; use App\Api\v1\Resources\TwoFAccountStoreResource; use App\Facades\Groups; @@ -70,8 +71,8 @@ public function store(TwoFAccountDynamicRequest $request) Groups::assign($twofaccount->id); return (new TwoFAccountReadResource($twofaccount->refresh())) - ->response() - ->setStatusCode(201); + ->response() + ->setStatusCode(201); } /** @@ -89,8 +90,8 @@ public function update(TwoFAccountUpdateRequest $request, TwoFAccount $twofaccou $twofaccount->save(); return (new TwoFAccountReadResource($twofaccount)) - ->response() - ->setStatusCode(200); + ->response() + ->setStatusCode(200); } /** @@ -143,6 +144,26 @@ public function preview(TwoFAccountUriRequest $request) return new TwoFAccountStoreResource($twofaccount); } + /** + * Export accounts + * + * @param \App\Api\v1\Requests\TwoFAccountBatchRequest $request + * @return TwoFAccountExportCollection|\Illuminate\Http\JsonResponse + */ + public function export(TwoFAccountBatchRequest $request) + { + $validated = $request->validated(); + + if ($this->tooManyIds($validated['ids'])) { + return response()->json([ + 'message' => 'bad request', + 'reason' => [__('errors.too_many_ids')], + ], 400); + } + + return new TwoFAccountExportCollection(TwoFAccounts::export($validated['ids'])); + } + /** * Get a One-Time Password * diff --git a/app/Api/v1/Resources/TwoFAccountExportCollection.php b/app/Api/v1/Resources/TwoFAccountExportCollection.php new file mode 100644 index 00000000..3ce03ebc --- /dev/null +++ b/app/Api/v1/Resources/TwoFAccountExportCollection.php @@ -0,0 +1,26 @@ + + */ + public function toArray($request) + { + return $this->collection; + } +} diff --git a/app/Api/v1/Resources/TwoFAccountExportResource.php b/app/Api/v1/Resources/TwoFAccountExportResource.php new file mode 100644 index 00000000..8acf1844 --- /dev/null +++ b/app/Api/v1/Resources/TwoFAccountExportResource.php @@ -0,0 +1,46 @@ + $this->otp_type, + 'account' => $this->account, + 'service' => $this->service, + 'icon' => $this->icon, + 'icon_mime' => $this->icon ? Storage::disk('icons')->mimeType((string) $this->icon) : null, + 'icon_file' => $this->icon ? base64_encode(Storage::disk('icons')->get((string) $this->icon)) : null, + 'secret' => $this->secret, + 'digits' => (int) $this->digits, + 'algorithm' => $this->algorithm, + 'period' => is_null($this->period) ? null : (int) $this->period, + 'counter' => is_null($this->counter) ? null : (int) $this->counter, + 'legacy_uri' => $this->legacy_uri, + ]; + } +} diff --git a/app/Services/TwoFAccountService.php b/app/Services/TwoFAccountService.php index 6b3886b1..bcdbb57b 100644 --- a/app/Services/TwoFAccountService.php +++ b/app/Services/TwoFAccountService.php @@ -58,6 +58,20 @@ public function migrate(string $migrationPayload) : Collection return self::markAsDuplicate($twofaccounts); } + /** + * Export one or more twofaccounts + * + * @param int|array|string $ids twofaccount ids to delete + * @return \Illuminate\Support\Collection The converted accounts + */ + public static function export($ids) : Collection + { + $ids = self::commaSeparatedToArray($ids); + $twofaccounts = TwoFAccount::whereIn('id', $ids)->get(); + + return $twofaccounts; + } + /** * Delete one or more twofaccounts * diff --git a/package-lock.json b/package-lock.json index caca4d86..bcc45836 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "bulma": "^0.9.3", "bulma-checkradio": "^2.1.2", "bulma-switch": "^2.0.0", + "file-saver": "^2.0.5", "object-equals": "^0.3.0", "v-clipboard": "^2.2.3", "vue": "^2.6.14", @@ -4696,6 +4697,11 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/file-type": { "version": "12.4.2", "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", @@ -13330,6 +13336,11 @@ } } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "file-type": { "version": "12.4.2", "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", diff --git a/package.json b/package.json index 68f5e8e4..bcd09fea 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "bulma": "^0.9.3", "bulma-checkradio": "^2.1.2", "bulma-switch": "^2.0.0", + "file-saver": "^2.0.5", "object-equals": "^0.3.0", "v-clipboard": "^2.2.3", "vue": "^2.6.14", diff --git a/resources/js/packages/fontawesome.js b/resources/js/packages/fontawesome.js index 3bd6f683..81371f98 100644 --- a/resources/js/packages/fontawesome.js +++ b/resources/js/packages/fontawesome.js @@ -1,4 +1,4 @@ -import Vue from 'vue' +import Vue from 'vue' import { library } from '@fortawesome/fontawesome-svg-core' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' @@ -37,7 +37,8 @@ import { faEye, faEyeSlash, faExternalLinkAlt, - faCamera + faCamera, + faFileDownload } from '@fortawesome/free-solid-svg-icons' import { @@ -79,7 +80,8 @@ library.add( faEye, faEyeSlash, faExternalLinkAlt, - faCamera + faCamera, + faFileDownload ); Vue.component('font-awesome-icon', FontAwesomeIcon) \ No newline at end of file diff --git a/resources/js/views/Accounts.vue b/resources/js/views/Accounts.vue index 7dad0cc0..6d4d9178 100644 --- a/resources/js/views/Accounts.vue +++ b/resources/js/views/Accounts.vue @@ -99,20 +99,23 @@
+ +
+
+ + + +
+
- {{ $t('commons.delete') }} - -
@@ -272,6 +275,7 @@ import draggable from 'vuedraggable' import Form from './../components/Form' import objectEquals from 'object-equals' + import { saveAs } from 'file-saver'; export default { data(){ @@ -485,6 +489,20 @@ } }, + /** + * Export selected accounts + */ + exportAccounts() { + let ids = [] + this.selectedAccounts.forEach(id => ids.push(id)) + + this.axios.get('/api/v1/twofaccounts/export?ids=' + ids.join(), {responseType: 'blob'}) + .then((response) => { + var blob = new Blob([response.data], {type: "application/json;charset=utf-8"}); + saveAs.saveAs(blob, "2fauth_export.json"); + }) + }, + /** * Move accounts selected from the Edit mode to another group or withdraw them */ diff --git a/routes/api/v1.php b/routes/api/v1.php index e1ba53bd..843dd5c4 100644 --- a/routes/api/v1.php +++ b/routes/api/v1.php @@ -36,6 +36,7 @@ Route::post('twofaccounts/reorder', [TwoFAccountController::class, 'reorder'])->name('twofaccounts.reorder'); Route::post('twofaccounts/migration', [TwoFAccountController::class, 'migrate'])->name('twofaccounts.migrate'); Route::post('twofaccounts/preview', [TwoFAccountController::class, 'preview'])->name('twofaccounts.preview'); + Route::get('twofaccounts/export', [TwoFAccountController::class, 'export'])->name('twofaccounts.export'); Route::get('twofaccounts/{twofaccount}/qrcode', [QrCodeController::class, 'show'])->name('twofaccounts.show.qrcode'); Route::get('twofaccounts/count', [TwoFAccountController::class, 'count'])->name('twofaccounts.count'); Route::get('twofaccounts/{id}/otp', [TwoFAccountController::class, 'otp'])->where('id', '[0-9]+')->name('twofaccounts.show.otp');