Add export feature to the Edit mode - Complete #100

This commit is contained in:
Bubka 2022-12-13 15:57:33 +01:00
parent 2d706e61b7
commit 88195a6afb
9 changed files with 153 additions and 13 deletions

View File

@ -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
*

View File

@ -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;
}
}

View File

@ -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,
];
}
}

View File

@ -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<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
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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)

View File

@ -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
*/

View File

@ -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');