瀏覽代碼

Add Logo fetching service - Close #99

Bubka 3 年之前
父節點
當前提交
bf32b37176
共有 5 個文件被更改,包括 207 次插入5 次删除
  1. 25 5
      app/Models/TwoFAccount.php
  2. 43 0
      app/Providers/TwoFAuthServiceProvider.php
  3. 121 0
      app/Services/LogoService.php
  4. 1 0
      config/app.php
  5. 17 0
      config/filesystems.php

+ 25 - 5
app/Models/TwoFAccount.php

@@ -3,6 +3,7 @@
 namespace App\Models;
 
 use Exception;
+use App\Services\LogoService;
 use App\Models\Dto\TotpDto;
 use App\Models\Dto\HotpDto;
 use App\Events\TwoFAccountDeleted;
@@ -26,6 +27,7 @@ use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use ParagonIE\ConstantTime\Base32;
+use Illuminate\Support\Facades\App;
 
 class TwoFAccount extends Model implements Sortable
 {
@@ -434,9 +436,12 @@ class TwoFAccount extends Model implements Sortable
         if ($isSteamTotp || strtolower($this->service) === 'steam') {
             $this->enforceAsSteam();
         }
-        else if ($this->generator->hasParameter('image')) {
+        if ($this->generator->hasParameter('image')) {
             $this->icon = $this->storeImageAsIcon($this->generator->getParameter('image'));
-        }        
+        }
+        if (!$this->icon) {
+            $this->icon = $this->defaultLogo();
+        }    
 
         Log::info(sprintf('TwoFAccount filled with an URI'));
 
@@ -453,9 +458,6 @@ class TwoFAccount extends Model implements Sortable
         $this->digits    = 5;
         $this->algorithm = self::SHA1;
         $this->period    = 30;
-        // if (!$this->icon) {
-        //     $this->icon = $this->storeImageAsIcon('https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Steam_icon_logo.svg/langfr-320px-Steam_icon_logo.svg.png');
-        // }
         
         Log::info(sprintf('TwoFAccount configured as Steam account'));
     }
@@ -567,6 +569,24 @@ class TwoFAccount extends Model implements Sortable
     }
 
 
+    /**
+     * Fetch a logo in the tfa directory and store it as a new stand alone icon
+     * 
+     * @return string|null The icon
+     */
+    private function defaultLogo()
+    {
+        $logoService = App::make(LogoService::class);
+        $logoFilename = $logoService->getLogo($this->service);
+
+        if ($logoFilename) {
+            $newFilename = Str::random(40).'.svg';
+            return Storage::disk('icons')->put($newFilename, Storage::disk('logos')->get($logoFilename)) ? $newFilename : null;
+        }
+        else return null;
+    }
+
+
     /**
      * Returns an acceptable value
      */

+ 43 - 0
app/Providers/TwoFAuthServiceProvider.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Providers;
+
+use App\Services\LogoService;
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Contracts\Support\DeferrableProvider;
+
+class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvider
+{
+    /**
+     * Register services.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        $this->app->singleton(LogoService::class, function ($app) {
+            return new LogoService();
+        });
+    }
+
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        //
+    } 
+
+
+    /**
+     * Get the services provided by the provider.
+     *
+     * @return array
+     */
+    public function provides()
+    {
+        return [LogoService::class];
+    }
+}

+ 121 - 0
app/Services/LogoService.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+
+class LogoService
+{
+    /**
+     * \Illuminate\Support\Collection
+     */
+    protected $tfas;
+
+    /**
+     * 
+     */
+    const TFA_JSON = 'tfa.json';
+    const TFA_URL = 'https://2fa.directory/api/v3/tfa.json';
+
+
+    public function __construct()
+    {
+        $this->setTfaCollection();
+    }
+
+
+    /**
+     * Return the logo's filename for a given service
+     * 
+     * @param string $serviceName Name of the service to fetch a logo for
+     * @return string|null The logo filename or null if no logo has been found
+     */
+    public function getLogo(string $serviceName) : string
+    {
+        $domain = $this->tfas->get(strtolower($serviceName));
+        $logoFilename = $domain.'.svg';
+
+        if ($domain && !Storage::disk('logos')->exists($logoFilename)) {
+            $this->fetchLogo($logoFilename);
+        }
+
+        return Storage::disk('logos')->exists($logoFilename) ? $logoFilename : '';
+    }
+
+
+    /**
+     * Build and set the TFA directoy collection
+     * 
+     * @return void
+     */
+    protected function setTfaCollection() : void
+    {
+        // We fetch a fresh tfaDirectory if necessary to prevent too many API calls
+        if (Storage::disk('logos')->exists(self::TFA_JSON)) {
+            if (time() - Storage::disk('logos')->lastModified(self::TFA_JSON) > 86400) {
+                $this->cacheTfaDirectorySource();
+            }
+        } else {
+            $this->cacheTfaDirectorySource();            
+        }
+
+        $this->tfas = Storage::disk('logos')->exists(self::TFA_JSON)
+            ? collect(json_decode(Storage::disk('logos')->get(self::TFA_JSON)))
+            : collect();
+    }
+
+
+    /**
+     * Fetch and cache fresh TFA.Directory data using the https://2fa.directory API
+     * 
+     * @return void
+     */
+    protected function cacheTfaDirectorySource() : void
+    {
+        try {
+            $response = Http::retry(3, 100)->get(self::TFA_URL);
+
+            $coll = collect(json_decode(htmlspecialchars_decode($response->body()), true))
+                    ->mapWithKeys(function ($item, $key) {
+                        return [
+                            strtolower(head($item)) => $item[1]["domain"]
+                        ];
+                    });
+
+            Storage::disk('logos')->put(self::TFA_JSON, $coll->toJson())
+                ? Log::info('Fresh tfa.json saved to logos dir')
+                : Log::notice('Cannot save tfa.json to logos dir');
+
+        }
+        catch (\Exception $e) {
+            Log::error('Caching of tfa.json failed');
+        }
+
+    }
+
+
+    /**
+     * Fetch a logo and store it to the disk
+     * 
+     * @param string $logoFile Logo filename to fetch
+     * @return void
+     */
+    protected function fetchLogo(string $logoFile) : void
+    {
+        try {
+            $response = Http::retry(3, 100)
+                ->get('https://raw.githubusercontent.com/2factorauth/twofactorauth/master/img/'.$logoFile[0].'/'.$logoFile);
+            
+            if ($response->successful()) {
+                Storage::disk('logos')->put($logoFile, $response->body())
+                    ? Log::info(sprintf('Logo "%s" saved to logos dir.', $logoFile))
+                    : Log::notice(sprintf('Cannot save logo "%s" to logos dir', $logoFile));
+            }
+        }
+        catch (\Exception $exception) {
+            Log::error(sprintf('Fetching of logo "%s" failed.', $logoFile));
+        }
+    }
+}

+ 1 - 0
config/app.php

@@ -161,6 +161,7 @@ return [
         Illuminate\Translation\TranslationServiceProvider::class,
         Illuminate\Validation\ValidationServiceProvider::class,
         Illuminate\View\ViewServiceProvider::class,
+        App\Providers\TwoFAuthServiceProvider::class,
 
         /*
          * Package Service Providers...

+ 17 - 0
config/filesystems.php

@@ -48,6 +48,23 @@ return [
             'root' => storage_path('app'),
         ],
 
+        'icons' => [
+            'driver' => 'local',
+            'root' => storage_path('app/public/icons'),
+            'url' => env('APP_URL').'/storage/icons',
+            'visibility' => 'public',
+        ],
+
+        'logos' => [
+            'driver' => 'local',
+            'root' => storage_path('app/logos'),
+        ],
+
+        'imagesLink' => [
+            'driver' => 'local',
+            'root' => storage_path('app/imagesLink'),
+        ],
+
         'public' => [
             'driver' => 'local',
             'root' => storage_path('app/public'),