Remove captcha login flow and allow user email updates

This commit is contained in:
Boen_Shi 2026-07-02 20:04:36 +08:00
parent 4682048dc5
commit 00c8b2e1d1
7 changed files with 8 additions and 52 deletions

View File

@ -13,6 +13,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
#[Apidoc\Group('后台')]
#[Apidoc\Title('用户与邀请码')]
@ -63,6 +64,7 @@ final class UserController extends Controller
$user = $this->resolveUser($user);
$data = $request->validate([
'name' => ['sometimes', 'string', 'max:50'],
'email' => ['sometimes', 'email', 'max:120', Rule::unique('users', 'email')->ignore($user->id)],
'role' => ['sometimes', 'in:admin,teacher,user'],
'is_active' => ['sometimes', 'boolean'],
'password' => ['nullable', 'string', 'min:6'],

View File

@ -13,7 +13,6 @@ use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;
use Tymon\JWTAuth\Facades\JWTAuth;
@ -76,38 +75,23 @@ final class AuthController extends Controller
$data = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
'captcha' => ['nullable', 'string'],
]);
$key = 'login:'.$request->ip().':'.$data['email'];
$user = User::query()->where('email', $data['email'])->first();
if (RateLimiter::tooManyAttempts($key, 5) || ($user?->failed_login_count ?? 0) >= 5) {
$captcha = (string) ($data['captcha'] ?? '');
$expectedCaptcha = (string) session('captcha', '');
if ($captcha === '' || $expectedCaptcha === '' || $captcha !== $expectedCaptcha) {
return ApiResponse::error('请输入验证码', 429, 429, ['captcha_required' => true]);
}
}
if (! $user || ! Hash::check($data['password'], $user->password)) {
RateLimiter::hit($key, 300);
$user?->update([
'failed_login_count' => $user->failed_login_count + 1,
'last_failed_login_at' => now(),
]);
return ApiResponse::error('账号或密码错误', 422, 422, [
'captcha_required' => RateLimiter::attempts($key) >= 5,
]);
return ApiResponse::error('账号或密码错误', 422, 422);
}
if (! $user->is_active) {
return ApiResponse::error('账号已被禁用', 403, 403);
}
RateLimiter::clear($key);
$user->update(['failed_login_count' => 0, 'last_login_at' => now()]);
OperationLog::create([

View File

@ -189,6 +189,7 @@ export function createUser(payload: { name: string; email: string; password: str
export function updateUser(userId: number, payload: {
name?: string
email?: string
role?: string
is_active?: boolean
password?: string

View File

@ -4,7 +4,6 @@ import type { User } from '@/types/api'
export interface LoginPayload {
email: string
password: string
captcha?: string
}
export interface LoginResult {
@ -31,7 +30,3 @@ export function register(payload: {
export function me() {
return apiGet<User>('/api/auth/me')
}
export function captcha() {
return apiGet<{ captcha: string; expires_in: number }>('/api/auth/captcha')
}

View File

@ -3,17 +3,13 @@ import { reactive, shallowRef } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
import { captcha } from '@/api/auth'
const router = useRouter()
const auth = useAuthStore()
const loading = shallowRef(false)
const captchaText = shallowRef('')
const captchaRequired = shallowRef(false)
const form = reactive({
email: '',
password: '',
captcha: '',
})
async function submit() {
@ -22,22 +18,11 @@ async function submit() {
await auth.login(form.email, form.password)
ElMessage.success('登录成功')
await router.push('/quiz')
} catch (error: any) {
captchaRequired.value = Boolean(error.response?.data?.data?.captcha_required)
if (captchaRequired.value) {
await loadCaptcha()
}
throw error
} finally {
loading.value = false
}
}
async function loadCaptcha() {
const response = await captcha()
captchaText.value = response.data.captcha
}
</script>
<template>
@ -55,12 +40,6 @@ async function loadCaptcha() {
<ElFormItem label="密码">
<ElInput v-model="form.password" type="password" autocomplete="current-password" show-password />
</ElFormItem>
<ElFormItem v-if="captchaRequired" label="验证码">
<div class="captcha-row">
<ElInput v-model="form.captcha" />
<ElButton @click="loadCaptcha">{{ captchaText || '获取验证码' }}</ElButton>
</div>
</ElFormItem>
<ElButton type="primary" :loading="loading" class="w-full" @click="submit">登录</ElButton>
<div class="login-links">
<RouterLink to="/register">邀请码注册</RouterLink>
@ -133,12 +112,6 @@ async function loadCaptcha() {
cursor: pointer;
}
.captcha-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
}
@media (max-width: 720px) {
.login-panel {
grid-template-columns: 1fr;

View File

@ -70,6 +70,7 @@ async function saveUser() {
} else if (editingUserId.value !== null) {
await updateUser(editingUserId.value, {
name: userForm.name,
email: userForm.email,
role: userForm.role,
is_active: userForm.is_active,
password: userForm.password || undefined,

View File

@ -158,8 +158,8 @@ onMounted(loadResources)
<ElButton type="primary" @click="start(bank, 'sequence')">顺序刷题</ElButton>
<ElButton plain @click="openRandom(bank)">随机刷题</ElButton>
<ElButton plain type="warning" :disabled="!bank.wrong_questions_count" @click="router.push(`/quiz/wrong-questions?bank_id=${bank.id}`)">错题列表</ElButton>
<ElButton plain type="warning" :disabled="!bank.wrong_questions_count" @click="reviewWrong(bank)">重刷错题</ElButton>
<ElButton plain @click="openHistory(bank)">历史回顾</ElButton>
<ElButton plain type="warning" :disabled="!bank.wrong_questions_count" @click="reviewWrong(bank)">重刷错题</ElButton>
</div>
</article>
</div>
@ -289,7 +289,7 @@ onMounted(loadResources)
.resource-card {
min-height: 176px;
display: grid;
grid-template-columns: minmax(0, 1fr) 180px;
grid-template-columns: minmax(0, 1fr) 260px;
gap: 18px;
align-items: center;
padding: 22px;
@ -322,7 +322,7 @@ onMounted(loadResources)
.resource-actions {
display: grid;
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}