validate([ 'name' => ['required', 'string', 'max:50'], 'email' => ['required', 'email', 'max:120', 'unique:users,email'], 'password' => ['required', 'string', 'min:6', 'confirmed'], 'invite_code' => ['required', 'string'], ]); $invite = InviteCode::query()->where('code', $data['invite_code'])->lockForUpdate()->first(); if (! $invite || ! $invite->available()) { throw ValidationException::withMessages(['invite_code' => '邀请码无效']); } if ($invite->assigned_name !== null && trim($data['name']) !== $invite->assigned_name) { throw ValidationException::withMessages(['name' => '姓名与邀请码指定使用人不一致']); } $user = User::create([ 'name' => $data['name'], 'email' => $data['email'], 'role' => $invite->role, 'is_active' => true, 'password' => Hash::make($data['password']), ]); $invite->increment('used_count'); return ApiResponse::success($this->tokenPayload($user), '注册成功'); } #[Apidoc\Title('验证码')] #[Apidoc\Url('/auth/captcha')] #[Apidoc\Method('GET')] public function captcha(Request $request): JsonResponse { $code = (string) random_int(1000, 9999); $request->session()->put('captcha', $code); return ApiResponse::success([ 'captcha' => $code, 'expires_in' => 300, ], '验证码已生成'); } #[Apidoc\Title('登录')] #[Apidoc\Url('/auth/login')] #[Apidoc\Method('POST')] public function login(Request $request): JsonResponse { $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, ]); } if (! $user->is_active) { return ApiResponse::error('账号已被禁用', 403, 403); } RateLimiter::clear($key); $user->update(['failed_login_count' => 0, 'last_login_at' => now()]); OperationLog::create([ 'user_id' => $user->id, 'action' => 'auth.login', 'ip' => $request->ip(), ]); return ApiResponse::success($this->tokenPayload($user), '登录成功'); } #[Apidoc\Title('刷新Token')] #[Apidoc\Url('/auth/refresh')] #[Apidoc\Method('POST')] public function refresh(): JsonResponse { return ApiResponse::success([ 'token' => JWTAuth::refresh(JWTAuth::getToken()), 'token_type' => 'bearer', 'expires_in' => auth('api')->factory()->getTTL() * 60, ]); } #[Apidoc\Title('当前用户')] #[Apidoc\Url('/auth/me')] #[Apidoc\Method('GET')] #[Apidoc\RouteMiddleware(['jwt.auth'])] public function me(Request $request): JsonResponse { return ApiResponse::success($request->user()->loadMissing('permissions')); } #[Apidoc\Title('退出登录')] #[Apidoc\Url('/auth/logout')] #[Apidoc\Method('POST')] #[Apidoc\RouteMiddleware(['jwt.auth'])] public function logout(): JsonResponse { JWTAuth::invalidate(JWTAuth::getToken()); return ApiResponse::success(null, '已退出'); } private function tokenPayload(User $user): array { return [ 'token' => JWTAuth::fromUser($user), 'token_type' => 'bearer', 'expires_in' => auth('api')->factory()->getTTL() * 60, 'user' => $user, ]; } }