From 4d901b3270451970f75c035c0a5f8083d3f8d3f8 Mon Sep 17 00:00:00 2001 From: romanetar Date: Tue, 17 Jun 2025 16:36:24 +0200 Subject: [PATCH 1/5] feat: propagate can_login to UI and give more feedback Signed-off-by: romanetar --- app/Http/Controllers/UserController.php | 5 +++-- resources/js/login/login.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d40af7ac..7631a2bb 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -258,14 +258,15 @@ public function getAccount() $user = $this->auth_service->getUserByUsername($email); - if (is_null($user) || !$user->canLogin()) + if (is_null($user)) throw new EntityNotFoundException(); return $this->ok( [ + 'can_login' => $user->canLogin(), 'pic' => $user->getPic(), 'full_name' => $user->getFullName(), - 'has_password_set' => $user->hasPasswordSet() + 'has_password_set' => $user->hasPasswordSet(), ] ); } catch (ValidationException $ex) { diff --git a/resources/js/login/login.js b/resources/js/login/login.js index ca9ee367..3abcdf44 100644 --- a/resources/js/login/login.js +++ b/resources/js/login/login.js @@ -533,10 +533,10 @@ class LoginPage extends React.Component { ...this.state, user_pic: response.pic, user_fullname: response.full_name, - user_verified: true, + user_verified: response.can_login, authFlow: response.has_password_set ? password_flow : otp_flow, errors: { - email: '', + email: response.can_login ? '' : 'User is inactive or email verification is pending.', otp: '', password: '' }, From 92b425c395a5d185c7dd021670c8abf3fdeaf011 Mon Sep 17 00:00:00 2001 From: romanetar Date: Wed, 18 Jun 2025 17:32:45 +0200 Subject: [PATCH 2/5] feat: add more feedback to UI on inactive and/or unverified accounts Signed-off-by: romanetar --- app/Http/Controllers/UserController.php | 45 ++++++- app/Services/Auth/UserService.php | 4 +- resources/js/components/custom_snackbar.js | 18 +++ resources/js/login/actions.js | 8 ++ resources/js/login/login.js | 150 +++++++++++++++++---- resources/views/auth/login.blade.php | 4 +- routes/web.php | 3 + 7 files changed, 202 insertions(+), 30 deletions(-) create mode 100644 resources/js/components/custom_snackbar.js diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 7631a2bb..599a4c49 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -21,7 +21,9 @@ use App\ModelSerializers\SerializerRegistry; use Auth\Exceptions\AuthenticationException; use Auth\Exceptions\UnverifiedEmailMemberException; +use App\Services\Auth\IUserService as AuthUserService; use Exception; +use Illuminate\Http\Request as LaravelRequest; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Redirect; @@ -86,6 +88,10 @@ final class UserController extends OpenIdController * @var IUserService */ private $user_service; + /** + * @var AuthUserService + */ + private $auth_user_service; /** * @var IUserActionService */ @@ -132,6 +138,7 @@ final class UserController extends OpenIdController * @param ITrustedSitesService $trusted_sites_service * @param DiscoveryController $discovery * @param IUserService $user_service + * @param AuthUserService $auth_user_service * @param IUserActionService $user_action_service * @param IClientRepository $client_repository * @param IApiScopeRepository $scope_repository @@ -150,6 +157,7 @@ public function __construct ITrustedSitesService $trusted_sites_service, DiscoveryController $discovery, IUserService $user_service, + AuthUserService $auth_user_service, IUserActionService $user_action_service, IClientRepository $client_repository, IApiScopeRepository $scope_repository, @@ -160,8 +168,6 @@ public function __construct LoginHintProcessStrategy $login_hint_process_strategy ) { - - $this->openid_memento_service = $openid_memento_service; $this->oauth2_memento_service = $oauth2_memento_service; $this->auth_service = $auth_service; @@ -169,6 +175,7 @@ public function __construct $this->trusted_sites_service = $trusted_sites_service; $this->discovery = $discovery; $this->user_service = $user_service; + $this->auth_user_service = $auth_user_service; $this->user_action_service = $user_action_service; $this->client_repository = $client_repository; $this->scope_repository = $scope_repository; @@ -263,7 +270,8 @@ public function getAccount() return $this->ok( [ - 'can_login' => $user->canLogin(), + 'is_active' => $user->isActive(), + 'is_verified' => $user->isEmailVerified(), 'pic' => $user->getPic(), 'full_name' => $user->getFullName(), 'has_password_set' => $user->hasPasswordSet(), @@ -351,6 +359,37 @@ public function emitOTP() } } + /** + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function resendVerificationEmail(LaravelRequest $request) + { + try { + $payload = $request->all(); + $validator = Validator::make($payload, [ + 'email' => 'required|string|email|max:255' + ]); + + if (!$validator->passes()) { + return $this->error412($validator->getMessageBag()->getMessages()); + } + $this->auth_user_service->resendVerificationEmail($payload); + return $this->ok(); + } + catch (ValidationException $ex) { + Log::warning($ex); + return $this->error412($ex->getMessages()); + } + catch (EntityNotFoundException $ex) { + Log::warning($ex); + return $this->error404(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + public function postLogin() { $max_login_attempts_2_show_captcha = $this->server_configuration_service->getConfigValue("MaxFailed.LoginAttempts.2ShowCaptcha"); diff --git a/app/Services/Auth/UserService.php b/app/Services/Auth/UserService.php index 5e4dc60a..e432bf0e 100644 --- a/app/Services/Auth/UserService.php +++ b/app/Services/Auth/UserService.php @@ -224,8 +224,10 @@ public function verifyEmail(string $token): User { return $this->tx_service->transaction(function () use ($token) { $user = $this->user_repository->getByVerificationEmailToken($token); - if (is_null($user)) + + if (is_null($user) || !$user->isActive()) throw new EntityNotFoundException(); + $user->verifyEmail(); try { diff --git a/resources/js/components/custom_snackbar.js b/resources/js/components/custom_snackbar.js new file mode 100644 index 00000000..06e236cc --- /dev/null +++ b/resources/js/components/custom_snackbar.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { Snackbar } from '@material-ui/core'; +import MuiAlert from '@material-ui/lab/Alert'; + +function Alert(props) { + return ; +} + +const CustomSnackbar = ({ message, severity = 'info', onClose }) => { + return ( + + + {message} + + + ); +}; +export default CustomSnackbar; \ No newline at end of file diff --git a/resources/js/login/actions.js b/resources/js/login/actions.js index 5eeb3d1f..d0d20ad9 100644 --- a/resources/js/login/actions.js +++ b/resources/js/login/actions.js @@ -19,3 +19,11 @@ export const emitOTP = (email, token, connection = 'email', send='code') => { return postRawRequest(window.EMIT_OTP_ENDPOINT)(params, {'X-CSRF-TOKEN': token}); } + +export const resendVerificationEmail = (email, token) => { + const params = { + email: email + }; + + return postRawRequest(window.RESEND_VERIFICATION_EMAIL_ENDPOINT)(params, {'X-CSRF-TOKEN': token}); +} diff --git a/resources/js/login/login.js b/resources/js/login/login.js index 3abcdf44..323095bf 100644 --- a/resources/js/login/login.js +++ b/resources/js/login/login.js @@ -12,7 +12,7 @@ import Container from '@material-ui/core/Container'; import Chip from '@material-ui/core/Chip'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '@material-ui/core/Checkbox'; -import {verifyAccount, emitOTP} from './actions'; +import {verifyAccount, emitOTP, resendVerificationEmail} from './actions'; import {MuiThemeProvider, createTheme} from '@material-ui/core/styles'; import DividerWithText from '../components/divider_with_text'; import Visibility from '@material-ui/icons/Visibility'; @@ -21,7 +21,7 @@ import InputAdornment from '@material-ui/core/InputAdornment'; import IconButton from '@material-ui/core/IconButton'; import { emailValidator } from '../validator'; import Grid from '@material-ui/core/Grid'; -import Swal from 'sweetalert2' +import CustomSnackbar from "../components/custom_snackbar"; import Banner from '../components/banner/banner'; import OtpInput from 'react-otp-input'; import {handleThirdPartyProvidersVerbiage} from '../utils'; @@ -29,7 +29,7 @@ import {handleThirdPartyProvidersVerbiage} from '../utils'; import styles from './login.module.scss' import "./third_party_identity_providers.scss"; -const EmailInputForm = ({ onValidateEmail, onHandleUserNameChange, disableInput, emailError }) => { +const EmailInputForm = ({ value, onValidateEmail, onHandleUserNameChange, disableInput, emailError }) => { return ( { let { response } = payload; + let error = ''; + if (response.is_active === false) { + error = 'Your user account is currently inactive. Please contact support for further assistance.'; + } else if (response.is_active === true && response.is_verified === false) { + error = 'Your email has not been verified. Please check your inbox or resend the verification email.'; + } + this.setState({ ...this.state, user_pic: response.pic, user_fullname: response.full_name, - user_verified: response.can_login, + user_verified: true, + user_active: response.is_active, + email_verified: response.is_verified, authFlow: response.has_password_set ? password_flow : otp_flow, errors: { - email: response.can_login ? '' : 'User is inactive or email verification is pending.', + email: error, otp: '', password: '' }, @@ -573,14 +600,45 @@ class LoginPage extends React.Component { return true; } + resendVerificationEmail(ev) { + ev.preventDefault(); + let {user_name} = this.state; + user_name = user_name?.trim(); + + resendVerificationEmail(user_name, this.props.token).then((payload) => { + this.showAlert( + 'We\'ve sent you a verification email. Please check your inbox and click the link to verify your account.', + 'info'); + }, (error) => { + let {response, status} = error; + if(status === 412){ + const {errors} = response.body; + const allErrors = Object.values(errors ?? {}) + ?.flat?.() + ?.join(', ') || ''; + this.showAlert(allErrors, 'error'); + return; + } + this.showAlert('Oops... Something went wrong!', 'error'); + }); + } + handleDelete(ev) { ev.preventDefault(); this.setState({ - ...this.state, user_name: null, user_pic: null, user_fullname: null, user_verified: false, authFlow: "password", errors: { + ...this.state, + user_name: null, + user_pic: null, + user_fullname: null, + user_verified: false, + user_active: null, + email_verified: null, + authFlow: "password", + errors: { email: '', otp: '', password: '' - }, + } }); return false; } @@ -594,6 +652,30 @@ class LoginPage extends React.Component { ev.preventDefault(); } + existingUserCanContinue() { + const { user_active, email_verified } = this.state; + return user_active !== false && email_verified !== false; + } + + getSignUpSignInTitle() { + const { errors, user_active } = this.state; + + if (errors.email && this.existingUserCanContinue()) { + return 'Create an account for:'; + } + return 'Sign in'; + } + + handleSnackbarClose() { + this.setState({ + ...this.state, + notification: { + message: null, + severity: 'info' + } + }); + }; + render() { return ( @@ -607,7 +689,7 @@ class LoginPage extends React.Component { src={this.props.appLogo}/> - {this.state.errors.email ? 'Create an account for:' : 'Sign in'} + {this.getSignUpSignInTitle()} {this.state.user_fullname && } @@ -617,16 +699,17 @@ class LoginPage extends React.Component { onDelete={this.handleDelete}/> } - {!this.state.user_verified && + {(!this.state.user_verified || !this.existingUserCanContinue()) && <> {this.state.allowNativeAuth && } - {this.state.errors.email == '' && + {this.state.errors.email === '' && this.props.thirdPartyProviders.length > 0 && - + {this.existingUserCanContinue() && + + } + { + this.state.email_verified === false && + + } } - {this.state.user_verified && this.state.authFlow == password_flow && + {this.state.user_verified && this.existingUserCanContinue() && this.state.authFlow === password_flow && // proceed to ask for password ( 2nd step ) <> } - {this.state.user_verified && this.state.authFlow == otp_flow && + {this.state.user_verified && this.existingUserCanContinue() && this.state.authFlow === otp_flow && // proceed to ask for password ( 2nd step ) <> } + diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 12ae4b17..1e4a4c9a 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -33,6 +33,7 @@ formAction: '{{ URL::action("UserController@postLogin") }}', accountVerifyAction : '{{URL::action("UserController@getAccount")}}', emitOtpAction : '{{URL::action("UserController@emitOTP")}}', + resendVerificationEmailAction: '{{ URL::action("UserController@resendVerificationEmail") }}', authError: authError, captchaPublicKey: '{{ Config::get("recaptcha.public_key") }}', flow: 'password', @@ -76,7 +77,8 @@ @endif window.VERIFY_ACCOUNT_ENDPOINT = config.accountVerifyAction; - window.EMIT_OTP_ENDPOINT = config.emitOtpAction; + window.EMIT_OTP_ENDPOINT = config.emitOtpAction; + window.RESEND_VERIFICATION_EMAIL_ENDPOINT = config.resendVerificationEmailAction; {!! script_to('assets/login.js') !!} @append \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index eeaee609..f8473c4c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -46,6 +46,9 @@ Route::get('', "UserController@getLogin"); Route::post('account-verify', [ 'middleware' => ['csrf'], 'uses' => 'UserController@getAccount']); Route::post('otp', ['middleware' => ['csrf'], 'uses' => 'UserController@emitOTP']); + Route::group(array('prefix' => 'verification'), function () { + Route::post('resend', ['middleware' => ['csrf'], 'uses' => 'UserController@resendVerificationEmail']); + }); Route::post('', ['middleware' => 'csrf', 'uses' => 'UserController@postLogin']); Route::get('cancel', "UserController@cancelLogin"); Route::group(array('prefix' => '{provider}'), function () { From c9e007add8c32542c82fde410ea036650d99c2da Mon Sep 17 00:00:00 2001 From: romanetar Date: Fri, 11 Jul 2025 22:16:03 +0200 Subject: [PATCH 3/5] fix: login flow ui tweaks Signed-off-by: romanetar --- resources/js/components/custom_snackbar.js | 6 +++++- resources/js/login/login.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/js/components/custom_snackbar.js b/resources/js/components/custom_snackbar.js index 06e236cc..ff8d9bf0 100644 --- a/resources/js/components/custom_snackbar.js +++ b/resources/js/components/custom_snackbar.js @@ -8,7 +8,11 @@ function Alert(props) { const CustomSnackbar = ({ message, severity = 'info', onClose }) => { return ( - + {message} diff --git a/resources/js/login/login.js b/resources/js/login/login.js index 323095bf..c92993b1 100644 --- a/resources/js/login/login.js +++ b/resources/js/login/login.js @@ -608,7 +608,7 @@ class LoginPage extends React.Component { resendVerificationEmail(user_name, this.props.token).then((payload) => { this.showAlert( 'We\'ve sent you a verification email. Please check your inbox and click the link to verify your account.', - 'info'); + 'success'); }, (error) => { let {response, status} = error; if(status === 412){ From 893d2f6a008f67a017fd81f0c35702b46491d2fe Mon Sep 17 00:00:00 2001 From: romanetar Date: Mon, 14 Jul 2025 16:30:41 +0200 Subject: [PATCH 4/5] fix: PR review feedback Signed-off-by: romanetar --- app/Services/Auth/UserService.php | 6 +++-- resources/js/login/login.js | 23 +++++++++-------- resources/js/utils.js | 42 +++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/app/Services/Auth/UserService.php b/app/Services/Auth/UserService.php index e432bf0e..4910a878 100644 --- a/app/Services/Auth/UserService.php +++ b/app/Services/Auth/UserService.php @@ -218,15 +218,17 @@ public function registerUser(array $payload, ?OAuth2OTP $otp = null):User * @param string $token * @return User * @throws EntityNotFoundException - * @throws ValidationException + * @throws ValidationException|\Exception */ public function verifyEmail(string $token): User { return $this->tx_service->transaction(function () use ($token) { $user = $this->user_repository->getByVerificationEmailToken($token); - if (is_null($user) || !$user->isActive()) + if (is_null($user) || !$user->isActive()) { + Log::warning(sprintf("UserService::verifyEmail user with id %s is not active", $user->getId())); throw new EntityNotFoundException(); + } $user->verifyEmail(); diff --git a/resources/js/login/login.js b/resources/js/login/login.js index c92993b1..3f17824b 100644 --- a/resources/js/login/login.js +++ b/resources/js/login/login.js @@ -24,7 +24,7 @@ import Grid from '@material-ui/core/Grid'; import CustomSnackbar from "../components/custom_snackbar"; import Banner from '../components/banner/banner'; import OtpInput from 'react-otp-input'; -import {handleThirdPartyProvidersVerbiage} from '../utils'; +import {handleErrorResponse, handleThirdPartyProvidersVerbiage} from '../utils'; import styles from './login.module.scss' import "./third_party_identity_providers.scss"; @@ -605,21 +605,22 @@ class LoginPage extends React.Component { let {user_name} = this.state; user_name = user_name?.trim(); + if (!user_name) { + this.showAlert( + 'Something went wrong while trying to resend the verification email. Please try again later.', + 'error'); + return; + } + resendVerificationEmail(user_name, this.props.token).then((payload) => { this.showAlert( 'We\'ve sent you a verification email. Please check your inbox and click the link to verify your account.', 'success'); }, (error) => { - let {response, status} = error; - if(status === 412){ - const {errors} = response.body; - const allErrors = Object.values(errors ?? {}) - ?.flat?.() - ?.join(', ') || ''; - this.showAlert(allErrors, 'error'); - return; - } - this.showAlert('Oops... Something went wrong!', 'error'); + handleErrorResponse(error, (title, messageLines, type) => { + const message = (messageLines ?? []).join(', ') + this.showAlert(`${title}: ${message}`, type); + }); }); } diff --git a/resources/js/utils.js b/resources/js/utils.js index 916f9695..3a45f2e9 100644 --- a/resources/js/utils.js +++ b/resources/js/utils.js @@ -1,20 +1,36 @@ import Swal from "sweetalert2"; -export const handleErrorResponse = (err) => { - if(err.status === 412){ - // validation error - let msg= ''; - for (let [key, value] of Object.entries(err.response.body.errors)) { - if (isNaN(key)) { - msg += key + ': '; - } +const createErrorHandler = (customHandler = null) => { + const showMessage = (title, message, type) => { + if (customHandler && typeof customHandler === 'function') { + return customHandler(title, message, type); + } + const formattedMessage = Array.isArray(message) ? message.join('
') : message; + return Swal(title, formattedMessage, type); + }; - msg += value + '
'; + return (err) => { + if (err.status === 412) { + // validation error + const messageLines = []; + for (let [key, value] of Object.entries(err.response.body.errors)) { + let line = ''; + if (isNaN(key)) { + line += key + ': '; + } + line += value; + messageLines.push(line); + } + return showMessage("Validation error", messageLines, "warning"); } - return Swal("Validation error", msg, "warning"); - } - return Swal("Something went wrong!", null, "error"); -} + return showMessage("Something went wrong!", null, "error"); + }; +}; + +export const handleErrorResponse = (err, customHandler = null) => { + const errorHandler = createErrorHandler(customHandler); + return errorHandler(err); +}; /** * From f4ab6ebfbc6e371e272af542a12858f9c9467afc Mon Sep 17 00:00:00 2001 From: sebastian marcet Date: Mon, 14 Jul 2025 11:41:50 -0300 Subject: [PATCH 5/5] feat: add login attempt counter (#82) * feat: add login attempt counter Change-Id: Icd5ceb7f886ffa918449c872047ce4f279ee9c81 * fix: remove hard coded test value Change-Id: Ib765702819565ec82e1bc60361aee27aa492350b --- app/Http/Controllers/UserController.php | 15 ++++-- .../LockUserCounterMeasure.php | 2 +- app/libs/Auth/Factories/UserFactory.php | 1 - app/libs/Auth/Models/User.php | 3 ++ resources/js/login/login.js | 53 +++++++++++++++++-- resources/views/auth/login.blade.php | 18 +++++-- 6 files changed, 76 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 599a4c49..5b52d7aa 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -393,6 +393,7 @@ public function resendVerificationEmail(LaravelRequest $request) public function postLogin() { $max_login_attempts_2_show_captcha = $this->server_configuration_service->getConfigValue("MaxFailed.LoginAttempts.2ShowCaptcha"); + $max_login_failed_attempts = intval($this->server_configuration_service->getConfigValue("MaxFailed.Login.Attempts")); $login_attempts = 0; $username = ''; $user = null; @@ -479,13 +480,15 @@ public function postLogin() ( [ 'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha, + 'max_login_failed_attempts' => $max_login_failed_attempts, 'login_attempts' => $login_attempts, 'error_message' => $ex->getMessage(), 'user_fullname' => !is_null($user) ? $user->getFullName() : "", 'user_pic' => !is_null($user) ? $user->getPic(): "", 'user_verified' => true, 'username' => $username, - 'flow' => $flow + 'flow' => $flow, + 'user_is_active' => !is_null($user) ? ($user->isActive() ? 1 : 0) : 0 ] ); } @@ -495,6 +498,7 @@ public function postLogin() // validator errors $response_data = [ 'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha, + 'max_login_failed_attempts' => $max_login_failed_attempts, 'login_attempts' => $login_attempts, 'validator' => $validator, ]; @@ -506,7 +510,8 @@ public function postLogin() if(!is_null($user)){ $response_data['user_fullname'] = $user->getFullName(); $response_data['user_pic'] = $user->getPic(); - $response_data['user_verified'] = true; + $response_data['user_verified'] = 1; + $response_data['user_is_active'] = $user->isActive() ? 1 : 0; } return $this->login_strategy->errorLogin @@ -521,9 +526,10 @@ public function postLogin() $response_data = [ 'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha, + 'max_login_failed_attempts' => $max_login_failed_attempts, 'login_attempts' => $login_attempts, 'username' => $username, - 'error_message' => $ex1->getMessage() + 'error_message' => $ex1->getMessage(), ]; if (is_null($user) && isset($data['username'])) { @@ -533,7 +539,8 @@ public function postLogin() if(!is_null($user)){ $response_data['user_fullname'] = $user->getFullName(); $response_data['user_pic'] = $user->getPic(); - $response_data['user_verified'] = true; + $response_data['user_verified'] = 1; + $response_data['user_is_active'] = $user->isActive() ? 1 : 0; } return $this->login_strategy->errorLogin diff --git a/app/Services/SecurityPolicies/LockUserCounterMeasure.php b/app/Services/SecurityPolicies/LockUserCounterMeasure.php index fc310094..b38df7b3 100644 --- a/app/Services/SecurityPolicies/LockUserCounterMeasure.php +++ b/app/Services/SecurityPolicies/LockUserCounterMeasure.php @@ -77,7 +77,7 @@ public function trigger(array $params = []) $user_id = $params["user_id"]; $user = $this->repository->getById($user_id); $max_login_failed_attempts = intval($this->server_configuration->getConfigValue("MaxFailed.Login.Attempts")); - if (!is_null($user) && $user instanceof User) { + if ($user instanceof User) { //apply lock policy if (intval($user->getLoginFailedAttempt()) < $max_login_failed_attempts) { $this->user_service->updateFailedLoginAttempts($user->getId()); diff --git a/app/libs/Auth/Factories/UserFactory.php b/app/libs/Auth/Factories/UserFactory.php index 5602116c..b0ca3027 100644 --- a/app/libs/Auth/Factories/UserFactory.php +++ b/app/libs/Auth/Factories/UserFactory.php @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -use Auth\Group; use Auth\User; use Illuminate\Support\Facades\Auth; diff --git a/app/libs/Auth/Models/User.php b/app/libs/Auth/Models/User.php index b04b5d13..23f98918 100644 --- a/app/libs/Auth/Models/User.php +++ b/app/libs/Auth/Models/User.php @@ -1854,6 +1854,8 @@ public function activate():void { if(!$this->active) { $this->active = true; $this->spam_type = self::SpamTypeHam; + // reset it + $this->login_failed_attempt = 0; Event::dispatch(new UserSpamStateUpdated( $this->getId() ) @@ -1886,6 +1888,7 @@ public function verifyEmail(bool $send_email_verified_notice = true) $this->spam_type = self::SpamTypeHam; $this->active = true; $this->lock = false; + $this->login_failed_attempt = 0; $this->email_verified_date = new \DateTime('now', new \DateTimeZone('UTC')); if($send_email_verified_notice) diff --git a/resources/js/login/login.js b/resources/js/login/login.js index 3f17824b..00e4cb45 100644 --- a/resources/js/login/login.js +++ b/resources/js/login/login.js @@ -82,7 +82,10 @@ const PasswordInputForm = ({ captchaPublicKey, onChangeRecaptcha, handleEmitOtpAction, - forgotPasswordAction + forgotPasswordAction, + loginAttempts, + maxLoginFailedAttempts, + userIsActive }) => { return (
@@ -114,9 +117,46 @@ const PasswordInputForm = ({ ) }} /> - {passwordError && -

- } + {(() => { + const attempts = parseInt(loginAttempts, 10); + const maxAttempts = parseInt(maxLoginFailedAttempts, 10); + const attemptsLeft = maxAttempts - attempts; + + if (!passwordError) return null; + + if (attempts > 0 && attempts < maxAttempts && userIsActive) { + return ( + <> +

+ Incorrect password. You have {attemptsLeft} more attempt{attemptsLeft !== 1 ? 's' : ''} before your account is locked. +

+ + ); + } + + if (attempts > 0 && attempts === maxAttempts && userIsActive) { + return ( + <> +

+ Incorrect password. You have reached the maximum ({maxAttempts}) login attempts. Your account will be locked after another failed login. +

+ + ); + } + + if (attempts > 0 && attempts === maxAttempts && !userIsActive) { + return ( + <> +

+ Your account has been locked due to multiple failed login attempts. Please contact support to unlock it. +

+ + ); + } + + return

; + })()} +