diff --git a/app/Http/Controllers/IconController.php b/app/Http/Controllers/IconController.php index bde6a8aa..498ac6b0 100644 --- a/app/Http/Controllers/IconController.php +++ b/app/Http/Controllers/IconController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use Validator; use Illuminate\Http\Request; use Illuminate\Http\File; use Illuminate\Support\Facades\Storage; @@ -18,17 +19,29 @@ class IconController extends Controller */ public function upload(Request $request) { + $messages = [ + 'icon.image' => 'Supported format are jpeg, png, bmp, gif, svg, or webp' + ]; - if($request->hasFile('icon')){ + $validator = Validator::make($request->all(), [ + 'icon' => 'required|image', + ], $messages); + + if ($validator->fails()) { + return response()->json(['error' => $validator->errors()], 400); + } + + + // if($request->hasFile('icon')){ $path = $request->file('icon')->storePublicly('public/icons'); return response()->json(pathinfo($path)['basename'], 201); - } - else - { - return response()->json('no file in $request', 204); - } + // } + // else + // { + // return response()->json('no file in $request', 204); + // } } diff --git a/app/Http/Controllers/QrCodeController.php b/app/Http/Controllers/QrCodeController.php index 85a78c37..94c3af92 100644 --- a/app/Http/Controllers/QrCodeController.php +++ b/app/Http/Controllers/QrCodeController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use Validator; use Illuminate\Http\Request; use Illuminate\Http\File; use Illuminate\Support\Facades\Storage; @@ -19,53 +20,74 @@ class QrCodecontroller extends Controller public function decode(Request $request) { - if($request->hasFile('qrcode')){ + // input validation + $messages = [ + 'qrcode.image' => 'Supported format are jpeg, png, bmp, gif, svg, or webp' + ]; - $path = $request->file('qrcode')->store('qrcodes'); + $validator = Validator::make($request->all(), [ + 'qrcode' => 'required|image', + ], $messages); - $qrcode = new QrReader(storage_path('app/' . $path)); - $uri = urldecode($qrcode->text()); - - $uriChunks = explode('?', $uri); - - foreach(explode('&', $uriChunks[1]) as $option) { - $option = explode('=', $option); - $options[$option[0]] = $option[1]; - } - - $account = $service = ''; - - $serviceChunks = explode(':', str_replace('otpauth://totp/', '', $uriChunks[0])); - - if( count($serviceChunks) > 1 ) { - $account = $serviceChunks[1]; - } - - $service = $serviceChunks[0]; - - if( strstr( $service, '@') ) { - $account = $service; - $service = ''; - } - - if( empty($service) & !empty($options['issuer']) ) { - $service = $options['issuer']; - } - - $twofaccount = (object) array( - 'service' => $service, - 'account' => $account, - 'uri' => $uri, - 'icon' => '', - 'options' => $options - ); - - Storage::delete($path); - - return response()->json($twofaccount, 201); + if ($validator->fails()) { + return response()->json(['error' => $validator->errors()], 400); } - else { - return response()->json('no file in $request', 204); + + + // qrcode analysis + $path = $request->file('qrcode')->store('qrcodes'); + $qrcode = new QrReader(storage_path('app/' . $path)); + $uri = urldecode($qrcode->text()); + + Storage::delete($path); + + if( empty($uri) ) { + + return response()->json([ + 'error' => [ + 'qrcode' => 'Nothing readable in this QR code 😕' + ] + ], 400); + } + + $uriChunks = explode('?', $uri); + + foreach(explode('&', $uriChunks[1]) as $option) { + $option = explode('=', $option); + $options[$option[0]] = $option[1]; + } + + $account = $service = ''; + + $serviceChunks = explode(':', str_replace('otpauth://totp/', '', $uriChunks[0])); + + if( count($serviceChunks) > 1 ) { + $account = $serviceChunks[1]; + } + + $service = $serviceChunks[0]; + + if( strstr( $service, '@') ) { + $account = $service; + $service = ''; + } + + if( empty($service) & !empty($options['issuer']) ) { + $service = $options['issuer']; + } + + + // returned object + $twofaccount = (object) array( + 'service' => $service, + 'account' => $account, + 'uri' => $uri, + 'icon' => '', + 'options' => $options + ); + + return response()->json($twofaccount, 201); } + } diff --git a/app/Http/Controllers/TwoFAccountController.php b/app/Http/Controllers/TwoFAccountController.php index 1e6890a4..518a395a 100644 --- a/app/Http/Controllers/TwoFAccountController.php +++ b/app/Http/Controllers/TwoFAccountController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use Validator; use App\TwoFAccount; use OTPHP\TOTP; use OTPHP\Factory; @@ -30,6 +31,22 @@ public function index() */ public function store(Request $request) { + + // see https://github.com/google/google-authenticator/wiki/Key-Uri-Format + // for otpauth uri format validation + $messages = [ + 'uri.starts_with' => 'Only valid TOTP uri are supported', + ]; + + $validator = Validator::make($request->all(), [ + 'service' => 'required', + 'uri' => 'required|starts_with:otpauth://totp/', + ], $messages); + + if ($validator->fails()) { + return response()->json(['error' => $validator->errors()], 400); + } + $twofaccount = TwoFAccount::create([ 'service' => $request->service, 'account' => $request->account, diff --git a/resources/js/views/Create.vue b/resources/js/views/Create.vue index bdb6e6ac..b885e199 100644 --- a/resources/js/views/Create.vue +++ b/resources/js/views/Create.vue @@ -17,17 +17,20 @@ +

{{ errors.qrcode.toString() }}

- +
+

{{ errors.service.toString() }}

+

{{ errors.account.toString() }}

@@ -51,6 +54,7 @@
+

{{ errors.uri.toString() }}

@@ -69,6 +73,7 @@
+

{{ errors.icon.toString() }}

@@ -94,7 +99,8 @@ 'icon' : '' }, uriIsLocked: true, - tempIcon: '' + tempIcon: '', + errors: {} } }, @@ -110,9 +116,15 @@ axios.defaults.headers.common['Content-Type'] = 'application/json' axios.defaults.headers.common['Authorization'] = 'Bearer ' + token - axios.post('/api/twofaccounts', this.twofaccount).then(response => { + axios.post('/api/twofaccounts', this.twofaccount) + .then(response => { this.$router.push({name: 'accounts', params: { InitialEditMode: false }}); }) + .catch(error => { + if (error.response.status === 400) { + this.errors = error.response.data.error + } + }); }, cancelCreation: function() { @@ -131,19 +143,9 @@ axios.defaults.headers.common['Content-Type'] = 'application/json' axios.defaults.headers.common['Authorization'] = 'Bearer ' + token - let files = this.$refs.qrcodeInput.files - - if (!files.length) { - console.log('no files'); - return false; - } - else { - console.log(files.length + ' file(s) found'); - } - let imgdata = new FormData(); - imgdata.append('qrcode', files[0]); + imgdata.append('qrcode', this.$refs.qrcodeInput.files[0]); let config = { header : { @@ -151,11 +153,16 @@ } } - axios.post('/api/qrcode/decode', imgdata, config).then(response => { - console.log('image upload response > ', response); + axios.post('/api/qrcode/decode', imgdata, config) + .then(response => { this.twofaccount = response.data; - } - ) + this.errors['qrcode'] = ''; + }) + .catch(error => { + if (error.response.status === 400) { + this.errors = error.response.data.error + } + }); }, uploadIcon(event) { @@ -165,12 +172,6 @@ axios.defaults.headers.common['Content-Type'] = 'application/json' axios.defaults.headers.common['Authorization'] = 'Bearer ' + token - let files = this.$refs.iconInput.files - - if (!files.length) { - return false; - } - // clean possible already uploaded temp icon if( this.tempIcon ) { this.deleteIcon() @@ -178,7 +179,7 @@ let imgdata = new FormData(); - imgdata.append('icon', files[0]); + imgdata.append('icon', this.$refs.iconInput.files[0]); let config = { header : { @@ -186,11 +187,18 @@ } } - axios.post('/api/icon/upload', imgdata, config).then(response => { + axios.post('/api/icon/upload', imgdata, config) + .then(response => { console.log('icon path > ', response); this.tempIcon = response.data; - } - ) + this.errors['icon'] = ''; + }) + .catch(error => { + if (error.response.status === 400) { + this.errors = error.response.data.error + } + }); + }, deleteIcon(event) { diff --git a/resources/sass/app.scss b/resources/sass/app.scss index 7b149d0b..126b6530 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -73,7 +73,7 @@ nav.level { .input, .select select, .textarea { background-color: hsl(0, 0%, 21%); border-color: hsl(0, 0%, 29%); - color: hsl(0, 0%, 71%); + color: hsl(0, 0%, 100%); } .select select::placeholder, .textarea::placeholder, .input::placeholder { @@ -135,6 +135,11 @@ footer .field.is-grouped { padding-top: 0.75rem; } +.help-for-file { + margin-top: -0.50rem; + margin-bottom: 0.75rem; +} + .no-account { display: block; opacity: 0.05;