QuickQuiz/app/Http/Controllers/Api/AuthController.php

164 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\InviteCode;
use App\Models\OperationLog;
use App\Models\User;
use App\Support\ApiResponse;
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;
#[Apidoc\Group('认证')]
#[Apidoc\Title('认证接口')]
final class AuthController extends Controller
{
#[Apidoc\Title('邀请码注册')]
#[Apidoc\Url('/auth/register')]
#[Apidoc\Method('POST')]
public function register(Request $request): JsonResponse
{
$data = $request->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,
];
}
}