diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index b2cc90c0..7a20ef53 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -177,8 +177,18 @@ class AuthController extends Controller if (config('mail.driver') == "") return json(trans('auth.forgot.close'), 1); - if (Session::has('last_mail_time') && (time() - session('last_mail_time')) < 60) - return json(trans('auth.forgot.frequent-mail'), 1); + $rateLimit = 180; + $lastMailCacheKey = sha1('last_mail_'.Utils::getClientIp()); + $remain = $rateLimit + Cache::get($lastMailCacheKey, 0) - time(); + + // Rate limit + if ($remain > 0) { + return json([ + 'errno' => 2, + 'msg' => trans('auth.forgot.frequent-mail'), + 'remain' => $remain + ]); + } // Get user instance $user = $users->get($request->input('email'), 'email'); @@ -199,8 +209,6 @@ class AuthController extends Controller $m->from(config('mail.username'), $site_name); $m->to($request->input('email'))->subject(trans('auth.forgot.mail.title', ['sitename' => $site_name])); }); - - Log::info("[Password Reset] Mail has been sent to [{$request->input('email')}] with token [$token]"); } catch (\Exception $e) { // Write the exception to log report($e); @@ -208,7 +216,7 @@ class AuthController extends Controller return json(trans('auth.forgot.failed', ['msg' => $e->getMessage()]), 2); } - Session::put('last_mail_time', time()); + Cache::put($lastMailCacheKey, time(), 60); return json(trans('auth.forgot.success'), 0); } diff --git a/resources/assets/src/js/__tests__/auth.test.js b/resources/assets/src/js/__tests__/auth.test.js index 731776ea..fdf5cc68 100644 --- a/resources/assets/src/js/__tests__/auth.test.js +++ b/resources/assets/src/js/__tests__/auth.test.js @@ -232,6 +232,8 @@ describe('tests for "register" module', () => { }); }); +jest.useFakeTimers(); + describe('tests for "forgot" module', () => { const modulePath = '../auth/forgot'; @@ -263,10 +265,20 @@ describe('tests for "forgot" module', () => {
- + `; - require(modulePath); + require(modulePath)(); + expect(setInterval).toHaveBeenCalledTimes(1); + jest.runTimersToTime(1000); + + $('button').click(); + expect($('button').prop('disabled')).toBe(true); + expect(fetch).not.toBeCalled(); + + jest.runTimersToTime(60000); + expect($('button').html()).toBe('auth.send'); + expect($('button').prop('disabled')).toBe(false); $('button').click(); expect(trans).toBeCalledWith('auth.emptyEmail'); @@ -294,7 +306,7 @@ describe('tests for "forgot" module', () => { captcha: 'captcha' } })); - expect($('button').html()).toBe('auth.send'); + expect($('button').html()).toEqual(expect.stringContaining('auth.send')); expect($('button').prop('disabled')).toBe(true); expect(showMsg).toBeCalledWith('success', 'success'); @@ -310,6 +322,8 @@ describe('tests for "forgot" module', () => { }); }); +jest.useRealTimers(); + describe('tests for "reset" module', () => { const modulePath = '../auth/reset'; diff --git a/resources/assets/src/js/auth/forgot.js b/resources/assets/src/js/auth/forgot.js index cf391d77..7f5e5336 100644 --- a/resources/assets/src/js/auth/forgot.js +++ b/resources/assets/src/js/auth/forgot.js @@ -24,7 +24,7 @@ $('#forgot-button').click(e => { } })(data, async () => { try { - const { errno, msg } = await fetch({ + const { errno, msg, remain } = await fetch({ type: 'POST', url: url('auth/forgot'), dataType: 'json', @@ -32,20 +32,55 @@ $('#forgot-button').click(e => { beforeSend: () => { $('#forgot-button').html( ' ' + trans('auth.sending') - ).prop('disabled', 'disabled'); + ).prop('disabled', true); } }); + if (errno === 0) { showMsg(msg, 'success'); - $('#forgot-button').html(trans('auth.send')).prop('disabled', 'disabled'); + showRemainTimeIndicator(180); } else { showMsg(msg, 'warning'); refreshCaptcha(); - $('#forgot-button').html(trans('auth.send')).prop('disabled', ''); + + if (remain) { + showRemainTimeIndicator(remain); + } else { + $('#forgot-button').html(trans('auth.send')).prop('disabled', false); + } } } catch (error) { showAjaxError(error); - $('#forgot-button').html(trans('auth.send')).prop('disabled', ''); + $('#forgot-button').html(trans('auth.send')).prop('disabled', false); } }); }); + +function showRemainTimeIndicator(seconds, intervalID) { + // Get remain time from elem data if not specified + if (seconds === undefined) { + seconds = $('#forgot-button').data('remain'); + } + + if (seconds > 0) { + $('#forgot-button').html(`${trans('auth.send')} (${seconds})`).prop('disabled', true); + } else { + $('#forgot-button').html(trans('auth.send')).prop('disabled', false); + // Stop timer + if (intervalID) clearInterval(intervalID); + } + + // Create timer for decreasing remain time by second + if (! intervalID) { + const intervalID = window.setInterval(function () { + showRemainTimeIndicator(--seconds, intervalID); + }, 1000); + } +} + +// Start timer +$(document).ready(() => showRemainTimeIndicator()); + +if (process.env.NODE_ENV === 'test') { + module.exports = showRemainTimeIndicator; +} diff --git a/resources/lang/en/auth.yml b/resources/lang/en/auth.yml index 10de14dd..2c870992 100644 --- a/resources/lang/en/auth.yml +++ b/resources/lang/en/auth.yml @@ -29,7 +29,7 @@ forgot: message: We will send you an E-mail to verify. login-link: I do remember it close: Password resetting is not available now. - frequent-mail: You click the send button too fast. Wait for 60 secs, guy. + frequent-mail: You click the send button too fast. Wait for some minutes, guy. unregistered: The email address is not registered. success: Mail sent, please check your inbox. The link will be expired in 1 hour. failed: Fail to send mail, detailed message :msg diff --git a/resources/lang/zh_CN/auth.yml b/resources/lang/zh_CN/auth.yml index ff92d49f..a876c26f 100644 --- a/resources/lang/zh_CN/auth.yml +++ b/resources/lang/zh_CN/auth.yml @@ -29,7 +29,7 @@ forgot: message: 我们将会向您发送一封验证邮件 login-link: 我又想起来了 close: 本站已关闭重置密码功能 - frequent-mail: 你邮件发送得太频繁啦,过 60 秒后再点发送吧 + frequent-mail: 你邮件发送得太频繁啦,过会儿再点发送吧 unregistered: 该邮箱尚未注册 success: 邮件已发送,一小时内有效,请注意查收。 failed: 邮件发送失败,详细信息::msg diff --git a/resources/views/auth/forgot.tpl b/resources/views/auth/forgot.tpl index e6c53fa8..a0c5583d 100644 --- a/resources/views/auth/forgot.tpl +++ b/resources/views/auth/forgot.tpl @@ -42,8 +42,11 @@ {{ trans('auth.forgot.login-link') }} +