Remove captcha login flow and allow user email updates
This commit is contained in:
parent
4682048dc5
commit
00c8b2e1d1
@ -13,6 +13,7 @@ use Illuminate\Http\JsonResponse;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
#[Apidoc\Group('后台')]
|
#[Apidoc\Group('后台')]
|
||||||
#[Apidoc\Title('用户与邀请码')]
|
#[Apidoc\Title('用户与邀请码')]
|
||||||
@ -63,6 +64,7 @@ final class UserController extends Controller
|
|||||||
$user = $this->resolveUser($user);
|
$user = $this->resolveUser($user);
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'name' => ['sometimes', 'string', 'max:50'],
|
'name' => ['sometimes', 'string', 'max:50'],
|
||||||
|
'email' => ['sometimes', 'email', 'max:120', Rule::unique('users', 'email')->ignore($user->id)],
|
||||||
'role' => ['sometimes', 'in:admin,teacher,user'],
|
'role' => ['sometimes', 'in:admin,teacher,user'],
|
||||||
'is_active' => ['sometimes', 'boolean'],
|
'is_active' => ['sometimes', 'boolean'],
|
||||||
'password' => ['nullable', 'string', 'min:6'],
|
'password' => ['nullable', 'string', 'min:6'],
|
||||||
|
|||||||
@ -13,7 +13,6 @@ use hg\apidoc\annotation as Apidoc;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Tymon\JWTAuth\Facades\JWTAuth;
|
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||||
|
|
||||||
@ -76,38 +75,23 @@ final class AuthController extends Controller
|
|||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'email' => ['required', 'email'],
|
'email' => ['required', 'email'],
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
'captcha' => ['nullable', 'string'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$key = 'login:'.$request->ip().':'.$data['email'];
|
|
||||||
$user = User::query()->where('email', $data['email'])->first();
|
$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)) {
|
if (! $user || ! Hash::check($data['password'], $user->password)) {
|
||||||
RateLimiter::hit($key, 300);
|
|
||||||
$user?->update([
|
$user?->update([
|
||||||
'failed_login_count' => $user->failed_login_count + 1,
|
'failed_login_count' => $user->failed_login_count + 1,
|
||||||
'last_failed_login_at' => now(),
|
'last_failed_login_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return ApiResponse::error('账号或密码错误', 422, 422, [
|
return ApiResponse::error('账号或密码错误', 422, 422);
|
||||||
'captcha_required' => RateLimiter::attempts($key) >= 5,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $user->is_active) {
|
if (! $user->is_active) {
|
||||||
return ApiResponse::error('账号已被禁用', 403, 403);
|
return ApiResponse::error('账号已被禁用', 403, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
RateLimiter::clear($key);
|
|
||||||
$user->update(['failed_login_count' => 0, 'last_login_at' => now()]);
|
$user->update(['failed_login_count' => 0, 'last_login_at' => now()]);
|
||||||
|
|
||||||
OperationLog::create([
|
OperationLog::create([
|
||||||
|
|||||||
@ -189,6 +189,7 @@ export function createUser(payload: { name: string; email: string; password: str
|
|||||||
|
|
||||||
export function updateUser(userId: number, payload: {
|
export function updateUser(userId: number, payload: {
|
||||||
name?: string
|
name?: string
|
||||||
|
email?: string
|
||||||
role?: string
|
role?: string
|
||||||
is_active?: boolean
|
is_active?: boolean
|
||||||
password?: string
|
password?: string
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import type { User } from '@/types/api'
|
|||||||
export interface LoginPayload {
|
export interface LoginPayload {
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
captcha?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
@ -31,7 +30,3 @@ export function register(payload: {
|
|||||||
export function me() {
|
export function me() {
|
||||||
return apiGet<User>('/api/auth/me')
|
return apiGet<User>('/api/auth/me')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function captcha() {
|
|
||||||
return apiGet<{ captcha: string; expires_in: number }>('/api/auth/captcha')
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,17 +3,13 @@ import { reactive, shallowRef } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { captcha } from '@/api/auth'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const loading = shallowRef(false)
|
const loading = shallowRef(false)
|
||||||
const captchaText = shallowRef('')
|
|
||||||
const captchaRequired = shallowRef(false)
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
captcha: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@ -22,22 +18,11 @@ async function submit() {
|
|||||||
await auth.login(form.email, form.password)
|
await auth.login(form.email, form.password)
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
await router.push('/quiz')
|
await router.push('/quiz')
|
||||||
} catch (error: any) {
|
|
||||||
captchaRequired.value = Boolean(error.response?.data?.data?.captcha_required)
|
|
||||||
if (captchaRequired.value) {
|
|
||||||
await loadCaptcha()
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCaptcha() {
|
|
||||||
const response = await captcha()
|
|
||||||
captchaText.value = response.data.captcha
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -55,12 +40,6 @@ async function loadCaptcha() {
|
|||||||
<ElFormItem label="密码">
|
<ElFormItem label="密码">
|
||||||
<ElInput v-model="form.password" type="password" autocomplete="current-password" show-password />
|
<ElInput v-model="form.password" type="password" autocomplete="current-password" show-password />
|
||||||
</ElFormItem>
|
</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>
|
<ElButton type="primary" :loading="loading" class="w-full" @click="submit">登录</ElButton>
|
||||||
<div class="login-links">
|
<div class="login-links">
|
||||||
<RouterLink to="/register">邀请码注册</RouterLink>
|
<RouterLink to="/register">邀请码注册</RouterLink>
|
||||||
@ -133,12 +112,6 @@ async function loadCaptcha() {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.login-panel {
|
.login-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@ -70,6 +70,7 @@ async function saveUser() {
|
|||||||
} else if (editingUserId.value !== null) {
|
} else if (editingUserId.value !== null) {
|
||||||
await updateUser(editingUserId.value, {
|
await updateUser(editingUserId.value, {
|
||||||
name: userForm.name,
|
name: userForm.name,
|
||||||
|
email: userForm.email,
|
||||||
role: userForm.role,
|
role: userForm.role,
|
||||||
is_active: userForm.is_active,
|
is_active: userForm.is_active,
|
||||||
password: userForm.password || undefined,
|
password: userForm.password || undefined,
|
||||||
|
|||||||
@ -158,8 +158,8 @@ onMounted(loadResources)
|
|||||||
<ElButton type="primary" @click="start(bank, 'sequence')">顺序刷题</ElButton>
|
<ElButton type="primary" @click="start(bank, 'sequence')">顺序刷题</ElButton>
|
||||||
<ElButton plain @click="openRandom(bank)">随机刷题</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="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 @click="openHistory(bank)">历史回顾</ElButton>
|
||||||
|
<ElButton plain type="warning" :disabled="!bank.wrong_questions_count" @click="reviewWrong(bank)">重刷错题</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@ -289,7 +289,7 @@ onMounted(loadResources)
|
|||||||
.resource-card {
|
.resource-card {
|
||||||
min-height: 176px;
|
min-height: 176px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 180px;
|
grid-template-columns: minmax(0, 1fr) 260px;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
@ -322,7 +322,7 @@ onMounted(loadResources)
|
|||||||
|
|
||||||
.resource-actions {
|
.resource-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user