1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-19 16:01:34 +02:00

Request for fail2ban log support and two factor authentication support (2FA) #1657

This commit is contained in:
Hillel Coren 2017-11-01 21:22:29 +02:00
parent 570e72c083
commit 478de96d81
23 changed files with 682 additions and 35 deletions

View File

@ -47,7 +47,7 @@ class AccountApiController extends BaseAPIController
$account = $this->accountRepo->create($request->first_name, $request->last_name, $request->email, $request->password);
$user = $account->users()->first();
Auth::login($user, true);
Auth::login($user);
event(new UserSignedUp());
return $this->processLogin($request);
@ -69,6 +69,7 @@ class AccountApiController extends BaseAPIController
}
return $this->processLogin($request);
} else {
error_log('login failed');
if ($user) {
$user->failed_logins = $user->failed_logins + 1;
$user->save();

View File

@ -1123,6 +1123,11 @@ class AccountController extends BaseController
}
$rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id'];
if ($user->google_2fa_secret) {
$rules['phone'] = 'required';
}
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
@ -1144,6 +1149,10 @@ class AccountController extends BaseController
$user->notify_approved = Input::get('notify_approved');
}
if ($user->google_2fa_secret && ! Input::get('enable_two_factor')) {
$user->google_2fa_secret = null;
}
if (Utils::isNinja()) {
if (Input::get('referral_code') && ! $user->referral_code) {
$user->referral_code = strtolower(str_random(RANDOM_KEY_LENGTH));

View File

@ -14,6 +14,9 @@ use Illuminate\Http\Request;
use Lang;
use Session;
use Utils;
use Cache;
use Illuminate\Contracts\Auth\Authenticatable;
use App\Http\Requests\ValidateTwoFactorRequest;
class AuthController extends Controller
{
@ -151,20 +154,12 @@ class AuthController extends Controller
if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) {
Session::flash('error', trans('texts.invalid_credentials'));
return redirect()->to('login');
}
$response = self::postLogin($request);
if (Auth::check()) {
if ($user && $user->failed_logins > 0) {
$user->failed_logins = 0;
$user->save();
}
Event::fire(new UserLoggedIn());
/*
$users = false;
// we're linking a new account
@ -176,10 +171,8 @@ class AuthController extends Controller
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
}
*/
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
Session::put(SESSION_USER_ACCOUNTS, $users);
} elseif ($user) {
error_log('login failed');
$user->failed_logins = $user->failed_logins + 1;
$user->save();
}
@ -187,6 +180,60 @@ class AuthController extends Controller
return $response;
}
/**
* Send the post-authentication response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return \Illuminate\Http\Response
*/
private function authenticated(Request $request, Authenticatable $user)
{
if ($user->google_2fa_secret) {
Auth::logout();
$request->session()->put('2fa:user:id', $user->id);
return redirect('/validate_two_factor/' . $user->account->account_key);
}
Event::fire(new UserLoggedIn());
return redirect()->intended($this->redirectTo);
}
/**
*
* @return \Illuminate\Http\Response
*/
public function getValidateToken()
{
if (session('2fa:user:id')) {
return view('auth.two_factor');
}
return redirect('login');
}
/**
*
* @param App\Http\Requests\ValidateSecretRequest $request
* @return \Illuminate\Http\Response
*/
public function postValidateToken(ValidateTwoFactorRequest $request)
{
//get user id and create cache key
$userId = $request->session()->pull('2fa:user:id');
$key = $userId . ':' . $request->totp;
//use cache to store token to blacklist
Cache::add($key, true, 4);
//login and redirect user
Auth::loginUsingId($userId);
Event::fire(new UserLoggedIn());
return redirect()->intended($this->redirectTo);
}
/**
* @return \Illuminate\Http\Response
*/

View File

@ -2,6 +2,8 @@
namespace App\Http\Controllers\Auth;
use Event;
use App\Events\UserLoggedIn;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
@ -18,7 +20,9 @@ class PasswordController extends Controller
|
*/
use ResetsPasswords;
use ResetsPasswords {
getResetSuccessResponse as protected traitGetResetSuccessResponse;
}
/**
* @var string
@ -49,4 +53,18 @@ class PasswordController extends Controller
return $this->getEmail();
}
protected function getResetSuccessResponse($response)
{
$user = auth()->user();
if ($user->google_2fa_secret) {
auth()->logout();
session(['2fa:user:id' => $user->id]);
return redirect('/validate_two_factor/' . $user->account->account_key);
} else {
Event::fire(new UserLoggedIn());
return $this->traitGetResetSuccessResponse($response);
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use PragmaRX\Google2FA\Google2FA;
use Crypt;
class Google2FAController extends Controller
{
public function enableTwoFactor()
{
$user = auth()->user();
if ($user->google_2fa_secret) {
return redirect('/settings/user_details');
}
$google2fa = new Google2FA();
$secret = $google2fa->generateSecretKey();
$user->google_2fa_secret = Crypt::encrypt($secret);
$user->save();
$qrCode = $google2fa->getQRCodeGoogleUrl(
APP_NAME,
$user->email,
$secret
);
$data = [
'secret' => $secret,
'qrCode' => $qrCode,
];
return view('users.two_factor', $data);
}
}

View File

@ -36,5 +36,6 @@ class Kernel extends HttpKernel
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
'api' => 'App\Http\Middleware\ApiCheck',
'cors' => '\Barryvdh\Cors\HandleCors',
'throttle' => 'Illuminate\Routing\Middleware\ThrottleRequests',
];
}

View File

@ -0,0 +1,78 @@
<?php
namespace App\Http\Requests;
use Cache;
use Crypt;
use Google2FA;
use App\Models\User;
use App\Http\Requests\Request;
use Illuminate\Validation\Factory as ValidatonFactory;
class ValidateTwoFactorRequest extends Request
{
/**
*
* @var \App\User
*/
private $user;
/**
* Create a new FormRequest instance.
*
* @param \Illuminate\Validation\Factory $factory
* @return void
*/
public function __construct(ValidatonFactory $factory)
{
$factory->extend(
'valid_token',
function ($attribute, $value, $parameters, $validator) {
$secret = Crypt::decrypt($this->user->google_2fa_secret);
return Google2FA::verifyKey($secret, $value);
},
trans('texts.invalid_code')
);
$factory->extend(
'used_token',
function ($attribute, $value, $parameters, $validator) {
$key = $this->user->id . ':' . $value;
return !Cache::has($key);
},
trans('texts.invalid_code')
);
}
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
try {
$this->user = User::findOrFail(
session('2fa:user:id')
);
} catch (Exception $exc) {
return false;
}
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'totp' => 'bail|required|digits:6|valid_token|used_token',
];
}
}

View File

@ -79,6 +79,8 @@ Route::group(['middleware' => 'lookup:postmark'], function () {
Route::group(['middleware' => 'lookup:account'], function () {
Route::post('/payment_hook/{account_key}/{gateway_id}', 'OnlinePaymentController@handlePaymentWebhook');
Route::match(['GET', 'POST', 'OPTIONS'], '/buy_now/{gateway_type?}', 'OnlinePaymentController@handleBuyNow');
Route::get('validate_two_factor/{account_key}', 'Auth\AuthController@getValidateToken');
Route::post('validate_two_factor/{account_key}', ['middleware' => 'throttle:5', 'uses' => 'Auth\AuthController@postValidateToken']);
});
//Route::post('/hook/bot/{platform?}', 'BotController@handleMessage');
@ -141,6 +143,8 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
Route::post('settings/user_details', 'AccountController@saveUserDetails');
Route::post('settings/payment_gateway_limits', 'AccountGatewayController@savePaymentGatewayLimits');
Route::post('users/change_password', 'UserController@changePassword');
Route::get('settings/enable_two_factor', 'Google2FAController@enableTwoFactor');
Route::get('settings/disable_two_factor', 'Google2FAController@disableTwoFactor');
Route::resource('clients', 'ClientController');
Route::get('api/clients', 'ClientController@getDatatable');

View File

@ -41,7 +41,8 @@ class HandleUserLoggedIn
*/
public function handle(UserLoggedIn $event)
{
$account = Auth::user()->account;
$user = auth()->user();
$account = $user->account;
if (! Utils::isNinja() && empty($account->last_login)) {
event(new UserSignedUp());
@ -50,6 +51,11 @@ class HandleUserLoggedIn
$account->last_login = Carbon::now()->toDateTimeString();
$account->save();
if ($user->failed_logins > 0) {
$user->failed_logins = 0;
$user->save();
}
$users = $this->accountRepo->loadAccounts(Auth::user()->id);
Session::put(SESSION_USER_ACCOUNTS, $users);
HistoryUtils::loadHistory($users ?: Auth::user()->id);

View File

@ -59,7 +59,15 @@ class User extends Authenticatable
*
* @var array
*/
protected $hidden = ['password', 'remember_token', 'confirmation_code'];
protected $hidden = [
'password',
'remember_token',
'confirmation_code',
'oauth_user_id',
'oauth_provider_id',
'google_2fa_secret',
'google_2fa_phone',
];
/**
* @var array

View File

@ -83,10 +83,15 @@ class AuthService
}
} else {
LookupUser::setServerByField('oauth_user_key', $providerId . '-' . $oauthUserId);
\Log::info("Find user: $providerId, $oauthUserId");
if ($user = $this->accountRepo->findUserByOauth($providerId, $oauthUserId)) {
Auth::login($user, true);
event(new UserLoggedIn());
if ($user->google_2fa_secret) {
session(['2fa:user:id' => $user->id]);
return redirect('/validate_two_factor/' . $user->account->account_key);
} else {
Auth::login($user);
event(new UserLoggedIn());
}
} else {
Session::flash('error', trans('texts.invalid_credentials'));

View File

@ -17,11 +17,13 @@
"ext-gd": "*",
"ext-gmp": "*",
"Dwolla/omnipay-dwolla": "dev-master",
"abdala/omnipay-pagseguro": "0.2",
"agmscode/omnipay-agms": "~1.0",
"alfaproject/omnipay-skrill": "dev-master",
"anahkiasen/former": "4.0.*@dev",
"andreas22/omnipay-fasapay": "1.*",
"asgrim/ofxparser": "^1.1",
"bacon/bacon-qr-code": "^1.0",
"barracudanetworks/archivestream-php": "^1.0",
"barryvdh/laravel-cors": "^0.9.1",
"barryvdh/laravel-debugbar": "~2.2",
@ -70,6 +72,7 @@
"mpdf/mpdf": "6.1.3",
"nwidart/laravel-modules": "^1.14",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/authorizenet": "dev-solution-id as 2.5.0",
"omnipay/bitpay": "dev-master",
"omnipay/braintree": "~2.0@dev",
"omnipay/gocardless": "dev-master",
@ -77,6 +80,7 @@
"omnipay/omnipay": "~2.3",
"omnipay/stripe": "dev-master",
"patricktalmadge/bootstrapper": "5.5.x",
"pragmarx/google2fa-laravel": "^0.1.2",
"predis/predis": "^1.1",
"simshaun/recurr": "dev-master",
"softcommerce/omnipay-paytrace": "~1.0",
@ -86,10 +90,8 @@
"webpatser/laravel-countries": "dev-master",
"websight/l5-google-cloud-storage": "dev-master",
"wepay/php-sdk": "^0.2",
"wildbit/laravel-postmark-provider": "3.0",
"abdala/omnipay-pagseguro": "0.2",
"omnipay/authorizenet": "dev-solution-id as 2.5.0"
},
"wildbit/laravel-postmark-provider": "3.0"
},
"require-dev": {
"codeception/c3": "~2.0",
"codeception/codeception": "2.3.3",

247
composer.lock generated
View File

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "13e8b7032220fe1181aed50719a55a11",
"content-hash": "eb01266703f4233d8fa703516a43bba8",
"hash": "80f6d3f1c26c0eb84d0d9ee5aa88aaa1",
"content-hash": "0891783210a35e9c83addbcdbb691993",
"packages": [
{
"name": "abdala/omnipay-pagseguro",
@ -458,6 +458,52 @@
],
"time": "2017-10-17 19:51:40"
},
{
"name": "bacon/bacon-qr-code",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "5a91b62b9d37cee635bbf8d553f4546057250bee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/5a91b62b9d37cee635bbf8d553f4546057250bee",
"reference": "5a91b62b9d37cee635bbf8d553f4546057250bee",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^5.4|^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8"
},
"suggest": {
"ext-gd": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-0": {
"BaconQrCode": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "http://www.dasprids.de",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"time": "2017-10-17 09:59:25"
},
{
"name": "barracudanetworks/archivestream-php",
"version": "1.0.5",
@ -7181,6 +7227,68 @@
],
"time": "2017-06-15 07:04:25"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4",
"reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4",
"shasum": ""
},
"require": {
"php": "^7"
},
"require-dev": {
"phpunit/phpunit": "^6",
"vimeo/psalm": "^0.3|^1"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"time": "2017-09-22 14:55:37"
},
{
"name": "paragonie/random_compat",
"version": "v1.4.2",
@ -7437,6 +7545,137 @@
],
"time": "2017-06-05 06:31:10"
},
{
"name": "pragmarx/google2fa",
"version": "v2.0.6",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/bc2d654305e4d09254125f8cd390a7fbc4742d46",
"reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "~1.0|~2.0",
"paragonie/random_compat": "~1.4|~2.0",
"php": ">=5.4",
"symfony/polyfill-php56": "~1.2"
},
"require-dev": {
"bacon/bacon-qr-code": "~1.0",
"phpspec/phpspec": "~2.1",
"phpunit/phpunit": "~4"
},
"suggest": {
"bacon/bacon-qr-code": "Required to generate inline QR Codes."
},
"type": "library",
"extra": {
"component": "package",
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"PragmaRX\\Google2FA\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"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",
"laravel"
],
"time": "2017-09-12 06:55:05"
},
{
"name": "pragmarx/google2fa-laravel",
"version": "v0.1.2",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa-laravel.git",
"reference": "ebc24520bea1f1ef5659e39d4024a3275ce49a49"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa-laravel/zipball/ebc24520bea1f1ef5659e39d4024a3275ce49a49",
"reference": "ebc24520bea1f1ef5659e39d4024a3275ce49a49",
"shasum": ""
},
"require": {
"laravel/framework": "~5",
"php": ">=5.4",
"pragmarx/google2fa": "~2.0"
},
"require-dev": {
"benconstable/phpspec-laravel": "~3.0",
"phpspec/phpspec": "~3"
},
"suggest": {
"bacon/bacon-qr-code": "Required to generate inline QR Codes."
},
"type": "library",
"extra": {
"component": "package",
"frameworks": [
"Laravel"
],
"branch-alias": {
"dev-master": "0.1-dev"
},
"laravel": {
"providers": [
"PragmaRX\\Google2FALaravel\\ServiceProvider"
],
"aliases": {
"Google2FA": "PragmaRX\\Google2FALaravel\\Facade"
}
}
},
"autoload": {
"psr-4": {
"PragmaRX\\Google2FALaravel\\": "src/",
"spec\\PragmaRX\\Google2FALaravel\\": "tests/spec/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
"keywords": [
"Authentication",
"Two Factor Authentication",
"google2fa",
"laravel"
],
"time": "2017-06-23 00:45:24"
},
{
"name": "predis/predis",
"version": "v1.1.1",
@ -12192,14 +12431,14 @@
"meebio/omnipay-creditcall": 20,
"meebio/omnipay-secure-trading": 20,
"omnipay/2checkout": 20,
"omnipay/authorizenet": 20,
"omnipay/bitpay": 20,
"omnipay/braintree": 20,
"omnipay/gocardless": 20,
"omnipay/stripe": 20,
"simshaun/recurr": 20,
"webpatser/laravel-countries": 20,
"websight/l5-google-cloud-storage": 20,
"omnipay/authorizenet": 20
"websight/l5-google-cloud-storage": 20
},
"prefer-stable": false,
"prefer-lowest": false,

View File

@ -158,6 +158,7 @@ return [
Codedge\Updater\UpdaterServiceProvider::class,
Nwidart\Modules\LaravelModulesServiceProvider::class,
Barryvdh\Cors\ServiceProvider::class,
PragmaRX\Google2FALaravel\ServiceProvider::class,
/*
* Application Service Providers...
@ -171,6 +172,7 @@ return [
'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
'Davibennun\LaravelPushNotification\LaravelPushNotificationServiceProvider',
],
/*
@ -266,6 +268,8 @@ return [
'DateUtils' => App\Libraries\DateUtils::class,
'HTMLUtils' => App\Libraries\HTMLUtils::class,
'Domain' => App\Constants\Domain::class,
'Google2FA' => PragmaRX\Google2FALaravel\Facade::class,
],
];

68
config/google2fa.php Normal file
View File

@ -0,0 +1,68 @@
<?php
return [
/*
* Auth container binding
*/
'enabled' => true,
/*
* Lifetime in minutes.
* In case you need your users to be asked for a new one time passwords from time to time.
*/
'lifetime' => 0, // 0 = eternal
/*
* Renew lifetime at every new request.
*/
'keep_alive' => true,
/*
* Auth container binding
*/
'auth' => 'auth',
/*
* 2FA verified session var
*/
'session_var' => 'google2fa',
/*
* One Time Password request input name
*/
'otp_input' => 'one_time_password',
/*
* One Time Password Window
*/
'window' => 1,
/*
* Forbid user to reuse One Time Passwords.
*/
'forbid_old_passwords' => false,
/*
* User's table column for google2fa secret
*/
'otp_secret_column' => 'google2fa_secret',
/*
* One Time Password View
*/
'view' => 'google2fa.index',
/*
* One Time Password error message
*/
'error_messages' => [
'wrong_otp' => "The 'One Time Password' typed was wrong.",
],
];

View File

@ -28,6 +28,10 @@ class AddDefaultRates extends Migration
$table->date('partial_due_date')->nullable();
});
Schema::table('users', function ($table) {
$table->string('google_2fa_secret')->nullable();
});
// Add 'Four Months' frequency option
if (DB::table('frequencies')->count() == 8) {
DB::table('frequencies')->where('id', '=', 7)->update(['name' => 'Four months']);
@ -61,5 +65,9 @@ class AddDefaultRates extends Migration
Schema::table('invoices', function ($table) {
$table->dropColumn('partial_due_date');
});
Schema::table('users', function ($table) {
$table->dropColumn('google_2fa_secret');
});
}
}

File diff suppressed because one or more lines are too long

View File

@ -2510,6 +2510,14 @@ $LANG = array(
'product_fields_help' => 'Drag and drop fields to change their order',
'custom_value1' => 'Custom Value',
'custom_value2' => 'Custom Value',
'enable_two_factor' => 'Two-Factor Authentication',
'enable_two_factor_help' => 'Use your phone to confirm your identity when logging in',
'two_factor_setup' => 'Two-Factor Setup',
'two_factor_setup_help' => 'Scan the bar code with a :link compatible app or enter the following code: :code',
'return_to_settings' => 'Return to Settings',
'one_time_password' => 'One Time Password',
'set_phone_for_two_factor' => 'Set your phone number to enable.',
);

View File

@ -6,7 +6,8 @@
{!! Former::open_for_files()->addClass('warn-on-exit')->rules(array(
'first_name' => 'required',
'last_name' => 'required',
'email' => 'email|required'
'email' => 'email|required',
'phone' => $user->google_2fa_secret ? 'required' : ''
)) !!}
{{ Former::populate($account) }}
@ -15,6 +16,7 @@
{{ Former::populateField('email', $user->email) }}
{{ Former::populateField('phone', $user->phone) }}
{{ Former::populateField('dark_mode', intval($user->dark_mode)) }}
{{ Former::populateField('enable_two_factor', $user->google_2fa_secret ? 1 : 0) }}
@if (Input::has('affiliate'))
{{ Former::populateField('referral_code', true) }}
@ -48,6 +50,21 @@
!!}
@endif
@if ($user->google_2fa_secret)
{!! Former::checkbox('enable_two_factor')
->help(trans('texts.enable_two_factor_help'))
->text(trans('texts.enable'))
->value(1) !!}
@elseif ($user->phone)
{!! Former::plaintext('enable_two_factor')->value(
Button::primary(trans('texts.enable'))->asLinkTo(url('settings/enable_two_factor'))->small()
)->help('enable_two_factor_help') !!}
@else
{!! Former::plaintext('enable_two_factor')
->value('<span class="text-muted">' . trans('texts.set_phone_for_two_factor') . '</span>') !!}
@endif
{!! Former::checkbox('dark_mode')
->help(trans('texts.dark_mode_help'))
->text(trans('texts.enable'))

View File

@ -47,6 +47,11 @@
<script type="text/javascript">
$(function() {
$('#email').focus();
$('.form-signin').submit(function() {
$('button.btn-success').prop('disabled', true);
});
})
</script>

View File

@ -0,0 +1,41 @@
@extends('login')
@section('form')
@include('partials.warn_session', ['redirectTo' => '/logout?reason=inactive'])
<div class="container">
{!! Former::open()
->rules(['totp' => 'required'])
->addClass('form-signin') !!}
<h2 class="form-signin-heading">
{{ trans('texts.enable_two_factor') }}
</h2>
<hr class="green">
{!! Former::text('totp')
->placeholder(trans('texts.one_time_password'))
->autofocus()
->forceValue('')
->raw() !!}
{!! Button::success(trans('texts.submit'))
->withAttributes(['id' => 'loginButton', 'class' => 'green'])
->large()->submit()->block() !!}
@if (count($errors->all()))
<br/>
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</div>
@endif
{!! Former::close() !!}
</div>
@endsection

View File

@ -41,4 +41,13 @@
{!! Former::close() !!}
</div>
@endsection
<script type="text/javascript">
$(function() {
$('.form-signin').submit(function() {
$('button.btn-success').prop('disabled', true);
});
})
</script>
@endsection

View File

@ -0,0 +1,31 @@
@extends('header')
@section('content')
@parent
@if (Utils::isAdmin())
@include('accounts.nav', ['selected' => ACCOUNT_USER_DETAILS])
@endif
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.two_factor_setup') !!}</h3>
</div>
<div class="panel-body form-padding-right">
<div class="text-center">
<img src="{{ $qrCode }}" alt="">
<p>{!! trans('texts.two_factor_setup_help', ['code' => $secret, 'link' => link_to('https://github.com/antonioribeiro/google2fa#google-authenticator-apps', 'Google Authenticator', ['target' => '_blank'])]) !!}</p>
</div>
<p>&nbsp;</p>
<center>
{!! Button::normal(trans('texts.return_to_settings'))->large()->asLinkTo(url('settings/user_details')) !!}
</center>
</div>
</div>
</div>
</div>
@stop