Refactor Options to a Setting service bound with the service container

This commit is contained in:
Bubka 2021-09-26 22:06:49 +02:00
parent afaa1a0a7a
commit 10fc144246
17 changed files with 453 additions and 143 deletions

View File

@ -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]);
}
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -4,7 +4,6 @@
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;

View File

@ -5,7 +5,6 @@
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 @@ public function handle($request, Closure $next)
$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;

View File

@ -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',
];
}
}

View File

@ -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',
];
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -4,12 +4,28 @@
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 @@ public function getAccounts(Group $group) : Collection
*/
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);
}

View File

@ -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;
}

View File

@ -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 @@ public function setSecretAttribute($value)
*/
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 @@ private function decryptOrReturn($value)
*/
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;
}
}

View File

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

View File

@ -29,4 +29,5 @@
'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'
];

View File

@ -28,13 +28,20 @@
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::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');