Browse Source

Add the logo fetching feature to the Create/Edit forms

Bubka 3 years ago
parent
commit
3d7607cb53

+ 25 - 3
app/Api/v1/Controllers/IconController.php

@@ -5,11 +5,13 @@ namespace App\Api\v1\Controllers;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Storage;
 use App\Http\Controllers\Controller;
+use App\Services\LogoService;
+use Illuminate\Support\Facades\App;
 
 
 class IconController extends Controller
 {
-   /**
+    /**
      * Handle uploaded icon image
      *
      * @param  \Illuminate\Http\Request  $request
@@ -21,15 +23,35 @@ class IconController extends Controller
             'icon' => 'required|image',
         ]);
         
-        $path = $request->file('icon')->store('public/icons');
         $path = $request->file('icon')->store('', 'icons');
         $response['filename'] = pathinfo($path)['basename'];
 
         return response()->json($response, 201);
     }
+
+
+    /**
+     * Fetch a logo
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function fetch(Request $request)
+    {
+        $this->validate($request, [
+            'service' => 'string|regex:/^[^:]+$/i',
+        ]);
+        
+        $logoService = App::make(LogoService::class);
+        $icon = $logoService->getIcon($request->service);
+
+        return $icon
+            ? response()->json(['filename' => $icon], 201)
+            : response()->json(null, 204);
+    }
     
 
-   /**
+    /**
      * delete an icon
      *
      * @param  \Illuminate\Http\Request  $request

+ 8 - 2
resources/js/components/Button.vue

@@ -1,12 +1,13 @@
 <template>
     <button 
         :type="nativeType"
-        :disabled="isLoading"
+        :disabled="isLoading || isDisabled"
         :class="{
             'button': true,
             [`${color}`]: true,
             'is-loading': isLoading,
-          }">
+        }"
+        v-on:click="$emit('click')">
         <slot />
     </button>
 </template>
@@ -30,6 +31,11 @@
                 type: Boolean,
                 default: false
             },
+
+            isDisabled: {
+                type: Boolean,
+                default: false
+            }
         }
     }
 

+ 2 - 2
resources/js/components/FormField.vue

@@ -1,8 +1,8 @@
 <template>
-    <div class="field" :class="{ 'with-offset' : hasOffset }">
+    <div class="field" :class="{ 'pt-3' : hasOffset }">
         <label class="label" v-html="label"></label>
         <div class="control">
-            <input :disabled="isDisabled" :id="fieldName" :type="inputType" class="input" v-model="form[fieldName]" :placeholder="placeholder" v-bind="$attrs" />
+            <input :disabled="isDisabled" :id="fieldName" :type="inputType" class="input" v-model="form[fieldName]" :placeholder="placeholder" v-bind="$attrs" v-on:change="$emit('field-changed', form[fieldName])"/>
         </div>
         <field-error :form="form" :field="fieldName" />
         <p class="help" v-html="help" v-if="help"></p>

+ 1 - 1
resources/js/components/FormToggle.vue

@@ -1,5 +1,5 @@
 <template>
-    <div class="field" :class="{ 'with-offset' : hasOffset }">
+    <div class="field" :class="{ 'pt-3' : hasOffset }">
         <label class="label" v-html="label"></label>
         <div class="is-toggle buttons">
             <label class="button is-dark" :disabled="isDisabled" v-for="choice in choices" :class="{ 'is-link' : form[fieldName] === choice.value }">

+ 4 - 0
resources/js/packages/fontawesome.js

@@ -25,6 +25,8 @@ import {
     faTh,
     faList,
     faTimesCircle,
+    faUpload,
+    faGlobe,
 } from '@fortawesome/free-solid-svg-icons'
 
 import {
@@ -54,6 +56,8 @@ library.add(
     faTh,
     faList,
     faTimesCircle,
+    faUpload,
+    faGlobe,
 );
 
 Vue.component('font-awesome-icon', FontAwesomeIcon)

+ 46 - 17
resources/js/views/twofaccounts/Create.vue

@@ -60,27 +60,42 @@
                 <!-- account -->
                 <form-field :form="form" fieldName="account" inputType="text" :label="$t('twofaccounts.account')" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
                 <!-- icon upload -->
-                <div class="field">
-                    <label class="label">{{ $t('twofaccounts.icon') }}</label>
-                    <div class="file is-dark">
-                        <label class="file-label">
-                            <input class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
-                            <span class="file-cta">
-                                <span class="file-icon">
-                                    <font-awesome-icon :icon="['fas', 'image']" />
+                <label class="label">{{ $t('twofaccounts.icon') }}</label>
+                <div class="field is-grouped">
+                    <!-- i'm lucky button -->
+                    <div class="control">
+                        <v-button @click="fetchLogo" :color="'is-dark'" :nativeType="'button'" :isDisabled="form.service.length < 3">
+                            <span class="icon is-small">
+                                <font-awesome-icon :icon="['fas', 'globe']" />
+                            </span>
+                            <span>{{ $t('twofaccounts.forms.i_m_lucky') }}</span>
+                        </v-button>
+                    </div>
+                    <!-- upload button -->
+                    <div class="control">
+                        <div class="file is-dark">
+                            <label class="file-label">
+                                <input class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
+                                <span class="file-cta">
+                                    <span class="file-icon">
+                                        <font-awesome-icon :icon="['fas', 'upload']" />
+                                    </span>
+                                    <span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
                                 </span>
-                                <span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
+                            </label>
+                            <span class="tag is-black is-large" v-if="tempIcon">
+                                <img class="icon-preview" :src="'/storage/icons/' + tempIcon" >
+                                <button class="delete is-small" @click.prevent="deleteIcon"></button>
                             </span>
-                        </label>
-                        <span class="tag is-black is-large" v-if="tempIcon">
-                            <img class="icon-preview" :src="'/storage/icons/' + tempIcon" >
-                            <button class="delete is-small" @click.prevent="deleteIcon"></button>
-                        </span>
+                        </div>
                     </div>
                 </div>
-                <field-error :form="form" field="icon" class="help-for-file" />
+                <div class="field">
+                    <field-error :form="form" field="icon" class="help-for-file" />
+                    <p class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
+                </div>
                 <!-- otp type -->
-                <form-toggle @otp_type="SetFormState" class="has-uppercased-button" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
+                <form-toggle @otp_type="setFormState" class="has-uppercased-button" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
                 <div v-if="form.otp_type">
                     <!-- secret -->
                     <label class="label" v-html="$t('twofaccounts.forms.secret.label')"></label>
@@ -345,7 +360,21 @@
                 const { data } = await this.form.upload('/api/v1/icons', imgdata)
 
                 this.tempIcon = data.filename;
+            },
+
+            fetchLogo() {
 
+                this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
+                    if (response.status === 201) {
+                        // clean possible already uploaded temp icon
+                        this.deleteIcon()
+                        this.tempIcon = response.data.filename;
+                    }
+                    else this.$notify({type: 'is-warning', text: this.$t('errors.no_logo_found_for_x', {service: this.form.service}) })
+                })
+                .catch(error => {
+                    this.$notify({type: 'is-warning', text: this.$t('errors.no_logo_found_for_x', {service: this.form.service}) })
+                });
             },
 
             deleteIcon(event) {
@@ -376,7 +405,7 @@
                 console.log('error', value)
             },
 
-            SetFormState (event) {
+            setFormState (event) {
                 this.form.otp_type = event
                 this.form.service = event === 'steamtotp' ? 'Steam' : ''
                 this.secretIsBase32Encoded = event === 'steamtotp' ? 1 : this.secretIsBase32Encoded

+ 46 - 16
resources/js/views/twofaccounts/Edit.vue

@@ -5,26 +5,41 @@
             <form-field :isDisabled="form.otp_type === 'steamtotp'" :form="form" fieldName="service" inputType="text" :label="$t('twofaccounts.service')" :placeholder="$t('twofaccounts.forms.service.placeholder')" autofocus />
             <!-- account -->
             <form-field :form="form" fieldName="account" inputType="text" :label="$t('twofaccounts.account')" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
-            <!-- icon -->
-            <div class="field">
-                <label class="label">{{ $t('twofaccounts.icon') }}</label>
-                <div class="file is-dark">
-                    <label class="file-label">
-                        <input class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
-                        <span class="file-cta">
-                            <span class="file-icon">
-                                <font-awesome-icon :icon="['fas', 'image']" />
+            <!-- icon upload -->
+            <label class="label">{{ $t('twofaccounts.icon') }}</label>
+            <div class="field is-grouped">
+                <!-- i'm lucky button -->
+                <div class="control">
+                    <v-button @click="fetchLogo" :color="'is-dark'" :nativeType="'button'" :isDisabled="form.service.length < 3">
+                        <span class="icon is-small">
+                            <font-awesome-icon :icon="['fas', 'globe']" />
+                        </span>
+                        <span>{{ $t('twofaccounts.forms.i_m_lucky') }}</span>
+                    </v-button>
+                </div>
+                <!-- upload button -->
+                <div class="control">
+                    <div class="file is-dark">
+                        <label class="file-label">
+                            <input class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
+                            <span class="file-cta">
+                                <span class="file-icon">
+                                    <font-awesome-icon :icon="['fas', 'upload']" />
+                                </span>
+                                <span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
                             </span>
-                            <span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
+                        </label>
+                        <span class="tag is-black is-large" v-if="tempIcon">
+                            <img class="icon-preview" :src="'/storage/icons/' + tempIcon" >
+                            <button class="delete is-small" @click.prevent="deleteIcon"></button>
                         </span>
-                    </label>
-                    <span class="tag is-black is-large" v-if="tempIcon">
-                        <img class="icon-preview" :src="'/storage/icons/' + tempIcon" >
-                        <button class="delete is-small" @click.prevent="deleteIcon"></button>
-                    </span>
+                    </div>
                 </div>
             </div>
-            <field-error :form="form" field="icon" class="help-for-file" />
+            <div class="field">
+                <field-error :form="form" field="icon" class="help-for-file" />
+                <p class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
+            </div>
             <!-- otp type -->
             <form-toggle class="has-uppercased-button" :isDisabled="true" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
             <div v-if="form.otp_type">
@@ -256,6 +271,21 @@
 
             },
 
+            fetchLogo() {
+
+                this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
+                    if (response.status === 201) {
+                        // clean possible already uploaded temp icon
+                        this.deleteIcon()
+                        this.tempIcon = response.data.filename;
+                    }
+                    else this.$notify({type: 'is-warning', text: this.$t('errors.no_logo_found_for_x', {service: this.form.service}) })
+                })
+                .catch(error => {
+                    this.$notify({type: 'is-warning', text: this.$t('errors.no_logo_found_for_x', {service: this.form.service}) })
+                });
+            },
+
             deleteIcon(event) {
 
                 if( this.tempIcon && this.tempIcon !== this.form.icon ) {

+ 1 - 0
resources/lang/en/errors.php

@@ -41,4 +41,5 @@ return [
     'auth_proxy_failed_legend' => '2Fauth is configured to run behind an authentication proxy but your proxy does not return the expected header. Check your configuration and try again.',
     'invalid_google_auth_migration' => 'Invalid or unreadable Google Authenticator data',
     'unsupported_otp_type' => 'Unsupported OTP type',
+    'no_logo_found_for_x' => 'No logo available for {service}'
 ];

+ 4 - 2
resources/lang/en/twofaccounts.php

@@ -25,7 +25,7 @@ return [
     'no_service' => '- no service -',
     'forms' => [
         'service' => [
-            'placeholder' => 'example.com',
+            'placeholder' => 'Google, Twitter, Apple',
         ],
         'account' => [
             'placeholder' => 'John DOE',
@@ -49,7 +49,9 @@ return [
             'val' => 'Lock',
             'title' => 'Lock it',
         ],
-        'choose_image' => 'Choose an image…',
+        'choose_image' => 'Upload',
+        'i_m_lucky' => 'I\'m lucky',
+        'i_m_lucky_legend' => 'The "I\'m lucky" button try to get the official icon of the given service. Enter actual service name without ".xyz" extension and try to avoid typo. (beta feature)',
         'test' => 'Test',
         'secret' => [
             'label' => 'Secret',

+ 1 - 0
routes/api/v1.php

@@ -43,6 +43,7 @@ Route::group(['middleware' => 'auth:api-guard'], function () {
 
     Route::post('qrcode/decode', 'QrCodeController@decode')->name('qrcode.decode');
 
+    Route::post('icons/default', 'IconController@fetch')->name('icons.fetch');
     Route::post('icons', 'IconController@upload')->name('icons.upload');
     Route::delete('icons/{icon}', 'IconController@delete')->name('icons.delete');
 });