164 lines
5.3 KiB
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,
|
|
];
|
|
}
|
|
}
|