Browse Source

Refactor Options to a Setting service bound with the service container

Bubka 3 years ago
parent
commit
10fc144246

+ 0 - 62
app/Classes/Options.php

@@ -1,62 +0,0 @@
-<?php
-
-namespace App\Classes;
-
-class Options
-{
-
-    /**
-     * Compile both default and user options
-     *
-     * @return Options collection or a signle
-     */
-    public static function get($option = null)
-    {
-        // Get a collection of user saved options
-        $userOptions = \Illuminate\Support\Facades\DB::table('options')->pluck('value', 'key');
-
-        // We replace patterned string that represent booleans with real booleans
-        $userOptions->transform(function ($item, $key) {
-                if( $item === '{{}}' ) {
-                    return false;
-                }
-                else if( $item === '{{1}}' ) {
-                    return true;
-                }
-                else {
-                    return $item;
-                }
-        });
-    
-        // Merge options from App configuration. It ensures we have a complete options collection with
-        // fallback values for every options
-        $options = collect(config('app.options'))->merge($userOptions);
-
-        if( $option ) {
-
-            return isset($options[$option]) ? $options[$option] : null;
-        }
-
-        return $options;
-    }
-
-
-    /**
-     * Set user options
-     *
-     * @param array All options to store
-     * @return void
-     */
-    public static function store($userOptions)
-    {
-        foreach($userOptions as $opt => $val) {
-
-            // We replace boolean values by a patterned string in order to retrieve
-            // them later (as the Laravel Options package do not support var type)
-            // Not a beatufilly solution but, hey, it works ^_^
-            option([$opt => is_bool($val) ? '{{' . $val . '}}' : $val]);
-        }
-    }
-
-
-}

+ 0 - 1
app/Group.php

@@ -2,7 +2,6 @@
 
 namespace App;
 
-use App\Classes\Options;
 use Illuminate\Database\Eloquent\Model;
 
 class Group extends Model

+ 164 - 0
app/Http/Controllers/SettingController.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\SettingStoreRequest;
+use App\Http\Requests\SettingUpdateRequest;
+use App\Services\SettingServiceInterface;
+use Illuminate\Http\Request;
+use App\Classes\DbProtection;
+use App\Http\Controllers\Controller;
+use Illuminate\Support\Collection;
+
+
+class SettingController extends Controller
+{
+
+    /**
+     * The Settings Service instance.
+     */
+    protected SettingServiceInterface $settingService;
+
+
+    /**
+     * Create a new controller instance.
+     * 
+     */
+    public function __construct(SettingServiceInterface $SettingServiceInterface)
+    {
+        $this->settingService = $SettingServiceInterface;
+    }
+
+
+    /**
+     * List all settings
+     * 
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        $settings = $this->settingService->all();
+        $settingsResources = collect();
+        $settings->each(function ($item, $key) use ($settingsResources) {
+            $settingsResources->push([
+                'name' => $key,
+                'data' => $item
+            ]);
+        });
+
+        // return SettingResource::collection($tata);
+        return response()->json($settingsResources->all(), 200);
+    }
+
+
+    /**
+     * Display a resource
+     *
+     * @param string $name
+     * 
+     * @return \App\Http\Resources\TwoFAccountReadResource
+     */
+    public function show($name)
+    {
+        $setting = $this->settingService->get($name);
+
+        if (!$setting) {
+            abort(404);
+        }
+
+        return response()->json([
+            'name' => $name,
+            'data' => $setting
+        ], 200);
+    }
+
+
+    /**
+     * Save options
+     * @return [type] [description]
+     */
+    public function store(SettingStoreRequest $request)
+    {
+        $validated = $request->validated();
+
+        $this->settingService->set($validated['name'], $validated['data']);
+
+        return response()->json([
+            'name' => $validated['name'],
+            'data' => $validated['data']
+        ], 201);
+    }
+
+
+    /**
+     * Save options
+     * @return [type] [description]
+     */
+    public function update(SettingUpdateRequest $request, $name)
+    {
+        $validated = $request->validated();
+
+        $setting = $this->settingService->get($name);
+
+        if (is_null($setting)) {
+            abort(404);
+        }
+
+        $setting = $this->settingService->set($name, $validated['data']);
+
+        return response()->json([
+            'name' => $name,
+            'data' => $validated['data']
+        ], 200);
+
+        // The useEncryption option impacts the [existing] content of the database.
+        // Encryption/Decryption of the data is done only if the user change the value of the option
+        // to prevent successive encryption
+
+        if( $request->has('useEncryption'))
+        {
+            if( $request->useEncryption && !$this->settingService->get('useEncryption') ) {
+
+                // user enabled the encryption
+                if( !DbProtection::enable() ) {
+                    return response()->json(['message' => __('errors.error_during_encryption')], 400);
+                }
+            }
+            else if( !$request->useEncryption && $this->settingService->get('useEncryption') ) {
+
+                // user disabled the encryption
+                if( !DbProtection::disable() ) {
+                    return response()->json(['message' => __('errors.error_during_decryption')], 400);
+                }
+            }
+        }
+
+    }
+
+
+    /**
+     * Save options
+     * @return [type] [description]
+     */
+    public function destroy($name)
+    {
+        $setting = $this->settingService->get($name);
+
+        if (is_null($setting)) {
+            abort(404);
+        }
+
+        $optionsConfig = config('app.options');
+        if(array_key_exists($name, $optionsConfig)) {
+            return response()->json(
+                ['message' => 'bad request',
+                'reason' => [__('errors.delete_user_setting_only')]
+            ], 400);
+        }
+
+        $this->settingService->delete($name);
+
+        return response()->json(null, 204);
+    }
+
+}

+ 0 - 61
app/Http/Controllers/Settings/OptionController.php

@@ -1,61 +0,0 @@
-<?php
-
-namespace App\Http\Controllers\Settings;
-
-use App\Classes\Options;
-use Illuminate\Http\Request;
-use App\Classes\DbProtection;
-use App\Http\Controllers\Controller;
-
-class OptionController extends Controller
-{
-
-
-    /**
-     * Get options
-     * @return [type] [description]
-     */
-    public function index()
-    {
-        // Fetch all setting values
-        $settings = Options::get();
-
-        return response()->json(['settings' => $settings], 200);
-    }
-
-
-    /**
-     * Save options
-     * @return [type] [description]
-     */
-    public function store(Request $request)
-    {
-        // The useEncryption option impacts the [existing] content of the database.
-        // Encryption/Decryption of the data is done only if the user change the value of the option
-        // to prevent successive encryption
-
-        if( isset($request->useEncryption))
-        {
-            if( $request->useEncryption && !Options::get('useEncryption') ) {
-
-                // user enabled the encryption
-                if( !DbProtection::enable() ) {
-                    return response()->json(['message' => __('errors.error_during_encryption'), 'settings' => Options::get()], 400);
-                }
-            }
-            else if( !$request->useEncryption && Options::get('useEncryption') ) {
-
-                // user disabled the encryption
-                if( !DbProtection::disable() ) {
-                    return response()->json(['message' => __('errors.error_during_decryption'), 'settings' => Options::get()], 400);
-                }
-            }
-        }
-
-        // Store all options
-        Options::store($request->all());
-
-        return response()->json(['message' => __('settings.forms.setting_saved'), 'settings' => Options::get()], 200);
-    }
-
-}

+ 19 - 2
app/Http/Controllers/SinglePageController.php

@@ -2,18 +2,35 @@
 
 namespace App\Http\Controllers;
 
-use App\Classes\Options;
+use App\Services\SettingServiceInterface;
 use Illuminate\Http\Request;
+use Illuminate\Support\Collection;
 
 class SinglePageController extends Controller
 {
 
+    /**
+     * The Settings Service instance.
+     */
+    protected SettingServiceInterface $settingService;
+
+
+    /**
+     * Create a new controller instance.
+     * 
+     */
+    public function __construct(SettingServiceInterface $SettingServiceInterface)
+    {
+        $this->settingService = $SettingServiceInterface;
+    }
+
+
     /**
      * return the main view
      * @return view
      */
     public function index()
     {
-        return view('landing')->with('appSettings', Options::get()->toJson());
+        return view('landing')->with('appSettings', $this->settingService->all()->toJson());
     }
 }

+ 0 - 1
app/Http/Controllers/TwoFAccountController.php

@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
 
 use App\Group;
 use App\TwoFAccount;
-use App\Classes\Options;
 use App\Http\Requests\TwoFAccountReorderRequest;
 use App\Http\Requests\TwoFAccountStoreRequest;
 use App\Http\Requests\TwoFAccountUpdateRequest;

+ 2 - 2
app/Http/Middleware/LogoutInactiveUser.php

@@ -5,7 +5,6 @@ namespace App\Http\Middleware;
 use Closure;
 use App\User;
 use Carbon\Carbon;
-use App\Classes\Options;
 use Illuminate\Http\Response;
 use Illuminate\Support\Facades\Auth;
 
@@ -32,7 +31,8 @@ class LogoutInactiveUser
         $inactiveFor = $now->diffInSeconds(Carbon::parse($user->last_seen_at));
 
         // Fetch all setting values
-        $settings = Options::get();
+        $settingService = resolve('App\Services\SettingServiceInterface');
+        $settings = $settingService->all();
 
         $kickUserAfterXSecond = intval($settings['kickUserAfter']) * 60;
 

+ 31 - 0
app/Http/Requests/SettingStoreRequest.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class SettingStoreRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            'name' => 'required|alpha|max:128|unique:options,key',
+            'data' => 'required',
+        ];
+    }
+}

+ 30 - 0
app/Http/Requests/SettingUpdateRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class SettingUpdateRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            'data' => 'required',
+        ];
+    }
+}

+ 29 - 0
app/Providers/TwoFAuthServiceProvider.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Providers;
+
+use App\Services\SettingServiceInterface;
+use App\Services\AppstractOptionsService;
+use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
+
+class TwoFAuthServiceProvider extends ServiceProvider
+{
+
+    /**
+     * Register any events for your application.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+    }
+
+    /**
+     * Register stuff.
+     *
+     */
+    public function register() : void
+    {
+        $this->app->bind(SettingServiceInterface::class, AppstractOptionsService::class);
+    }
+}

+ 94 - 0
app/Services/AppstractOptionsService.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
+
+class AppstractOptionsService implements SettingServiceInterface
+{
+    /**
+     * @inheritDoc
+     */
+    public function get(string $setting)
+    {
+        $value = option($setting, config('app.options' . $setting));
+        $value = $this->restoreType($value);
+
+        return $value;
+    }
+
+
+    /**
+     * @inheritDoc
+     */
+    public function all() : Collection
+    {
+        // Get a collection of user saved options
+        $userOptions = DB::table('options')->pluck('value', 'key');
+        $userOptions->transform(function ($item, $key) {
+            return $this->restoreType($item);
+        });
+        $userOptions = collect(config('app.options'))->merge($userOptions);
+
+        return $userOptions;
+    }
+
+
+    /**
+     * @inheritDoc
+     */
+    public function set($setting, $value = null) : void
+    {
+        $settings = is_array($setting) ? $setting : [$setting => $value];
+
+        foreach ($settings as $setting => $value) {
+            $settings[$setting] = $this->replaceBoolean($value);
+        }
+
+        option($settings);
+    }
+
+
+    /**
+     * @inheritDoc
+     */
+    public function delete(string $name) : void
+    {
+        option()->remove($name);
+    }
+    
+
+    /**
+     * Replaces boolean by a patterned string as appstrack/laravel-options package does not support var type
+     * 
+     * @param \Illuminate\Support\Collection $settings
+     * @return \Illuminate\Support\Collection
+     */
+    private function replaceBoolean($value)
+    {
+        return is_bool($value) ? '{{' . $value . '}}' : $value;
+    }
+
+
+    /**
+     * Replaces patterned string that represent booleans with real booleans
+     * 
+     * @param \Illuminate\Support\Collection $settings
+     * @return \Illuminate\Support\Collection
+     */
+    private function restoreType($value)
+    {
+        $value = is_numeric($value) ? (float) $value : $value;
+
+        if( $value === '{{}}' ) {
+            return false;
+        }
+        else if( $value === '{{1}}' ) {
+            return true;
+        }
+        else {
+            return $value;
+        }
+    }
+}

+ 18 - 2
app/Services/GroupService.php

@@ -4,12 +4,28 @@ namespace App\Services;
 
 use App\Group;
 use App\TwoFAccount;
-use App\Classes\Options;
+use App\Services\SettingServiceInterface;
 use Illuminate\Database\Eloquent\Collection;
 
 class GroupService
 {
 
+    /**
+     * The Settings Service instance.
+     */
+    protected SettingServiceInterface $settingService;
+
+
+    /**
+     * Create a new controller instance.
+     * 
+     */
+    public function __construct(SettingServiceInterface $SettingServiceInterface)
+    {
+        $this->settingService = $SettingServiceInterface;
+    }
+
+
     /**
      * Returns all existing groups
      * 
@@ -129,7 +145,7 @@ class GroupService
      */
     private function destinationGroup() : Group
     {
-        $id = Options::get('defaultGroup') === '-1' ? (int) Options::get('activeGroup') : (int) Options::get('defaultGroup');
+        $id = $this->settingService->get('defaultGroup') === '-1' ? (int) $this->settingService->get('activeGroup') : (int) $this->settingService->get('defaultGroup');
 
         return Group::find($id);
     }

+ 41 - 0
app/Services/SettingServiceInterface.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Collection;
+
+interface SettingServiceInterface
+{
+    /**
+     * Get a setting
+     *
+     * @param string|array $setting A single setting name or an associative array of name:value settings
+     * @return mixed string|int|boolean|null
+     */
+    public function get(string $setting);
+
+
+    /**
+     * Get all settings
+     *
+     * @return mixed Collection of settings
+     */
+    public function all() : Collection;
+
+
+    /**
+     * Set a setting
+     *
+     * @param string|array $setting A single setting name or an associative array of name:value settings
+     * @param string|int|boolean|null $value The value for single setting
+     */
+    public function set($setting, $value = null) : void;
+
+
+    /**
+     * Delete a setting
+     *
+     * @param string $name The setting name
+     */
+    public function delete(string $name) : void;
+}

+ 7 - 3
app/TwoFAccount.php

@@ -3,7 +3,7 @@
 namespace App;
 
 use Exception;
-use OTPHP\TOTP;
+// use App\Services\SettingServiceInterface;
 use OTPHP\HOTP;
 use OTPHP\Factory;
 use App\Classes\Options;
@@ -195,8 +195,10 @@ class TwoFAccount extends Model implements Sortable
      */
     private function decryptOrReturn($value)
     {
+        $settingService = resolve('App\Services\SettingServiceInterface');
+
         // Decipher when needed
-        if ( Options::get('useEncryption') )
+        if ( $settingService->get('useEncryption') )
         {
             try {
                 return Crypt::decryptString($value);
@@ -216,8 +218,10 @@ class TwoFAccount extends Model implements Sortable
      */
     private function encryptOrReturn($value)
     {
+        $settingService = resolve('App\Services\SettingServiceInterface');
+
         // should be replaced by laravel 8 attribute encryption casting
-        return Options::get('useEncryption') ? Crypt::encryptString($value) : $value;
+        return $settingService->get('useEncryption') ? Crypt::encryptString($value) : $value;
     }
 
 }

+ 2 - 1
config/app.php

@@ -205,7 +205,8 @@ return [
         App\Providers\AuthServiceProvider::class,
         // App\Providers\BroadcastServiceProvider::class,
         App\Providers\EventServiceProvider::class,
-        App\Providers\RouteServiceProvider::class
+        App\Providers\RouteServiceProvider::class,
+        App\Providers\TwoFAuthServiceProvider::class
 
     ],
 

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

@@ -29,4 +29,5 @@ return [
     'error_during_decryption' => 'Decryption failed, your database is still protected. This is mainly caused by an integrity issue of encrypted data for one or more accounts.',
     'qrcode_cannot_be_read' => 'This QR code is unreadable',
     'too_many_ids' => 'too many ids were included in the query parameter, max 100 allowed',
+    'delete_user_setting_only' => 'Only user-created setting can be deleted'
 ];

+ 15 - 8
routes/api.php

@@ -28,13 +28,20 @@ Route::group(['middleware' => 'auth:api'], function() {
 
     Route::post('auth/logout', 'Auth\LoginController@logout');
 
-    Route::prefix('settings')->group(function () {
-        Route::get('account', 'Settings\AccountController@show');
-        Route::patch('account', 'Settings\AccountController@update');
-        Route::patch('password', 'Settings\PasswordController@update');
-        Route::get('options', 'Settings\OptionController@index');
-        Route::post('options', 'Settings\OptionController@store');
-    });
+    Route::get('settings/{name}', 'SettingController@show');
+    Route::get('settings', 'SettingController@index');
+    Route::post('settings', 'SettingController@store');
+    Route::put('settings/{name}', 'SettingController@update');
+    Route::delete('settings/{name}', 'SettingController@destroy');
+
+    // Route::prefix('settings')->group(function () {
+        // Route::get('account', 'Settings\AccountController@show');
+        // Route::patch('account', 'Settings\AccountController@update');
+        // Route::patch('password', 'Settings\PasswordController@update');
+        // Route::post('options', 'Settings\OptionController@store');
+    // });
+
+
 
     Route::delete('twofaccounts', 'TwoFAccountController@batchDestroy');
     Route::patch('twofaccounts/withdraw', 'TwoFAccountController@withdraw');
@@ -42,7 +49,7 @@ Route::group(['middleware' => 'auth:api'], function() {
     Route::post('twofaccounts/preview', 'TwoFAccountController@preview');
     Route::get('twofaccounts/{twofaccount}/qrcode', 'QrCodeController@show');
     Route::get('twofaccounts/count', 'TwoFAccountController@count');
-    Route::get('twofaccounts/{id}/otp', 'TwoFAccountController@otp')->where('id', '[0-9]+');;
+    Route::get('twofaccounts/{id}/otp', 'TwoFAccountController@otp')->where('id', '[0-9]+');
     Route::post('twofaccounts/otp', 'TwoFAccountController@otp');
     Route::apiResource('twofaccounts', 'TwoFAccountController');
     Route::get('groups/{group}/twofaccounts', 'GroupController@accounts');