Browse Source

Add export feature to the Edit mode - Complete #100

Bubka 2 years ago
parent
commit
88195a6afb

+ 25 - 4
app/Api/v1/Controllers/TwoFAccountController.php

@@ -10,6 +10,7 @@ use App\Api\v1\Requests\TwoFAccountStoreRequest;
 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 @@ class TwoFAccountController extends Controller
         Groups::assign($twofaccount->id);
 
         return (new TwoFAccountReadResource($twofaccount->refresh()))
-                ->response()
-                ->setStatusCode(201);
+            ->response()
+            ->setStatusCode(201);
     }
 
     /**
@@ -89,8 +90,8 @@ class TwoFAccountController extends Controller
         $twofaccount->save();
 
         return (new TwoFAccountReadResource($twofaccount))
-                ->response()
-                ->setStatusCode(200);
+            ->response()
+            ->setStatusCode(200);
     }
 
     /**
@@ -143,6 +144,26 @@ class TwoFAccountController extends Controller
         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
      *

+ 26 - 0
app/Api/v1/Resources/TwoFAccountExportCollection.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Api\v1\Resources;
+
+use Illuminate\Http\Resources\Json\ResourceCollection;
+
+class TwoFAccountExportCollection extends ResourceCollection
+{
+    /**
+     * The resource that this resource collects.
+     *
+     * @var string
+     */
+    public $collects = TwoFAccountExportResource::class;
+
+    /**
+     * Transform the resource collection into an array.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Support\Collection<int|string, TwoFAccountExportResource>
+     */
+    public function toArray($request)
+    {
+        return $this->collection;
+    }
+}

+ 46 - 0
app/Api/v1/Resources/TwoFAccountExportResource.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Api\v1\Resources;
+
+use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Support\Facades\Storage;
+
+/**
+ * @property mixed $otp_type
+ * @property string $account
+ * @property string $service
+ * @property string|null $icon
+ * @property string|null $icon_file
+ * @property string $secret
+ * @property int $digits
+ * @property string $algorithm
+ * @property int|null $period
+ * @property int|null $counter
+ * @property string $legacy_uri
+ */
+class TwoFAccountExportResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return array
+     */
+    public function toArray($request)
+    {
+        return [
+            'otp_type'   => $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,
+        ];
+    }
+}

+ 14 - 0
app/Services/TwoFAccountService.php

@@ -58,6 +58,20 @@ class TwoFAccountService
         return self::markAsDuplicate($twofaccounts);
     }
 
+    /**
+     * Export one or more twofaccounts
+     *
+     * @param  int|array|string  $ids twofaccount ids to delete
+     * @return \Illuminate\Support\Collection<int, TwoFAccount> 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
      *

+ 11 - 0
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",

+ 1 - 0
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",

+ 5 - 3
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)

+ 24 - 6
resources/js/views/Accounts.vue

@@ -99,20 +99,23 @@
                             <div v-if="selectedAccounts.length > 0" class="control">
                                 <div tabindex="0" role="button" class="tag-button tag-button-link tags are-medium has-addons is-clickable" @click="showGroupSelector = true" @keyup.enter="showGroupSelector = true">
                                     <span class="tag is-dark mb-0">
-                                        {{ $t('groups.change_group') }}
-                                    </span>
-                                    <span class="tag is-link mb-0">
+                                        {{ $t('groups.change_group') }}&nbsp;&nbsp;
                                         <font-awesome-icon :icon="['fas', 'layer-group']" />
                                     </span>
                                 </div>
                             </div>
+                            <!-- export selected button -->
+                            <div v-if="selectedAccounts.length > 0" class="control">
+                                <div tabindex="0" role="button" class="tag-button tags are-medium has-addons is-clickable" @click="exportAccounts" @keyup.enter="exportAccounts">
+                                    <span class="tag is-dark mb-0">
+                                        <font-awesome-icon :icon="['fas', 'file-download']" />
+                                    </span>
+                                </div>
+                            </div>
                             <!-- delete selected button -->
                             <div v-if="selectedAccounts.length > 0" class="control">
                                 <div tabindex="0" role="button" class="tag-button tag-button-danger tags are-medium has-addons is-clickable" @click="destroyAccounts" @keyup.enter="destroyAccounts">
                                     <span class="tag is-dark mb-0">
-                                        {{ $t('commons.delete') }}
-                                    </span>
-                                    <span class="tag is-danger mb-0">
                                         <font-awesome-icon :icon="['fas', 'trash']" />
                                     </span>
                                 </div>
@@ -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
              */

+ 1 - 0
routes/api/v1.php

@@ -36,6 +36,7 @@ Route::group(['middleware' => 'auth:api-guard'], function () {
     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');