diff --git a/app/Http/Controllers/TwoFactorController.php b/app/Http/Controllers/TwoFactorController.php new file mode 100644 index 000000000000..d3c9097bd471 --- /dev/null +++ b/app/Http/Controllers/TwoFactorController.php @@ -0,0 +1,63 @@ +user(); + + if ($user->google_2fa_secret) + return response()->json(['message' => '2FA already enabled'], 400); + elseif(! $user->phone) + return response()->json(['message' => ctrans('texts.set_phone_for_two_factor')], 400); + elseif(! $user->confirmed) + return response()->json(['message' => 'Please confirm your account first'], 400); + + $google2fa = new Google2FA(); + $secret = $google2fa->generateSecretKey(); + + $qr_code = $google2fa->getQRCodeGoogleUrl( + config('ninja.app_name') + $user->email, + $secret + ); + + $data = [ + 'secret' => $secret, + 'qrCode' => $qrCode, + ]; + + return response()->json(['data' => $data], 200); + + } + + public function enableTwoFactor() + { + $user = auth()->user(); + $secret = request()->input('secret'); + $oneTimePassword = request()->input('one_time_password'); + + if (! $secret || ! \Google2FA::verifyKey($secret, $oneTimePassword)) { + return response()->json('message' > ctrans('texts.invalid_one_time_password')); + } elseif (! $user->google_2fa_secret && $user->phone && $user->confirmed) { + $user->google_2fa_secret = encrypt($secret); + $user->save(); + } + + return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 8f4760192f17..aa85deb5bcb6 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -23,11 +23,16 @@ use App\Http\Requests\User\CreateUserRequest; use App\Http\Requests\User\DestroyUserRequest; use App\Http\Requests\User\DetachCompanyUserRequest; use App\Http\Requests\User\EditUserRequest; +use App\Http\Requests\User\ReconfirmUserRequest; use App\Http\Requests\User\ShowUserRequest; use App\Http\Requests\User\StoreUserRequest; use App\Http\Requests\User\UpdateUserRequest; use App\Jobs\Company\CreateCompanyToken; +use App\Jobs\Mail\NinjaMailer; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\User\UserEmailChanged; +use App\Mail\Admin\VerifyUserObject; use App\Models\CompanyUser; use App\Models\User; use App\Repositories\UserRepository; @@ -685,4 +690,70 @@ class UserController extends BaseController return response()->json(['message' => ctrans('texts.user_detached')], 200); } + + /** + * Detach an existing user to a company. + * + * @OA\Post( + * path="/api/v1/users/{user}/reconfirm", + * operationId="reconfirmUser", + * tags={"users"}, + * summary="Reconfirm an existing user to a company", + * description="Reconfirm an existing user from a company", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="user", + * in="path", + * description="The user hashed_id", + * example="FD767dfd7", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Success response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + * @param ReconfirmUserRequest $request + * @param User $user + * @return \Illuminate\Http\JsonResponse + */ + public function reconfirm(ReconfirmUserRequest $request, User $user) + { + $user->confirmation_code = $this->createDbHash($user->company()->db); + $user->save(); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new VerifyUserObject($user, $user->company()))->build()); + $nmo->company = $user->company(); + $nmo->to_user = $user; + $nmo->settings = $user->company->settings; + + NinjaMailerJob::dispatch($nmo); + + return response()->json(['message' => ctrans('texts.confirmation_resent')], 200); + + } + + } diff --git a/app/Http/Requests/User/ReconfirmUserRequest.php b/app/Http/Requests/User/ReconfirmUserRequest.php new file mode 100644 index 000000000000..a4a1f988ad4f --- /dev/null +++ b/app/Http/Requests/User/ReconfirmUserRequest.php @@ -0,0 +1,28 @@ +user()->Admin(); + } +} diff --git a/composer.json b/composer.json index c46ad17ce94a..08678c6b3ca3 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "ext-libxml": "*", "asgrim/ofxparser": "^1.2", "authorizenet/authorizenet": "^2.0", + "bacon/bacon-qr-code": "^2.0", "beganovich/snappdf": "^1.0", "checkout/checkout-sdk-php": "^1.0", "cleverit/ubl_invoice": "^1.3", @@ -59,6 +60,7 @@ "maennchen/zipstream-php": "^1.2", "nwidart/laravel-modules": "^8.0", "omnipay/paypal": "^3.0", + "pragmarx/google2fa": "^8.0", "predis/predis": "^1.1", "sentry/sentry-laravel": "^2", "stripe/stripe-php": "^7.50", diff --git a/composer.lock b/composer.lock index 3415f0b38265..ed4a5e2734b5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7b93ec16ae5791e0767c92eaf7061cc7", + "content-hash": "fd9ff106339b9e720558827e1c1ede1a", "packages": [ { "name": "asgrim/ofxparser", @@ -204,6 +204,59 @@ }, "time": "2021-01-29T19:17:51+00:00" }, + { + "name": "bacon/bacon-qr-code", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "3e9d791b67d0a2912922b7b7c7312f4b37af41e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/3e9d791b67d0a2912922b7b7c7312f4b37af41e4", + "reference": "3e9d791b67d0a2912922b7b7c7312f4b37af41e4", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^1.4", + "phpunit/phpunit": "^7 | ^8 | ^9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.3" + }, + "time": "2020-10-30T02:02:47+00:00" + }, { "name": "beganovich/snappdf", "version": "v1.5.0", @@ -1012,6 +1065,53 @@ }, "time": "2020-07-03T08:02:12+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/5abf82f213618696dda8e3bf6f64dd042d8542b2", + "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^7 | ^8 | ^9", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.3" + }, + "time": "2020-10-02T16:03:48+00:00" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "v0.1.1", @@ -5360,6 +5460,58 @@ ], "time": "2021-01-25T19:02:05+00:00" }, + { + "name": "pragmarx/google2fa", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/26c4c5cf30a2844ba121760fd7301f8ad240100b", + "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.18", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/8.0.0" + }, + "time": "2020-04-05T10:47:18+00:00" + }, { "name": "predis/predis", "version": "v1.1.6", diff --git a/routes/api.php b/routes/api.php index 4b196cb2cf49..53e6e8fc87dd 100644 --- a/routes/api.php +++ b/routes/api.php @@ -146,6 +146,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('tokens', 'TokenController')->middleware('password_protected'); // name = (tokens. index / create / show / update / destroy / edit Route::post('tokens/bulk', 'TokenController@bulk')->name('tokens.bulk')->middleware('password_protected'); + Route::get('settings/enable_two_factor', 'TwoFactorController@setupTwoFactor'); + Route::post('settings/enable_two_factor', 'TwoFactorController@enableTwoFactor'); + Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk'); Route::put('vendors/{vendor}/upload', 'VendorController@upload'); @@ -156,6 +159,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('users/{user}/attach_to_company', 'UserController@attach')->middleware('password_protected'); Route::delete('users/{user}/detach_from_company', 'UserController@detach')->middleware('password_protected'); Route::post('users/bulk', 'UserController@bulk')->name('users.bulk')->middleware('password_protected'); + Route::post('/user/{user}/reconfirm', 'UserController@reconfirm')->middleware('password_protected'); Route::resource('webhooks', 'WebhookController'); Route::post('webhooks/bulk', 'WebhookController@bulk')->name('webhooks.bulk');