mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-11-30 12:13:32 +01:00
Merge branch 'release/1.2.0' into master
This commit is contained in:
commit
89eed65218
39
README.md
39
README.md
@ -8,6 +8,10 @@ # 2FAuth
|
|||||||
|
|
||||||
![screens](https://user-images.githubusercontent.com/858858/74479269-267a1600-4eaf-11ea-9281-415e5a54bd9f.png)
|
![screens](https://user-images.githubusercontent.com/858858/74479269-267a1600-4eaf-11ea-9281-415e5a54bd9f.png)
|
||||||
|
|
||||||
|
#### [2FAuth Demo](https://demo.2fauth.app/)
|
||||||
|
|
||||||
|
Credentials (login - password) : *demo@2fauth.app* - *demo*
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator that you can use both on mobile or desktop.
|
2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator that you can use both on mobile or desktop.
|
||||||
|
|
||||||
@ -46,9 +50,14 @@ #### Install all php dependencies
|
|||||||
```
|
```
|
||||||
Don't have `composer`? [you can get it here](https://getcomposer.org/download/)
|
Don't have `composer`? [you can get it here](https://getcomposer.org/download/)
|
||||||
|
|
||||||
#### Set your variables
|
#### Set up your database
|
||||||
In your installation directory make a copy of the `.env.example` file and rename the copy `.env`.
|
|
||||||
|
|
||||||
|
Create a database with one of the supported tools (see Requirements).
|
||||||
|
For SQLite, place the database `.sqlite` file in the `database/` folder of your 2FAuth installation.
|
||||||
|
|
||||||
|
#### Set your variables
|
||||||
|
|
||||||
|
In your installation directory make a copy of the `.env.example` file and rename the copy `.env`.
|
||||||
Edit the `.env` file and adapt the settings to your running environment (see instructions in the file)
|
Edit the `.env` file and adapt the settings to your running environment (see instructions in the file)
|
||||||
|
|
||||||
#### Prepare some stuff
|
#### Prepare some stuff
|
||||||
@ -59,17 +68,25 @@ #### Prepare some stuff
|
|||||||
php artisan config:cache
|
php artisan config:cache
|
||||||
php artisan vue-i18n:generate
|
php artisan vue-i18n:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install js dependencies
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Build
|
|
||||||
`npm run dev` or `npm run prod`
|
|
||||||
|
|
||||||
You are ready to go.
|
You are ready to go.
|
||||||
|
|
||||||
|
#### For development only
|
||||||
|
Install and build js dependencies
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update your installation
|
||||||
|
First, **backup your database**.
|
||||||
|
|
||||||
|
Then, using command line :
|
||||||
|
```
|
||||||
|
git pull
|
||||||
|
php composer.phar install
|
||||||
|
php artisan migrate
|
||||||
|
php artisan config:clear
|
||||||
|
```
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
to complete
|
to complete
|
||||||
|
@ -6,11 +6,11 @@ class Options
|
|||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a collection of options to apply
|
* Compile both default and user options
|
||||||
*
|
*
|
||||||
* @return Options collection
|
* @return Options collection or a signle
|
||||||
*/
|
*/
|
||||||
public static function get()
|
public static function get($option = null)
|
||||||
{
|
{
|
||||||
// Get a collection of user saved options
|
// Get a collection of user saved options
|
||||||
$userOptions = \Illuminate\Support\Facades\DB::table('options')->pluck('value', 'key');
|
$userOptions = \Illuminate\Support\Facades\DB::table('options')->pluck('value', 'key');
|
||||||
@ -32,7 +32,7 @@ public static function get()
|
|||||||
// fallback values for every options
|
// fallback values for every options
|
||||||
$options = collect(config('app.options'))->merge($userOptions);
|
$options = collect(config('app.options'))->merge($userOptions);
|
||||||
|
|
||||||
return $options;
|
return !is_null($option) ? $options[$option] : $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ private function customApiResponse($exception, $debug)
|
|||||||
default:
|
default:
|
||||||
$response['message'] = ($statusCode >= 500) ? 'Whoops, looks like something went wrong' : $exception->getMessage();
|
$response['message'] = ($statusCode >= 500) ? 'Whoops, looks like something went wrong' : $exception->getMessage();
|
||||||
|
|
||||||
if (env('APP_DEBUG')) {
|
if (config('app.debug')) {
|
||||||
$response['originalMessage'] = $exception->getMessage();
|
$response['originalMessage'] = $exception->getMessage();
|
||||||
$response['debug'] = $debug;
|
$response['debug'] = $debug;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
use Zxing\QrReader;
|
use Zxing\QrReader;
|
||||||
use OTPHP\TOTP;
|
use OTPHP\TOTP;
|
||||||
use OTPHP\Factory;
|
use OTPHP\Factory;
|
||||||
|
use App\Classes\Options;
|
||||||
use Assert\AssertionFailedException;
|
use Assert\AssertionFailedException;
|
||||||
use Illuminate\Http\File;
|
use Illuminate\Http\File;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -21,19 +22,30 @@ class QrCodecontroller extends Controller
|
|||||||
public function decode(Request $request)
|
public function decode(Request $request)
|
||||||
{
|
{
|
||||||
|
|
||||||
// input validation
|
if(Options::get('useBasicQrcodeReader')) {
|
||||||
$this->validate($request, [
|
|
||||||
'qrcode' => 'required|image',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// qrcode analysis
|
// input validation
|
||||||
$path = $request->file('qrcode')->store('qrcodes');
|
$this->validate($request, [
|
||||||
$qrcode = new QrReader(storage_path('app/' . $path));
|
'qrcode' => 'required|image',
|
||||||
|
]);
|
||||||
|
|
||||||
$uri = urldecode($qrcode->text());
|
// qrcode analysis
|
||||||
|
$path = $request->file('qrcode')->store('qrcodes');
|
||||||
|
$qrcode = new QrReader(storage_path('app/' . $path));
|
||||||
|
|
||||||
// delete uploaded file
|
$uri = urldecode($qrcode->text());
|
||||||
Storage::delete($path);
|
|
||||||
|
// delete uploaded file
|
||||||
|
Storage::delete($path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'uri' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uri = $request->uri;
|
||||||
|
}
|
||||||
|
|
||||||
// return the OTP object
|
// return the OTP object
|
||||||
try {
|
try {
|
||||||
|
@ -17,7 +17,7 @@ class TwoFAccountController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
return response()->json(TwoFAccount::all()->toArray());
|
return response()->json(TwoFAccount::ordered()->get()->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -62,6 +62,19 @@ public function show(TwoFAccount $twofaccount)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save new order.
|
||||||
|
*
|
||||||
|
* @param \App\TwoFAccount $twofaccount
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function reorder(Request $request)
|
||||||
|
{
|
||||||
|
TwoFAccount::setNewOrder($request->orderedIds);
|
||||||
|
return response()->json('order saved', 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a TOTP
|
* Generate a TOTP
|
||||||
*
|
*
|
||||||
|
@ -4,12 +4,18 @@
|
|||||||
|
|
||||||
use OTPHP\HOTP;
|
use OTPHP\HOTP;
|
||||||
use OTPHP\Factory;
|
use OTPHP\Factory;
|
||||||
|
use Spatie\EloquentSortable\Sortable;
|
||||||
|
use Spatie\EloquentSortable\SortableTrait;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Contracts\Encryption\DecryptException;
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
|
|
||||||
class TwoFAccount extends Model
|
class TwoFAccount extends Model implements Sortable
|
||||||
{
|
{
|
||||||
|
|
||||||
|
use SortableTrait;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* model's array form.
|
* model's array form.
|
||||||
*
|
*
|
||||||
@ -49,6 +55,17 @@ protected static function boot()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sortable settings
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $sortable = [
|
||||||
|
'order_column_name' => 'order_column',
|
||||||
|
'sort_when_creating' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Null empty icon resource has gone
|
* Null empty icon resource has gone
|
||||||
*
|
*
|
||||||
|
12
changelog.md
12
changelog.md
@ -1,3 +1,15 @@
|
|||||||
|
# Change log
|
||||||
|
|
||||||
|
## [1.2.0] - 2020-09-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- QR Code scan using live stream when a camera is detected. Previous QR Code scanner remains available as fallback method or can be forced in Settings.
|
||||||
|
- New alternative layouts: List or Grid
|
||||||
|
- Accounts can be reordered
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Notification banner (when saving settings) now has a fixed position
|
||||||
|
|
||||||
## [1.1.0] - 2020-03-23
|
## [1.1.0] - 2020-03-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -10,11 +10,13 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
"appstract/laravel-options": "^3.0",
|
"appstract/laravel-options": "^3.0",
|
||||||
|
"doctrine/dbal": "^2.10",
|
||||||
"fideloper/proxy": "^4.0",
|
"fideloper/proxy": "^4.0",
|
||||||
"khanamiryan/qrcode-detector-decoder": "^1.0",
|
"khanamiryan/qrcode-detector-decoder": "^1.0",
|
||||||
"laravel/framework": "5.8.*",
|
"laravel/framework": "5.8.*",
|
||||||
"laravel/passport": "^7.2",
|
"laravel/passport": "^7.2",
|
||||||
"laravel/tinker": "^1.0",
|
"laravel/tinker": "^1.0",
|
||||||
|
"spatie/eloquent-sortable": "^3.8",
|
||||||
"spomky-labs/otphp": "^10.0"
|
"spomky-labs/otphp": "^10.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
1458
composer.lock
generated
1458
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'version' => '1.1.0',
|
'version' => '1.2.0',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -35,6 +35,8 @@
|
|||||||
'isDemoApp' => env('IS_DEMO_APP', false),
|
'isDemoApp' => env('IS_DEMO_APP', false),
|
||||||
'showTokenAsDot' => false,
|
'showTokenAsDot' => false,
|
||||||
'closeTokenOnCopy' => false,
|
'closeTokenOnCopy' => false,
|
||||||
|
'useBasicQrcodeReader' => false,
|
||||||
|
'displayMode' => 'list',
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\TwoFAccount;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddOrderColumnToTwofaccountsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('twofaccounts', function (Blueprint $table) {
|
||||||
|
$table->integer('order_column')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The primary index is used to set a default value for the newly
|
||||||
|
// created order_column
|
||||||
|
foreach (TwoFAccount::get() as $twofaccount) {
|
||||||
|
$twofaccount->order_column = $twofaccount->id;
|
||||||
|
$twofaccount->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('twofaccounts', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('order_column');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
4314
package-lock.json
generated
4314
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -10,32 +10,32 @@
|
|||||||
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
|
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios": "^0.19.2",
|
|
||||||
"bootstrap": "^4.4.1",
|
|
||||||
"bulma": "^0.8.0",
|
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jquery": "^3.2",
|
"laravel-mix": "^5.0.5",
|
||||||
"laravel-mix": "^4.1.4",
|
"lodash": "^4.17.20",
|
||||||
"lodash": "^4.17.15",
|
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"resolve-url-loader": "^2.3.1",
|
"resolve-url-loader": "^2.3.1",
|
||||||
"sass": "^1.26.3",
|
"sass": "^1.26.10",
|
||||||
"sass-loader": "^7.3.1",
|
"sass-loader": "^7.3.1",
|
||||||
"vue": "^2.6.11",
|
"vue-template-compiler": "^2.6.12"
|
||||||
"vue-template-compiler": "^2.6.11"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.27",
|
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.12.1",
|
"@fortawesome/free-brands-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.12.1",
|
"@fortawesome/free-regular-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.9",
|
"@fortawesome/vue-fontawesome": "^0.1.10",
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"bulma": "^0.8.2",
|
||||||
"bulma-checkradio": "^1.1.1",
|
"bulma-checkradio": "^1.1.1",
|
||||||
"bulma-switch": "^2.0.0",
|
"bulma-switch": "^2.0.0",
|
||||||
"v-clipboard": "^2.2.2",
|
"v-clipboard": "^2.2.3",
|
||||||
|
"vue": "^2.6.12",
|
||||||
"vue-axios": "^2.1.5",
|
"vue-axios": "^2.1.5",
|
||||||
"vue-i18n": "^8.15.5",
|
"vue-i18n": "^8.21.1",
|
||||||
"vue-pull-refresh": "^0.2.7",
|
"vue-pull-refresh": "^0.2.7",
|
||||||
"vue-router": "^3.1.6"
|
"vue-qrcode-reader": "^2.3.13",
|
||||||
|
"vue-router": "^3.4.3",
|
||||||
|
"vuedraggable": "^2.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
public/css/app.css
vendored
4
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
3
public/js/app.js
vendored
3
public/js/app.js
vendored
File diff suppressed because one or more lines are too long
6
public/js/app.js.LICENSE.txt
Normal file
6
public/js/app.js.LICENSE.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/**!
|
||||||
|
* Sortable 1.10.2
|
||||||
|
* @author RubaXa <trash@rubaxa.org>
|
||||||
|
* @author owenm <owen23355@gmail.com>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
2
public/js/locales.js
vendored
2
public/js/locales.js
vendored
File diff suppressed because one or more lines are too long
3
public/js/vendor.js
vendored
3
public/js/vendor.js
vendored
File diff suppressed because one or more lines are too long
18
public/js/vendor.js.LICENSE.txt
Normal file
18
public/js/vendor.js.LICENSE.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*!
|
||||||
|
* Vue.js v2.6.12
|
||||||
|
* (c) 2014-2020 Evan You
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* vue-i18n v8.21.1
|
||||||
|
* (c) 2020 kazuya kawaguchi
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**!
|
||||||
|
* Sortable 1.10.2
|
||||||
|
* @author RubaXa <trash@rubaxa.org>
|
||||||
|
* @author owenm <owen23355@gmail.com>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250",
|
"/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250",
|
||||||
"/js/app.js": "/js/app.js?id=80de66960444f655531d",
|
"/js/app.js": "/js/app.js?id=06dde914944b58fdffb5",
|
||||||
"/css/app.css": "/css/app.css?id=46032e0e6368c4c5cfcc",
|
"/css/app.css": "/css/app.css?id=6454f3aa078ad9bd4e25",
|
||||||
"/js/locales.js": "/js/locales.js?id=63696a94f3a7b1fe09d6",
|
"/js/locales.js": "/js/locales.js?id=79bb717b28f53a8b3b72",
|
||||||
"/js/vendor.js": "/js/vendor.js?id=1cd1d953565ebfcb7231"
|
"/js/vendor.js": "/js/vendor.js?id=5ba3d19fe9d922bf8630"
|
||||||
}
|
}
|
||||||
|
1
resources/js/app.js
vendored
1
resources/js/app.js
vendored
@ -4,6 +4,7 @@ import api from './api'
|
|||||||
import i18n from './langs/i18n'
|
import i18n from './langs/i18n'
|
||||||
import FontAwesome from './packages/fontawesome'
|
import FontAwesome from './packages/fontawesome'
|
||||||
import Clipboard from './packages/clipboard'
|
import Clipboard from './packages/clipboard'
|
||||||
|
import QrcodeReader from './packages/qrcodeReader'
|
||||||
import App from './components/App'
|
import App from './components/App'
|
||||||
|
|
||||||
import './components'
|
import './components'
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
<h1 class="title" v-html="title" v-if="title"></h1>
|
<h1 class="title" v-html="title" v-if="title"></h1>
|
||||||
<slot />
|
<slot />
|
||||||
<p v-if="showTag">
|
<p v-if="showTag">
|
||||||
<notification :message="fail" type="is-danger" v-if="fail" />
|
<notification :message="fail" type="is-danger" :isFixed="hasFixedNotification" v-if="fail" />
|
||||||
<notification :message="success" type="is-success" v-if="success" />
|
<notification :message="success" type="is-success" :isFixed="hasFixedNotification" v-if="success" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -42,6 +42,11 @@
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasFixedNotification: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="notification" :class="type" v-if="show">
|
<div class="notification" :class="[type, isFixed ? 'is-fixed' : '']" v-if="show">
|
||||||
<button class="delete" v-if="isDeletable" @click="close"></button>
|
<button class="delete" v-if="isDeletable" @click="close"></button>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
@ -30,6 +30,11 @@
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isFixed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
226
resources/js/components/QuickUploader.vue
Normal file
226
resources/js/components/QuickUploader.vue
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<template>
|
||||||
|
<div id="quick-uploader">
|
||||||
|
<!-- static landing UI -->
|
||||||
|
<div v-show="!(showStream && canStream)" class="container has-text-centered">
|
||||||
|
<div class="columns quick-uploader">
|
||||||
|
<!-- trailer phrase that invite to add an account -->
|
||||||
|
<div class="column is-full quick-uploader-header" :class="{ 'is-invisible' : !showTrailer }">
|
||||||
|
{{ $t('twofaccounts.no_account_here') }}<br>
|
||||||
|
{{ $t('twofaccounts.add_first_account') }}
|
||||||
|
</div>
|
||||||
|
<!-- add button -->
|
||||||
|
<div class="column is-full quick-uploader-button" >
|
||||||
|
<div class="quick-uploader-centerer">
|
||||||
|
<!-- scan button that launch camera stream -->
|
||||||
|
<label v-if="canStream" class="button is-link is-medium is-rounded is-focused" @click="enableStream()">
|
||||||
|
{{ $t('twofaccounts.forms.scan_qrcode') }}
|
||||||
|
</label>
|
||||||
|
<!-- or classic input field -->
|
||||||
|
<form v-else @submit.prevent="createAccount" @keydown="form.onKeydown($event)">
|
||||||
|
<label :class="{'is-loading' : form.isBusy}" class="button is-link is-medium is-rounded is-focused">
|
||||||
|
<input v-if="$root.appSettings.useBasicQrcodeReader" class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
|
||||||
|
<qrcode-capture v-else @decode="uploadQrcode" class="file-input" ref="qrcodeInput" />
|
||||||
|
{{ $t('twofaccounts.forms.use_qrcode.val') }}
|
||||||
|
</label>
|
||||||
|
<field-error :form="form" field="qrcode" />
|
||||||
|
<field-error :form="form" field="uri" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Fallback link to classic form -->
|
||||||
|
<div class="column is-full quick-uploader-footer">
|
||||||
|
<router-link :to="{ name: 'create' }" class="is-link">{{ $t('twofaccounts.use_full_form') }}</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-if="showError" class="column is-full quick-uploader-footer">
|
||||||
|
<notification :message="errorText" :isDeletable="false" type="is-danger" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- camera stream fullscreen scanner -->
|
||||||
|
<div v-show="showStream && canStream">
|
||||||
|
<div class="fullscreen-alert has-text-centered">
|
||||||
|
<span class="is-size-4 has-text-light">
|
||||||
|
<font-awesome-icon :icon="['fas', 'spinner']" size="2x" spin />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-streamer">
|
||||||
|
<qrcode-stream @decode="uploadQrcode" @init="onStreamerInit" :camera="camera" />
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-footer">
|
||||||
|
<!-- Cancel button -->
|
||||||
|
<label class="button is-large is-warning is-rounded" @click="disableStream()">
|
||||||
|
{{ $t('commons.cancel') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import Form from './Form'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'QuickUploader',
|
||||||
|
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
form: new Form({
|
||||||
|
qrcode: null,
|
||||||
|
uri: '',
|
||||||
|
}),
|
||||||
|
errorName: '',
|
||||||
|
errorText: '',
|
||||||
|
showStream: false,
|
||||||
|
canStream: true,
|
||||||
|
camera: 'auto',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
debugMode: function() {
|
||||||
|
return process.env.NODE_ENV
|
||||||
|
},
|
||||||
|
|
||||||
|
showError: function() {
|
||||||
|
return this.debugMode == 'development' && this.errorName == 'NotAllowedError'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
showTrailer: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
directStreaming: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if( this.$root.appSettings.useBasicQrcodeReader ) {
|
||||||
|
// User has set the basic QR code reader so we disable the modern one
|
||||||
|
this.canStream = this.showStream = false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if( this.directStreaming ) {
|
||||||
|
this.enableStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.form.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
async enableStream() {
|
||||||
|
|
||||||
|
this.$parent.$emit('initStreaming')
|
||||||
|
|
||||||
|
this.camera = 'auto'
|
||||||
|
this.showStream = true
|
||||||
|
|
||||||
|
console.log('stream enabled')
|
||||||
|
},
|
||||||
|
|
||||||
|
async disableStream() {
|
||||||
|
|
||||||
|
this.camera = 'off'
|
||||||
|
this.showStream = false
|
||||||
|
|
||||||
|
this.$parent.$emit('stopStreaming')
|
||||||
|
|
||||||
|
console.log('stream disabled')
|
||||||
|
},
|
||||||
|
|
||||||
|
async onStreamerInit (promise) {
|
||||||
|
|
||||||
|
this.errorText = ''
|
||||||
|
this.errorName = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
|
||||||
|
this.errorName = error.name
|
||||||
|
|
||||||
|
if (error.name === 'NotAllowedError') {
|
||||||
|
this.errorText = this.$t('twofaccounts.stream.need_grant_permission')
|
||||||
|
|
||||||
|
} else if (error.name === 'NotReadableError') {
|
||||||
|
this.errorText = this.$t('twofaccounts.stream.not_readable')
|
||||||
|
|
||||||
|
} else if (error.name === 'NotFoundError') {
|
||||||
|
this.errorText = this.$t('twofaccounts.stream.no_cam_on_device')
|
||||||
|
|
||||||
|
} else if (error.name === 'NotSupportedError' || error.name === 'InsecureContextError') {
|
||||||
|
this.errorText = this.$t('twofaccounts.stream.secured_context_required')
|
||||||
|
|
||||||
|
} else if (error.name === 'OverconstrainedError') {
|
||||||
|
this.errorText = this.$t('twofaccounts.stream.camera_not_suitable')
|
||||||
|
|
||||||
|
} else if (error.name === 'StreamApiNotSupportedError') {
|
||||||
|
this.errorText = this.$t('twofaccounts.stream.stream_api_not_supported')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setUploader()
|
||||||
|
},
|
||||||
|
|
||||||
|
setUploader() {
|
||||||
|
|
||||||
|
if( this.errorName ) {
|
||||||
|
this.canStream = false
|
||||||
|
this.$parent.$emit('cannotStream')
|
||||||
|
|
||||||
|
console.log('fail to stream : ' + this.errorText)
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !this.errorName && !this.showStream ) {
|
||||||
|
this.camera = 'off'
|
||||||
|
|
||||||
|
console.log('stream stopped')
|
||||||
|
}
|
||||||
|
|
||||||
|
if( this.canStream && this.showStream) {
|
||||||
|
this.$parent.$emit('startStreaming')
|
||||||
|
|
||||||
|
console.log('stream started')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadQrcode(event) {
|
||||||
|
|
||||||
|
var response
|
||||||
|
|
||||||
|
if(this.$root.appSettings.useBasicQrcodeReader) {
|
||||||
|
let imgdata = new FormData();
|
||||||
|
imgdata.append('qrcode', this.$refs.qrcodeInput.files[0]);
|
||||||
|
|
||||||
|
response = await this.form.upload('/api/qrcode/decode', imgdata)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We post the decoded URI instead of an image to decode
|
||||||
|
this.form.uri = event
|
||||||
|
|
||||||
|
if( !this.form.uri ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await this.form.post('/api/qrcode/decode')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router.push({ name: 'create', params: { qrAccount: response.data } });
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
46
resources/js/langs/locales.js
vendored
46
resources/js/langs/locales.js
vendored
@ -97,7 +97,17 @@ export default {
|
|||||||
"close_token_on_copy": {
|
"close_token_on_copy": {
|
||||||
"label": "Close token after copy",
|
"label": "Close token after copy",
|
||||||
"help": "Automatically close the popup showing the generated token after it has been copied"
|
"help": "Automatically close the popup showing the generated token after it has been copied"
|
||||||
}
|
},
|
||||||
|
"use_basic_qrcode_reader": {
|
||||||
|
"label": "Use basic qrcode reader",
|
||||||
|
"help": "If you experiences issues when capturing qrCodes enables this option to switch to a more basic but more reliable qrcode reader"
|
||||||
|
},
|
||||||
|
"display_mode": {
|
||||||
|
"label": "Display mode",
|
||||||
|
"help": "Choose whether you want accounts to be displayed as a list or as a grid"
|
||||||
|
},
|
||||||
|
"grid": "Grid",
|
||||||
|
"list": "List"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"twofaccounts": {
|
"twofaccounts": {
|
||||||
@ -107,7 +117,7 @@ export default {
|
|||||||
"new": "New",
|
"new": "New",
|
||||||
"no_account_here": "No 2FA here!",
|
"no_account_here": "No 2FA here!",
|
||||||
"add_first_account": "Add your first account",
|
"add_first_account": "Add your first account",
|
||||||
"use_full_form": "Use the full form",
|
"use_full_form": "Or use the full form",
|
||||||
"add_one": "Add one",
|
"add_one": "Add one",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
@ -122,6 +132,7 @@ export default {
|
|||||||
"edit_account": "Edit account",
|
"edit_account": "Edit account",
|
||||||
"otp_uri": "OTP Uri",
|
"otp_uri": "OTP Uri",
|
||||||
"hotp_counter": "HOTP Counter",
|
"hotp_counter": "HOTP Counter",
|
||||||
|
"scan_qrcode": "Scan a qrcode",
|
||||||
"use_qrcode": {
|
"use_qrcode": {
|
||||||
"val": "Use a qrcode",
|
"val": "Use a qrcode",
|
||||||
"title": "Use a QR code to fill the form magically"
|
"title": "Use a QR code to fill the form magically"
|
||||||
@ -139,6 +150,14 @@ export default {
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"test": "Test"
|
"test": "Test"
|
||||||
},
|
},
|
||||||
|
"stream": {
|
||||||
|
"need_grant_permission": "You need to grant camera access permission",
|
||||||
|
"not_readable": "Fail to load scanner. Is the camera already in use?",
|
||||||
|
"no_cam_on_device": "No camera on this device",
|
||||||
|
"secured_context_required": "Secure context required (HTTPS or localhost)",
|
||||||
|
"camera_not_suitable": "Installed cameras are not suitable",
|
||||||
|
"stream_api_not_supported": "Stream API is not supported in this browser"
|
||||||
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"delete": "Are you sure you want to delete this account?",
|
"delete": "Are you sure you want to delete this account?",
|
||||||
"cancel": "The account will be lost. Are you sure?"
|
"cancel": "The account will be lost. Are you sure?"
|
||||||
@ -366,7 +385,17 @@ export default {
|
|||||||
"close_token_on_copy": {
|
"close_token_on_copy": {
|
||||||
"label": "Ne plus afficher les codes copiés",
|
"label": "Ne plus afficher les codes copiés",
|
||||||
"help": "Ferme automatiquement le popup affichant le code généré dès que ce dernier a été copié."
|
"help": "Ferme automatiquement le popup affichant le code généré dès que ce dernier a été copié."
|
||||||
}
|
},
|
||||||
|
"use_basic_qrcode_reader": {
|
||||||
|
"label": "Utiliser le lecteur de qrcode basique",
|
||||||
|
"help": "Si vous rencontrez des problèmes lors de la lecture des qrCodes activez cette option pour utiliser un lecteur de qrcode moins évolué mais plus largement compatible"
|
||||||
|
},
|
||||||
|
"display_mode": {
|
||||||
|
"label": "Mode d'affichage",
|
||||||
|
"help": "Change le mode d'affichage des comptes, soit sous forme de liste, soit sous forme de grille"
|
||||||
|
},
|
||||||
|
"grid": "Grille",
|
||||||
|
"list": "Liste"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"twofaccounts": {
|
"twofaccounts": {
|
||||||
@ -376,7 +405,7 @@ export default {
|
|||||||
"new": "Nouveau",
|
"new": "Nouveau",
|
||||||
"no_account_here": "Aucun compte 2FA !",
|
"no_account_here": "Aucun compte 2FA !",
|
||||||
"add_first_account": "Ajouter votre premier compte",
|
"add_first_account": "Ajouter votre premier compte",
|
||||||
"use_full_form": "Utiliser le formulaire détaillé",
|
"use_full_form": "Ou utiliser le formulaire détaillé",
|
||||||
"add_one": "Add one",
|
"add_one": "Add one",
|
||||||
"manage": "Gérer",
|
"manage": "Gérer",
|
||||||
"done": "Terminé",
|
"done": "Terminé",
|
||||||
@ -391,6 +420,7 @@ export default {
|
|||||||
"edit_account": "Modifier le compte",
|
"edit_account": "Modifier le compte",
|
||||||
"otp_uri": "OTP Uri",
|
"otp_uri": "OTP Uri",
|
||||||
"hotp_counter": "Compteur HOTP",
|
"hotp_counter": "Compteur HOTP",
|
||||||
|
"scan_qrcode": "Scanner un QR code",
|
||||||
"use_qrcode": {
|
"use_qrcode": {
|
||||||
"val": "Utiliser un QR code",
|
"val": "Utiliser un QR code",
|
||||||
"title": "Utiliser un QR code pour renseigner le formulaire d'un seul coup d'un seul"
|
"title": "Utiliser un QR code pour renseigner le formulaire d'un seul coup d'un seul"
|
||||||
@ -408,6 +438,14 @@ export default {
|
|||||||
"save": "Enregistrer",
|
"save": "Enregistrer",
|
||||||
"test": "Tester"
|
"test": "Tester"
|
||||||
},
|
},
|
||||||
|
"stream": {
|
||||||
|
"need_grant_permission": "Vous devez autoriser l'utilisation de votre caméra",
|
||||||
|
"not_readable": "Le scanner ne se charge pas. La caméra est-elle déjà utilisée ?",
|
||||||
|
"no_cam_on_device": "Votre équipement ne dispose pas de caméra",
|
||||||
|
"secured_context_required": "Contexte sécurisé requis (HTTPS ou localhost)",
|
||||||
|
"camera_not_suitable": "Votre équipement ne dispose pas d'une caméra adaptée",
|
||||||
|
"stream_api_not_supported": "L'API Stream n'est pas supportée par votre navigateur"
|
||||||
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"delete": "Etes-vous sûrs de vouloir supprimer le compte ?",
|
"delete": "Etes-vous sûrs de vouloir supprimer le compte ?",
|
||||||
"cancel": "Les données seront perdues, êtes-vous sûrs ?"
|
"cancel": "Les données seront perdues, êtes-vous sûrs ?"
|
||||||
|
8
resources/js/packages/fontawesome.js
vendored
8
resources/js/packages/fontawesome.js
vendored
@ -14,7 +14,9 @@ import {
|
|||||||
faLock,
|
faLock,
|
||||||
faLockOpen,
|
faLockOpen,
|
||||||
faSearch,
|
faSearch,
|
||||||
faEllipsisH
|
faEllipsisH,
|
||||||
|
faBars,
|
||||||
|
faSpinner
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
@ -27,7 +29,9 @@ library.add(
|
|||||||
faLock,
|
faLock,
|
||||||
faLockOpen,
|
faLockOpen,
|
||||||
faSearch,
|
faSearch,
|
||||||
faEllipsisH
|
faEllipsisH,
|
||||||
|
faBars,
|
||||||
|
faSpinner
|
||||||
);
|
);
|
||||||
|
|
||||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
4
resources/js/packages/qrcodeReader.js
vendored
Normal file
4
resources/js/packages/qrcodeReader.js
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import QrcodeReader from 'vue-qrcode-reader'
|
||||||
|
|
||||||
|
Vue.use(QrcodeReader)
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<!-- show accounts list -->
|
||||||
<div class="container" v-if="this.showAccounts">
|
<div class="container" v-if="this.showAccounts">
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<div class="columns is-gapless is-mobile is-centered">
|
<div class="columns is-gapless is-mobile is-centered">
|
||||||
@ -26,76 +27,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- accounts -->
|
<!-- accounts -->
|
||||||
<vue-pull-refresh :on-refresh="onRefresh" :config="{
|
<!-- <vue-pull-refresh :on-refresh="onRefresh" :config="{
|
||||||
errorLabel: 'error',
|
errorLabel: 'error',
|
||||||
startLabel: '',
|
startLabel: '',
|
||||||
readyLabel: '',
|
readyLabel: '',
|
||||||
loadingLabel: 'refreshing'
|
loadingLabel: 'refreshing'
|
||||||
}" class="accounts columns is-multiline is-centered">
|
}" > -->
|
||||||
<div class="tfa column is-narrow has-text-white" v-for="account in filteredAccounts">
|
<draggable v-model="filteredAccounts" @start="drag = true" @end="saveOrder" ghost-class="ghost" handle=".tfa-dots" animation="200" class="accounts">
|
||||||
<div class="tfa-container">
|
<transition-group class="columns is-multiline" :class="{ 'is-centered': $root.appSettings.displayMode === 'grid' }" type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<transition name="slideCheckbox">
|
<div :class="[$root.appSettings.displayMode === 'grid' ? 'tfa-grid' : 'tfa-list']" class="column is-narrow has-text-white" v-for="account in filteredAccounts" :key="account.id">
|
||||||
<div class="tfa-checkbox" v-if="editMode">
|
<div class="tfa-container">
|
||||||
<div class="field">
|
<transition name="slideCheckbox">
|
||||||
<input class="is-checkradio is-small is-white" :id="'ckb_' + account.id" :value="account.id" type="checkbox" :name="'ckb_' + account.id" v-model="selectedAccounts">
|
<div class="tfa-checkbox" v-if="editMode">
|
||||||
<label :for="'ckb_' + account.id"></label>
|
<div class="field">
|
||||||
</div>
|
<input class="is-checkradio is-small is-white" :id="'ckb_' + account.id" :value="account.id" type="checkbox" :name="'ckb_' + account.id" v-model="selectedAccounts">
|
||||||
</div>
|
<label :for="'ckb_' + account.id"></label>
|
||||||
</transition>
|
</div>
|
||||||
<div class="tfa-content is-size-3 is-size-4-mobile" @click.stop="showAccount(account)">
|
</div>
|
||||||
<div class="tfa-text has-ellipsis">
|
</transition>
|
||||||
<img :src="'/storage/icons/' + account.icon" v-if="account.icon">
|
<div class="tfa-content is-size-3 is-size-4-mobile" @click.stop="showAccount(account)">
|
||||||
{{ account.service }}
|
<div class="tfa-text has-ellipsis">
|
||||||
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey ">{{ account.account }}</span>
|
<img :src="'/storage/icons/' + account.icon" v-if="account.icon">
|
||||||
|
{{ account.service }}
|
||||||
|
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey ">{{ account.account }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<transition name="fadeInOut">
|
||||||
|
<div class="tfa-edit has-text-grey" v-if="editMode">
|
||||||
|
<router-link :to="{ name: 'edit', params: { twofaccountId: account.id }}" class="tag is-dark is-rounded">
|
||||||
|
{{ $t('commons.edit') }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<transition name="fadeInOut">
|
||||||
|
<div class="tfa-dots has-text-grey" v-if="editMode">
|
||||||
|
<font-awesome-icon :icon="['fas', 'bars']" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<transition name="fadeInOut">
|
</transition-group>
|
||||||
<div class="tfa-dots has-text-grey" v-if="editMode">
|
</draggable>
|
||||||
<router-link :to="{ name: 'edit', params: { twofaccountId: account.id }}" class="tag is-dark is-rounded">
|
<!-- </vue-pull-refresh> -->
|
||||||
{{ $t('commons.edit') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</vue-pull-refresh>
|
|
||||||
</div>
|
|
||||||
<!-- No account -->
|
|
||||||
<div class="container has-text-centered" v-show="showQuickForm">
|
|
||||||
<div class="columns is-mobile" :class="{ 'is-invisible' : this.accounts.length > 0}">
|
|
||||||
<div class="column quickform-header">
|
|
||||||
{{ $t('twofaccounts.no_account_here') }}<br>
|
|
||||||
{{ $t('twofaccounts.add_first_account') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<form @submit.prevent="createAccount" @keydown="form.onKeydown($event)">
|
|
||||||
<div class="columns is-mobile no-account is-vcentered">
|
|
||||||
<div class="column has-text-centered">
|
|
||||||
<label :class="{'is-loading' : form.isBusy}" class="button is-link is-medium is-rounded is-focused">
|
|
||||||
<input class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
|
|
||||||
{{ $t('twofaccounts.forms.use_qrcode.val') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<field-error :form="form" field="qrcode" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="columns is-mobile">
|
|
||||||
<div class="column quickform-footer">
|
|
||||||
<router-link :to="{ name: 'create' }" class="is-link">{{ $t('twofaccounts.use_full_form') }}</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Show uploader (because no account) -->
|
||||||
|
<quick-uploader v-if="showUploader" :directStreaming="accounts.length > 0" :showTrailer="accounts.length === 0" ref="QuickUploader"></quick-uploader>
|
||||||
<!-- modal -->
|
<!-- modal -->
|
||||||
<modal v-model="ShowTwofaccountInModal">
|
<modal v-model="showTwofaccountInModal">
|
||||||
<twofaccount-show ref="TwofaccountShow" ></twofaccount-show>
|
<twofaccount-show ref="TwofaccountShow" ></twofaccount-show>
|
||||||
</modal>
|
</modal>
|
||||||
<!-- footer -->
|
<!-- footer -->
|
||||||
<vue-footer :showButtons="this.accounts.length > 0">
|
<vue-footer v-if="showFooter" :showButtons="accounts.length > 0">
|
||||||
<!-- New item buttons -->
|
<!-- New item buttons -->
|
||||||
<p class="control" v-if="!showQuickForm && !editMode">
|
<p class="control" v-if="!showUploader && !editMode">
|
||||||
<a class="button is-link is-rounded is-focus" @click="showQuickForm = true">
|
<a class="button is-link is-rounded is-focus" @click="showUploader = true">
|
||||||
<span>{{ $t('twofaccounts.new') }}</span>
|
<span>{{ $t('twofaccounts.new') }}</span>
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<font-awesome-icon :icon="['fas', 'qrcode']" />
|
<font-awesome-icon :icon="['fas', 'qrcode']" />
|
||||||
@ -103,11 +88,11 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<!-- Manage button -->
|
<!-- Manage button -->
|
||||||
<p class="control" v-if="!showQuickForm && !editMode">
|
<p class="control" v-if="!showUploader && !editMode">
|
||||||
<a class="button is-dark is-rounded" @click="setEditModeTo(true)">{{ $t('twofaccounts.manage') }}</a>
|
<a class="button is-dark is-rounded" @click="setEditModeTo(true)">{{ $t('twofaccounts.manage') }}</a>
|
||||||
</p>
|
</p>
|
||||||
<!-- Done button -->
|
<!-- Done button -->
|
||||||
<p class="control" v-if="!showQuickForm && editMode">
|
<p class="control" v-if="!showUploader && editMode">
|
||||||
<a class="button is-success is-rounded" @click="setEditModeTo(false)">
|
<a class="button is-success is-rounded" @click="setEditModeTo(false)">
|
||||||
<span>{{ $t('twofaccounts.done') }}</span>
|
<span>{{ $t('twofaccounts.done') }}</span>
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
@ -116,8 +101,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<!-- Cancel QuickFormButton -->
|
<!-- Cancel QuickFormButton -->
|
||||||
<p class="control" v-if="showQuickForm">
|
<p class="control" v-if="showUploader && showFooter">
|
||||||
<a class="button is-dark is-rounded" @click="cancelQuickForm">
|
<a class="button is-dark is-rounded" @click="showUploader = false">
|
||||||
{{ $t('commons.cancel') }}
|
{{ $t('commons.cancel') }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@ -130,41 +115,48 @@
|
|||||||
|
|
||||||
import Modal from '../components/Modal'
|
import Modal from '../components/Modal'
|
||||||
import TwofaccountShow from '../components/TwofaccountShow'
|
import TwofaccountShow from '../components/TwofaccountShow'
|
||||||
import Form from './../components/Form'
|
import QuickUploader from './../components/QuickUploader'
|
||||||
import vuePullRefresh from 'vue-pull-refresh';
|
// import vuePullRefresh from 'vue-pull-refresh';
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
accounts : [],
|
accounts : [],
|
||||||
selectedAccounts: [],
|
selectedAccounts: [],
|
||||||
ShowTwofaccountInModal : false,
|
showTwofaccountInModal : false,
|
||||||
search: '',
|
search: '',
|
||||||
editMode: this.InitialEditMode,
|
editMode: this.InitialEditMode,
|
||||||
showQuickForm: false,
|
showUploader: false,
|
||||||
form: new Form({
|
showFooter: true,
|
||||||
qrcode: null
|
drag: false,
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filteredAccounts() {
|
filteredAccounts: {
|
||||||
return this.accounts.filter(
|
get: function() {
|
||||||
item => {
|
return this.accounts.filter(
|
||||||
return item.service.toLowerCase().includes(this.search.toLowerCase()) || item.account.toLowerCase().includes(this.search.toLowerCase());
|
item => {
|
||||||
}
|
return item.service.toLowerCase().includes(this.search.toLowerCase()) || item.account.toLowerCase().includes(this.search.toLowerCase());
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: function(reorderedAccounts) {
|
||||||
|
this.accounts = reorderedAccounts
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showAccounts() {
|
showAccounts() {
|
||||||
return this.accounts.length > 0 && !this.showQuickForm ? true : false
|
return this.accounts.length > 0 && !this.showUploader ? true : false
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
props: ['InitialEditMode'],
|
props: ['InitialEditMode'],
|
||||||
|
|
||||||
created() {
|
mounted() {
|
||||||
|
|
||||||
this.fetchAccounts()
|
this.fetchAccounts()
|
||||||
|
|
||||||
@ -174,38 +166,35 @@
|
|||||||
this.$refs.TwofaccountShow.clearOTP()
|
this.$refs.TwofaccountShow.clearOTP()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// hide Footer when stream is on
|
||||||
|
this.$on('initStreaming', function() {
|
||||||
|
// this.showFooter = this.accounts.length > 0 ? false : true
|
||||||
|
this.showFooter = false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$on('stopStreaming', function() {
|
||||||
|
|
||||||
|
this.showUploader = this.accounts.length > 0 ? false : true
|
||||||
|
this.showFooter = true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$on('cannotStream', function() {
|
||||||
|
|
||||||
|
this.showFooter = true
|
||||||
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
TwofaccountShow,
|
TwofaccountShow,
|
||||||
'vue-pull-refresh': vuePullRefresh
|
// 'vue-pull-refresh': vuePullRefresh,
|
||||||
|
QuickUploader,
|
||||||
|
draggable,
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
onRefresh() {
|
|
||||||
var that = this
|
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
setTimeout(function () {
|
|
||||||
that.fetchAccounts()
|
|
||||||
resolve();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async uploadQrcode(event) {
|
|
||||||
|
|
||||||
let imgdata = new FormData();
|
|
||||||
imgdata.append('qrcode', this.$refs.qrcodeInput.files[0]);
|
|
||||||
|
|
||||||
const { data } = await this.form.upload('/api/qrcode/decode', imgdata)
|
|
||||||
|
|
||||||
this.$router.push({ name: 'create', params: { qrAccount: data } });
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchAccounts() {
|
fetchAccounts() {
|
||||||
this.accounts = []
|
this.accounts = []
|
||||||
this.selectedAccounts = []
|
this.selectedAccounts = []
|
||||||
@ -220,7 +209,7 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.showQuickForm = response.data.length === 0 ? true: false
|
this.showUploader = response.data.length === 0 ? true : false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -241,7 +230,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteAccount: function (id) {
|
saveOrder() {
|
||||||
|
this.drag = false
|
||||||
|
this.axios.patch('/api/twofaccounts/reorder', {orderedIds: this.accounts.map(a => a.id)})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAccount(id) {
|
||||||
if(confirm(this.$t('twofaccounts.confirm.delete'))) {
|
if(confirm(this.$t('twofaccounts.confirm.delete'))) {
|
||||||
this.axios.delete('/api/twofaccounts/' + id)
|
this.axios.delete('/api/twofaccounts/' + id)
|
||||||
|
|
||||||
@ -277,10 +271,6 @@
|
|||||||
this.$parent.showToolbar = state
|
this.$parent.showToolbar = state
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelQuickForm() {
|
|
||||||
this.form.clear()
|
|
||||||
this.showQuickForm = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeRouteEnter (to, from, next) {
|
beforeRouteEnter (to, from, next) {
|
||||||
@ -292,4 +282,15 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flip-list-move {
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
opacity: 1;
|
||||||
|
/*background: hsl(0, 0%, 21%);*/
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<form-wrapper :fail="fail" :success="success">
|
<form-wrapper :fail="fail" :success="success" :hasFixedNotification="true">
|
||||||
<div class="tags has-addons">
|
<div class="tags has-addons">
|
||||||
<span class="tag is-dark">2FAuth</span>
|
<span class="tag is-dark">2FAuth</span>
|
||||||
<span class="tag is-info">v{{ $root.appVersion }}</span>
|
<span class="tag is-info">v{{ $root.appVersion }}</span>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)">
|
<form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)">
|
||||||
<form-select :options="options" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
|
<form-select :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
|
||||||
|
<form-select :options="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
|
||||||
<form-switch :form="form" fieldName="showTokenAsDot" :label="$t('settings.forms.show_token_as_dot.label')" :help="$t('settings.forms.show_token_as_dot.help')" />
|
<form-switch :form="form" fieldName="showTokenAsDot" :label="$t('settings.forms.show_token_as_dot.label')" :help="$t('settings.forms.show_token_as_dot.help')" />
|
||||||
<form-switch :form="form" fieldName="closeTokenOnCopy" :label="$t('settings.forms.close_token_on_copy.label')" :help="$t('settings.forms.close_token_on_copy.help')" />
|
<form-switch :form="form" fieldName="closeTokenOnCopy" :label="$t('settings.forms.close_token_on_copy.label')" :help="$t('settings.forms.close_token_on_copy.help')" />
|
||||||
|
<form-switch :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
|
||||||
</form>
|
</form>
|
||||||
</form-wrapper>
|
</form-wrapper>
|
||||||
</template>
|
</template>
|
||||||
@ -25,10 +27,16 @@
|
|||||||
lang: this.$root.$i18n.locale,
|
lang: this.$root.$i18n.locale,
|
||||||
showTokenAsDot: this.$root.appSettings.showTokenAsDot,
|
showTokenAsDot: this.$root.appSettings.showTokenAsDot,
|
||||||
closeTokenOnCopy: this.$root.appSettings.closeTokenOnCopy,
|
closeTokenOnCopy: this.$root.appSettings.closeTokenOnCopy,
|
||||||
|
useBasicQrcodeReader: this.$root.appSettings.useBasicQrcodeReader,
|
||||||
|
displayMode: this.$root.appSettings.displayMode,
|
||||||
}),
|
}),
|
||||||
options: [
|
langs: [
|
||||||
{ text: this.$t('languages.en'), value: 'en' },
|
{ text: this.$t('languages.en'), value: 'en' },
|
||||||
{ text: this.$t('languages.fr'), value: 'fr' },
|
{ text: this.$t('languages.fr'), value: 'fr' },
|
||||||
|
],
|
||||||
|
layouts: [
|
||||||
|
{ text: this.$t('settings.forms.grid'), value: 'grid' },
|
||||||
|
{ text: this.$t('settings.forms.list'), value: 'list' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -35,6 +35,16 @@
|
|||||||
'label' => 'Close token after copy',
|
'label' => 'Close token after copy',
|
||||||
'help' => 'Automatically close the popup showing the generated token after it has been copied'
|
'help' => 'Automatically close the popup showing the generated token after it has been copied'
|
||||||
],
|
],
|
||||||
|
'use_basic_qrcode_reader' => [
|
||||||
|
'label' => 'Use basic qrcode reader',
|
||||||
|
'help' => 'If you experiences issues when capturing qrCodes enables this option to switch to a more basic but more reliable qrcode reader'
|
||||||
|
],
|
||||||
|
'display_mode' => [
|
||||||
|
'label' => 'Display mode',
|
||||||
|
'help' => 'Choose whether you want accounts to be displayed as a list or as a grid'
|
||||||
|
],
|
||||||
|
'grid' => 'Grid',
|
||||||
|
'list' => 'List',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
'new' => 'New',
|
'new' => 'New',
|
||||||
'no_account_here' => 'No 2FA here!',
|
'no_account_here' => 'No 2FA here!',
|
||||||
'add_first_account' => 'Add your first account',
|
'add_first_account' => 'Add your first account',
|
||||||
'use_full_form' => 'Use the full form',
|
'use_full_form' => 'Or use the full form',
|
||||||
'add_one' => 'Add one',
|
'add_one' => 'Add one',
|
||||||
'manage' => 'Manage',
|
'manage' => 'Manage',
|
||||||
'done' => 'Done',
|
'done' => 'Done',
|
||||||
@ -34,6 +34,7 @@
|
|||||||
'edit_account' => 'Edit account',
|
'edit_account' => 'Edit account',
|
||||||
'otp_uri' => 'OTP Uri',
|
'otp_uri' => 'OTP Uri',
|
||||||
'hotp_counter' => 'HOTP Counter',
|
'hotp_counter' => 'HOTP Counter',
|
||||||
|
'scan_qrcode' => 'Scan a qrcode',
|
||||||
'use_qrcode' => [
|
'use_qrcode' => [
|
||||||
'val' => 'Use a qrcode',
|
'val' => 'Use a qrcode',
|
||||||
'title' => 'Use a QR code to fill the form magically',
|
'title' => 'Use a QR code to fill the form magically',
|
||||||
@ -51,6 +52,14 @@
|
|||||||
'save' => 'Save',
|
'save' => 'Save',
|
||||||
'test' => 'Test',
|
'test' => 'Test',
|
||||||
],
|
],
|
||||||
|
'stream' => [
|
||||||
|
'need_grant_permission' => 'You need to grant camera access permission',
|
||||||
|
'not_readable' => 'Fail to load scanner. Is the camera already in use?',
|
||||||
|
'no_cam_on_device' => 'No camera on this device',
|
||||||
|
'secured_context_required' => 'Secure context required (HTTPS or localhost)',
|
||||||
|
'camera_not_suitable' => 'Installed cameras are not suitable',
|
||||||
|
'stream_api_not_supported' => 'Stream API is not supported in this browser'
|
||||||
|
],
|
||||||
'confirm' => [
|
'confirm' => [
|
||||||
'delete' => 'Are you sure you want to delete this account?',
|
'delete' => 'Are you sure you want to delete this account?',
|
||||||
'cancel' => 'The account will be lost. Are you sure?'
|
'cancel' => 'The account will be lost. Are you sure?'
|
||||||
|
@ -35,6 +35,16 @@
|
|||||||
'label' => 'Ne plus afficher les codes copiés',
|
'label' => 'Ne plus afficher les codes copiés',
|
||||||
'help' => 'Ferme automatiquement le popup affichant le code généré dès que ce dernier a été copié.'
|
'help' => 'Ferme automatiquement le popup affichant le code généré dès que ce dernier a été copié.'
|
||||||
],
|
],
|
||||||
|
'use_basic_qrcode_reader' => [
|
||||||
|
'label' => 'Utiliser le lecteur de qrcode basique',
|
||||||
|
'help' => 'Si vous rencontrez des problèmes lors de la lecture des qrCodes activez cette option pour utiliser un lecteur de qrcode moins évolué mais plus largement compatible'
|
||||||
|
],
|
||||||
|
'display_mode' => [
|
||||||
|
'label' => 'Mode d\'affichage',
|
||||||
|
'help' => 'Change le mode d\'affichage des comptes, soit sous forme de liste, soit sous forme de grille'
|
||||||
|
],
|
||||||
|
'grid' => 'Grille',
|
||||||
|
'list' => 'Liste',
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
'new' => 'Nouveau',
|
'new' => 'Nouveau',
|
||||||
'no_account_here' => 'Aucun compte 2FA !',
|
'no_account_here' => 'Aucun compte 2FA !',
|
||||||
'add_first_account' => 'Ajouter votre premier compte',
|
'add_first_account' => 'Ajouter votre premier compte',
|
||||||
'use_full_form' => 'Utiliser le formulaire détaillé',
|
'use_full_form' => 'Ou utiliser le formulaire détaillé',
|
||||||
'add_one' => 'Add one',
|
'add_one' => 'Add one',
|
||||||
'manage' => 'Gérer',
|
'manage' => 'Gérer',
|
||||||
'done' => 'Terminé',
|
'done' => 'Terminé',
|
||||||
@ -34,6 +34,7 @@
|
|||||||
'edit_account' => 'Modifier le compte',
|
'edit_account' => 'Modifier le compte',
|
||||||
'otp_uri' => 'OTP Uri',
|
'otp_uri' => 'OTP Uri',
|
||||||
'hotp_counter' => 'Compteur HOTP',
|
'hotp_counter' => 'Compteur HOTP',
|
||||||
|
'scan_qrcode' => 'Scanner un QR code',
|
||||||
'use_qrcode' => [
|
'use_qrcode' => [
|
||||||
'val' => 'Utiliser un QR code',
|
'val' => 'Utiliser un QR code',
|
||||||
'title' => 'Utiliser un QR code pour renseigner le formulaire d\'un seul coup d\'un seul'
|
'title' => 'Utiliser un QR code pour renseigner le formulaire d\'un seul coup d\'un seul'
|
||||||
@ -51,6 +52,14 @@
|
|||||||
'save' => 'Enregistrer',
|
'save' => 'Enregistrer',
|
||||||
'test' => 'Tester',
|
'test' => 'Tester',
|
||||||
],
|
],
|
||||||
|
'stream' => [
|
||||||
|
'need_grant_permission' => 'Vous devez autoriser l\'utilisation de votre caméra',
|
||||||
|
'not_readable' => 'Le scanner ne se charge pas. La caméra est-elle déjà utilisée ?',
|
||||||
|
'no_cam_on_device' => 'Votre équipement ne dispose pas de caméra',
|
||||||
|
'secured_context_required' => 'Contexte sécurisé requis (HTTPS ou localhost)',
|
||||||
|
'camera_not_suitable' => 'Votre équipement ne dispose pas d\'une caméra adaptée',
|
||||||
|
'stream_api_not_supported' => 'L\'API Stream n\'est pas supportée par votre navigateur'
|
||||||
|
],
|
||||||
'confirm' => [
|
'confirm' => [
|
||||||
'delete' => 'Etes-vous sûrs de vouloir supprimer le compte ?',
|
'delete' => 'Etes-vous sûrs de vouloir supprimer le compte ?',
|
||||||
'cancel' => 'Les données seront perdues, êtes-vous sûrs ?'
|
'cancel' => 'Les données seront perdues, êtes-vous sûrs ?'
|
||||||
|
183
resources/sass/app.scss
vendored
183
resources/sass/app.scss
vendored
@ -49,17 +49,31 @@ a:hover {
|
|||||||
background-color: hsl(0, 0%, 21%) !important;
|
background-color: hsl(0, 0%, 21%) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tfa {
|
.tfa-grid {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
|
||||||
background-color: hsl(0, 0%, 10%); /*black-bis from Bulma*/
|
background-color: hsl(0, 0%, 10%); /*black-bis from Bulma*/
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
.tfa-list {
|
||||||
.tfa {
|
text-align: inherit;
|
||||||
|
border-bottom: 1px solid hsl(0, 0%, 21%);
|
||||||
|
background-color: hsl(0, 0%, 14%); /*black-ter from Bulma*/
|
||||||
|
margin: 0 1%;
|
||||||
|
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
||||||
|
width: 31.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1217px) {
|
||||||
|
.tfa-list {
|
||||||
|
width: 48%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 769px) {
|
||||||
|
.tfa-list {
|
||||||
border-radius: unset;
|
border-radius: unset;
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
border-bottom: 1px solid hsl(0, 0%, 21%);
|
border-bottom: 1px solid hsl(0, 0%, 21%);
|
||||||
@ -67,6 +81,7 @@ a:hover {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,90 +89,104 @@ a:hover {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tfa-grid .tfa-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
.tfa-list .tfa-container {
|
||||||
.tfa-container {
|
flex-direction: row;
|
||||||
flex-direction: row;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.tfa-container > div:first-of-type {
|
|
||||||
padding: 0 0 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tfa-container > div:last-of-type {
|
|
||||||
padding: 0 0.5rem 0 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tfa-checkbox, .tfa-dots {
|
.tfa-list .tfa-container > div:first-of-type {
|
||||||
|
padding: 0 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tfa-list .tfa-container > div:last-of-type {
|
||||||
|
padding: 0 1rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tfa-grid .tfa-checkbox,
|
||||||
|
.tfa-grid .tfa-dots,
|
||||||
|
.tfa-grid .tfa-edit {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.5rem 0 0 0;
|
padding: 0.5rem 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
.tfa-list .tfa-checkbox,
|
||||||
.tfa-checkbox, .tfa-dots {
|
.tfa-list .tfa-dots,
|
||||||
display: flex;
|
.tfa-list .tfa-edit {
|
||||||
align-items: center;
|
display: flex;
|
||||||
padding: 0
|
align-items: center;
|
||||||
}
|
padding: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
.tfa-content {
|
.tfa-list .tfa-dots {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tfa-grid .tfa-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
// order: 1;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.tfa-list .tfa-content {
|
||||||
.tfa-checkbox {
|
width: 100%;
|
||||||
// order: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tfa-dots {
|
.tfa-dots {
|
||||||
// order: 3;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
.tfa-grid .is-checkradio[type="checkbox"] + label, .tfa-grid .is-checkradio[type="radio"] + label {
|
||||||
.tfa-checkbox {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tfa-content {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tfa-dots {
|
|
||||||
order: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 769px) {
|
|
||||||
.is-checkradio[type="checkbox"] + label, .is-checkradio[type="radio"] + label {
|
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.tfa-text {
|
.tfa-text {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tfa img {
|
.tfa-container img {
|
||||||
height: 0.75em;
|
height: 0.75em;
|
||||||
width: 0.75em;
|
width: 0.75em;
|
||||||
margin-right: .1em;
|
margin-right: .1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tfa span {
|
.tfa-container span {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fullscreen-streamer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-alert {
|
||||||
|
position: fixed;
|
||||||
|
top: 25vh;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-footer {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(100vh - 8rem );
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.has-ellipsis {
|
.has-ellipsis {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -285,6 +314,16 @@ footer .field.is-grouped {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification.is-fixed {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0.5rem 2.5rem 0.5rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.file .tag {
|
.file .tag {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -311,27 +350,38 @@ footer .field.is-grouped {
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickform-header {
|
.quick-uploader {
|
||||||
height: 20vh;
|
flex-direction: column
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickform-footer {
|
.quick-uploader-header {
|
||||||
padding-top: 3rem;
|
padding-top: 7vh;
|
||||||
|
padding-bottom: 7vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
margin-top: 20vh;
|
margin-top: 20vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-account {
|
.quick-uploader-button {
|
||||||
height: 256px;
|
height: 256px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-account::before {
|
.quick-uploader-centerer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 256px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-uploader-button::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
opacity: 0.05;
|
opacity: 0.05;
|
||||||
@ -367,39 +417,40 @@ footer .field.is-grouped {
|
|||||||
animation: fadeOut 500ms
|
animation: fadeOut 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideCheckbox-enter-active {
|
|
||||||
|
.tfa-grid .slideCheckbox-enter-active {
|
||||||
animation: enterFromTop 500ms
|
animation: enterFromTop 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideCheckbox-enter-active + .tfa-content {
|
.tfa-grid .slideCheckbox-enter-active + .tfa-content {
|
||||||
animation: addTopOffset 500ms
|
animation: addTopOffset 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideCheckbox-leave-active {
|
.tfa-grid .slideCheckbox-leave-active {
|
||||||
animation: leaveToTop 500ms
|
animation: leaveToTop 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideCheckbox-leave-active + .tfa-content {
|
.tfa-grid .slideCheckbox-leave-active + .tfa-content {
|
||||||
animation: removeTopOffset 500ms
|
animation: removeTopOffset 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
// @media screen and (max-width: 768px) {
|
||||||
.slideCheckbox-enter-active {
|
.tfa-list .slideCheckbox-enter-active {
|
||||||
animation: enterFromLeft 500ms
|
animation: enterFromLeft 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideCheckbox-enter-active + .tfa-content {
|
.tfa-list .slideCheckbox-enter-active + .tfa-content {
|
||||||
animation: addLeftOffset 500ms
|
animation: addLeftOffset 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideCheckbox-leave-active {
|
.tfa-list .slideCheckbox-leave-active {
|
||||||
animation: leaveToLeft 500ms
|
animation: leaveToLeft 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideCheckbox-leave-active + .tfa-content {
|
.tfa-list .slideCheckbox-leave-active + .tfa-content {
|
||||||
animation: removeLeftOffset 500ms
|
animation: removeLeftOffset 500ms
|
||||||
}
|
}
|
||||||
}
|
// }
|
||||||
|
|
||||||
/*FadeInOut*/
|
/*FadeInOut*/
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>{{ env("APP_NAME") }}</title>
|
<title>{{ config('app.name') }}</title>
|
||||||
|
|
||||||
<link rel="shortcut icon" href="{{ asset('favicon.ico') }}" />
|
<link rel="shortcut icon" href="{{ asset('favicon.ico') }}" />
|
||||||
<link rel="icon" type="image/png" href="{{ asset('favicon.png') }}" />
|
<link rel="icon" type="image/png" href="{{ asset('favicon.png') }}" />
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
Route::delete('twofaccounts/batch', 'TwoFAccountController@batchDestroy');
|
Route::delete('twofaccounts/batch', 'TwoFAccountController@batchDestroy');
|
||||||
|
Route::patch('twofaccounts/reorder', 'TwoFAccountController@reorder');
|
||||||
Route::apiResource('twofaccounts', 'TwoFAccountController');
|
Route::apiResource('twofaccounts', 'TwoFAccountController');
|
||||||
Route::post('twofaccounts/otp', 'TwoFAccountController@generateOTP')->name('twofaccounts.generateOTP');
|
Route::post('twofaccounts/otp', 'TwoFAccountController@generateOTP')->name('twofaccounts.generateOTP');
|
||||||
Route::post('qrcode/decode', 'QrCodeController@decode');
|
Route::post('qrcode/decode', 'QrCodeController@decode');
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\User;
|
use App\User;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
use App\TwoFAccount;
|
||||||
use App\Http\Controllers\TwoFAccountController;
|
use App\Http\Controllers\TwoFAccountController;
|
||||||
use Illuminate\Auth\Authenticatable;
|
use Illuminate\Auth\Authenticatable;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
@ -113,7 +114,21 @@ public function test_HTTP_UNPROCESSABLE_ENTITY()
|
|||||||
*/
|
*/
|
||||||
public function test_HTTP_INTERNAL_SERVER_ERROR()
|
public function test_HTTP_INTERNAL_SERVER_ERROR()
|
||||||
{
|
{
|
||||||
|
factory(TwoFAccount::class, 3)->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->user, 'api')
|
||||||
|
->json('PATCH', '/api/twofaccounts/reorder', [
|
||||||
|
'orderedIds' => 'x'])
|
||||||
|
->assertStatus(500)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'message',
|
||||||
|
'originalMessage',
|
||||||
|
'debug'
|
||||||
|
])
|
||||||
|
->assertJsonFragment([
|
||||||
|
'message' => 'Whoops, looks like something went wrong'
|
||||||
|
]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -8,6 +8,7 @@
|
|||||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
use Tests\Classes\LocalFile;
|
use Tests\Classes\LocalFile;
|
||||||
|
use App\Classes\Options;
|
||||||
|
|
||||||
class QrcodeTest extends TestCase
|
class QrcodeTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -50,13 +51,42 @@ public function testDecodeInvalidQrcode()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* test Decode a qrcode via API
|
||||||
|
*
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function testDecodeValidUri()
|
||||||
|
{
|
||||||
|
$response = $this->json('POST', '/api/qrcode/decode', [
|
||||||
|
'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHVIRBGY7UIW'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonFragment([
|
||||||
|
'service' => 'test@test.com',
|
||||||
|
'account' => '',
|
||||||
|
'options' => [
|
||||||
|
'algorithm' => 'sha1',
|
||||||
|
'digits' => 6,
|
||||||
|
'epoch' => 0,
|
||||||
|
'period' => 30,
|
||||||
|
'secret' => 'A4GRFHVIRBGY7UIW'
|
||||||
|
],
|
||||||
|
'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHVIRBGY7UIW'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* test Decode a qrcode via API
|
* test Decode a qrcode via API
|
||||||
*
|
*
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function testDecodeValidQrcode()
|
public function testDecodeValidQrcode()
|
||||||
{
|
{
|
||||||
|
Options::store(array('useBasicQrcodeReader' => true));
|
||||||
|
|
||||||
$file = LocalFile::fake()->validQrcode();
|
$file = LocalFile::fake()->validQrcode();
|
||||||
|
|
||||||
$response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
|
$response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
|
||||||
|
@ -250,7 +250,7 @@ public function testTwoFAccountUpdateOfMissingTwoFAccount()
|
|||||||
*/
|
*/
|
||||||
public function testTwoFAccountIndexListing()
|
public function testTwoFAccountIndexListing()
|
||||||
{
|
{
|
||||||
$twofaccount = factory(TwoFAccount::class, 3)->create();
|
factory(TwoFAccount::class, 3)->create();
|
||||||
|
|
||||||
$response = $this->actingAs($this->user, 'api')
|
$response = $this->actingAs($this->user, 'api')
|
||||||
->json('GET', '/api/twofaccounts')
|
->json('GET', '/api/twofaccounts')
|
||||||
@ -293,9 +293,7 @@ public function testTwoFAccountDeletion()
|
|||||||
*/
|
*/
|
||||||
public function testTwoFAccountBatchDestroy()
|
public function testTwoFAccountBatchDestroy()
|
||||||
{
|
{
|
||||||
$twofaccount = factory(TwoFAccount::class)->create();
|
factory(TwoFAccount::class, 3)->create();
|
||||||
$twofaccount = factory(TwoFAccount::class)->create();
|
|
||||||
$twofaccount = factory(TwoFAccount::class)->create();
|
|
||||||
|
|
||||||
$ids = \Illuminate\Support\Facades\DB::table('twofaccounts')->value('id');
|
$ids = \Illuminate\Support\Facades\DB::table('twofaccounts')->value('id');
|
||||||
|
|
||||||
@ -305,4 +303,19 @@ public function testTwoFAccountBatchDestroy()
|
|||||||
->assertStatus(204);
|
->assertStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* test TwoFAccounts reorder
|
||||||
|
*
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function testTwoFAccountReorder()
|
||||||
|
{
|
||||||
|
factory(TwoFAccount::class, 3)->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->user, 'api')
|
||||||
|
->json('PATCH', '/api/twofaccounts/reorder', [
|
||||||
|
'orderedIds' => [3,2,1]])
|
||||||
|
->assertStatus(200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
webpack.mix.js
vendored
3
webpack.mix.js
vendored
@ -19,7 +19,8 @@ mix.js('resources/js/app.js', 'public/js')
|
|||||||
'vue-axios',
|
'vue-axios',
|
||||||
'vue-i18n',
|
'vue-i18n',
|
||||||
'vue-router',
|
'vue-router',
|
||||||
'v-clipboard'
|
'v-clipboard',
|
||||||
|
'vuedraggable'
|
||||||
])
|
])
|
||||||
.sass('resources/sass/app.scss', 'public/css');
|
.sass('resources/sass/app.scss', 'public/css');
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user