diff --git a/app/Classes/Options.php b/app/Classes/Options.php deleted file mode 100644 index 9735ec77..00000000 --- a/app/Classes/Options.php +++ /dev/null @@ -1,62 +0,0 @@ -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]); - } - } - - -} diff --git a/app/Group.php b/app/Group.php index 7a86503a..a7c32f32 100644 --- a/app/Group.php +++ b/app/Group.php @@ -2,7 +2,6 @@ namespace App; -use App\Classes\Options; use Illuminate\Database\Eloquent\Model; class Group extends Model diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php new file mode 100644 index 00000000..787586ea --- /dev/null +++ b/app/Http/Controllers/SettingController.php @@ -0,0 +1,164 @@ +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); + } + +} diff --git a/app/Http/Controllers/Settings/OptionController.php b/app/Http/Controllers/Settings/OptionController.php deleted file mode 100644 index 2737434e..00000000 --- a/app/Http/Controllers/Settings/OptionController.php +++ /dev/null @@ -1,61 +0,0 @@ -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); - } - -} diff --git a/app/Http/Controllers/SinglePageController.php b/app/Http/Controllers/SinglePageController.php index 74c272ba..82d4031a 100644 --- a/app/Http/Controllers/SinglePageController.php +++ b/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()); } } diff --git a/app/Http/Controllers/TwoFAccountController.php b/app/Http/Controllers/TwoFAccountController.php index 11a43abe..568f86f0 100644 --- a/app/Http/Controllers/TwoFAccountController.php +++ b/app/Http/Controllers/TwoFAccountController.php @@ -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; diff --git a/app/Http/Middleware/LogoutInactiveUser.php b/app/Http/Middleware/LogoutInactiveUser.php index 9fa0c814..20fa8799 100644 --- a/app/Http/Middleware/LogoutInactiveUser.php +++ b/app/Http/Middleware/LogoutInactiveUser.php @@ -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; diff --git a/app/Http/Requests/SettingStoreRequest.php b/app/Http/Requests/SettingStoreRequest.php new file mode 100644 index 00000000..04eeb524 --- /dev/null +++ b/app/Http/Requests/SettingStoreRequest.php @@ -0,0 +1,31 @@ + 'required|alpha|max:128|unique:options,key', + 'data' => 'required', + ]; + } +} diff --git a/app/Http/Requests/SettingUpdateRequest.php b/app/Http/Requests/SettingUpdateRequest.php new file mode 100644 index 00000000..c436a9d3 --- /dev/null +++ b/app/Http/Requests/SettingUpdateRequest.php @@ -0,0 +1,30 @@ + 'required', + ]; + } +} diff --git a/app/Providers/TwoFAuthServiceProvider.php b/app/Providers/TwoFAuthServiceProvider.php new file mode 100644 index 00000000..b9228cb1 --- /dev/null +++ b/app/Providers/TwoFAuthServiceProvider.php @@ -0,0 +1,29 @@ +app->bind(SettingServiceInterface::class, AppstractOptionsService::class); + } +} diff --git a/app/Services/AppstractOptionsService.php b/app/Services/AppstractOptionsService.php new file mode 100644 index 00000000..e8f3c7ad --- /dev/null +++ b/app/Services/AppstractOptionsService.php @@ -0,0 +1,94 @@ +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; + } + } +} \ No newline at end of file diff --git a/app/Services/GroupService.php b/app/Services/GroupService.php index dd04d229..0b4f4580 100644 --- a/app/Services/GroupService.php +++ b/app/Services/GroupService.php @@ -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); } diff --git a/app/Services/SettingServiceInterface.php b/app/Services/SettingServiceInterface.php new file mode 100644 index 00000000..d2b093af --- /dev/null +++ b/app/Services/SettingServiceInterface.php @@ -0,0 +1,41 @@ +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; } } \ No newline at end of file diff --git a/config/app.php b/config/app.php index 7ba04df1..aba7a97b 100644 --- a/config/app.php +++ b/config/app.php @@ -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 ], diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index 4bafe0f6..a6f28518 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -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' ]; \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 183e9edb..ecbaaefa 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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');