diff --git a/app/Http/Controllers/Api/VerificationController.php b/app/Http/Controllers/Api/VerificationController.php new file mode 100644 index 0000000..d1b7e9d --- /dev/null +++ b/app/Http/Controllers/Api/VerificationController.php @@ -0,0 +1,71 @@ +validate([ + 'email' => 'required|email' + ]); + + $email = $request->input('email'); + $user = $request->user(); + + // 1. 检查总控制开关 + if (SysParam::where('alias', 'smtp_enabled')->value('body') !== '1') { + return response()->json([ + 'status' => 'error', + 'message' => '抱歉,当前系统未开启外部邮件发信服务,请联系管理员。' + ], 403); + } + + // 2. 检查是否有频率限制(同一用户或同一邮箱,60秒只允许发1次) + $throttleKey = 'email_throttle_' . $user->id; + if (Cache::has($throttleKey)) { + $ttl = Cache::ttl($throttleKey); + return response()->json([ + 'status' => 'error', + 'message' => "发送过于频繁,请等待 {$ttl} 秒后再试。" + ], 429); + } + + // 3. 生成 6 位随机验证码并缓存,有效期 5 分钟 + $code = mt_rand(100000, 999999); + $codeKey = 'email_verify_code_' . $user->id . '_' . $email; + Cache::put($codeKey, $code, now()->addMinutes(5)); + + // 设置频率锁,过期时间 60 秒 + Cache::put($throttleKey, true, now()->addSeconds(60)); + + // 4. 执行发信动作 + try { + Mail::raw("【飘落的流星】聊天室\n\n您正在试图绑定或修改您的验证邮箱。\n该操作的验证码为:{$code}\n打死不要告诉其他人哦!验证码5分钟内有效。", function ($msg) use ($email) { + $msg->to($email)->subject('飘落流星聊天室 - 绑定邮箱验证码'); + }); + + return response()->json([ + 'status' => 'success', + 'message' => '验证码已发送,请注意查收邮件。' + ]); + } catch (\Throwable $e) { + // 如果发信失败,主动接触频率限制锁方便用户下一次立重试 + Cache::forget($throttleKey); + return response()->json([ + 'status' => 'error', + 'message' => '邮件系统发送异常,请稍后再试: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index fd1e0a5..4d4ff6c 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -64,6 +64,30 @@ class UserController extends Controller { $user = Auth::user(); $data = $request->validated(); + + // 当用户试图更新邮箱,并且新邮箱不等于当前旧邮箱时启动验证码拦截 + if (isset($data['email']) && $data['email'] !== $user->email) { + // 首先判断系统开关是否开启,没开启直接禁止修改邮箱 + if (\App\Models\SysParam::where('alias', 'smtp_enabled')->value('body') !== '1') { + return response()->json(['status' => 'error', 'message' => '系统未开启邮件服务,当前禁止绑定/修改邮箱。'], 403); + } + + $emailCode = $request->input('email_code'); + if (empty($emailCode)) { + return response()->json(['status' => 'error', 'message' => '新邮箱需要验证码,请先获取并填写验证码。'], 422); + } + + // 获取缓存的验证码 + $codeKey = 'email_verify_code_' . $user->id . '_' . $data['email']; + $cachedCode = \Illuminate\Support\Facades\Cache::get($codeKey); + + if (!$cachedCode || $cachedCode != $emailCode) { + return response()->json(['status' => 'error', 'message' => '验证码不正确或已过期(有效期5分钟),请重新获取。'], 422); + } + + // 验证成功后,立即核销该验证码防止二次利用 + \Illuminate\Support\Facades\Cache::forget($codeKey); + } $user->update($data); diff --git a/resources/views/chat/partials/toolbar.blade.php b/resources/views/chat/partials/toolbar.blade.php index 3df347a..8ebf1cf 100644 --- a/resources/views/chat/partials/toolbar.blade.php +++ b/resources/views/chat/partials/toolbar.blade.php @@ -117,9 +117,21 @@
-
+ @if (\App\Models\SysParam::where('alias', 'smtp_enabled')->value('body') === '1') +
+ + + +
+ @endif @@ -203,6 +215,8 @@ const profileData = { sex: document.getElementById('set-sex').value, email: document.getElementById('set-email').value, + email_code: document.getElementById('set-email-code') ? document.getElementById('set-email-code') + .value : '', question: document.getElementById('set-question').value, answer: document.getElementById('set-answer').value, headface: @json(Auth::user()->usersf ?: '1.gif'), @@ -229,4 +243,71 @@ alert('网络异常'); } } + + /** + * 发送邮箱验证码 (带有 60s 倒计时机制防灌水) + */ + async function sendEmailCode() { + const emailInput = document.getElementById('set-email').value.trim(); + if (!emailInput) { + alert('请先填写邮箱地址后再获取验证码!'); + return; + } + + const btn = document.getElementById('btn-send-code'); + btn.disabled = true; + btn.innerText = '正在发送...'; + btn.style.opacity = '0.6'; + btn.style.cursor = 'not-allowed'; + + try { + const res = await fetch('{{ route('user.send_email_code') }}', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + email: emailInput + }) + }); + + const data = await res.json(); + + if (res.ok && data.status === 'success') { + alert(data.message || '验证码发送成功,请前往邮箱查收!(有效期5分钟)'); + + // 开始 60 秒防暴力点击倒计时 + let count = 60; + btn.innerText = count + 's 后重试'; + const timer = setInterval(() => { + count--; + if (count <= 0) { + clearInterval(timer); + btn.innerText = '获取验证码'; + btn.disabled = false; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + } else { + btn.innerText = count + 's 后重试'; + } + }, 1000); + + } else { + alert('发送失败: ' + (data.message || '系统繁忙')); + // 失败了立刻解除禁用以重新尝试 + btn.innerText = '获取验证码'; + btn.disabled = false; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + } + } catch (e) { + alert('网络异常,验证码发送请求失败。'); + btn.innerText = '获取验证码'; + btn.disabled = false; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + } + } diff --git a/routes/web.php b/routes/web.php index 505b7be..c0b5562 100644 --- a/routes/web.php +++ b/routes/web.php @@ -49,6 +49,7 @@ Route::middleware(['chat.auth'])->group(function () { // ---- 第七阶段:用户资料与特权管理 ---- Route::get('/user/{username}', [UserController::class, 'show'])->name('user.show'); Route::put('/user/profile', [UserController::class, 'updateProfile'])->name('user.update_profile'); + Route::post('/user/send-email-code', [\App\Http\Controllers\Api\VerificationController::class, 'sendEmailCode'])->name('user.send_email_code'); Route::put('/user/password', [UserController::class, 'changePassword'])->name('user.update_password'); Route::post('/user/{username}/kick', [UserController::class, 'kick'])->name('user.kick'); Route::post('/user/{username}/mute', [UserController::class, 'mute'])->name('user.mute');