diff --git a/backend/app/Controller/api/v1/AuthController.php b/backend/app/Controller/api/v1/AuthController.php index 78910ba..69e6f57 100644 --- a/backend/app/Controller/api/v1/AuthController.php +++ b/backend/app/Controller/api/v1/AuthController.php @@ -5,54 +5,144 @@ declare(strict_types=1); namespace App\Controller\Api\V1; use App\Controller\AbstractController; +use App\Middleware\AuthMiddleware; use App\Model\User; -use Hyperf\HttpServer\Contract\RequestInterface; -use Hyperf\HttpServer\Contract\ResponseInterface; -use Qbhy\HyperfAuth\AuthManager; use Carbon\Carbon; use Hyperf\HttpServer\Annotation\Controller; -use Hyperf\HttpServer\Annotation\RequestMapping; use Hyperf\HttpServer\Annotation\Middleware; -use Qbhy\HyperfAuth\AuthMiddleware; +use Hyperf\HttpServer\Annotation\RequestMapping; +use Hyperf\HttpServer\Contract\RequestInterface; +use Hyperf\HttpServer\Contract\ResponseInterface; +use OpenApi\Attributes as OA; +use Qbhy\HyperfAuth\AuthManager; - - #[Controller] +#[OA\Tag(name: 'Auth', description: '认证与个人信息')] +#[Controller(prefix: "/api/v1")] class AuthController extends AbstractController { /** * 用户注册 */ - #[RequestMapping(path:'/register', methods:'post')] - public function register(RequestInterface $request, ResponseInterface $response) + #[OA\Post( + path: '/register', + summary: '用户注册', + description: '注册新用户,需提供用户名、密码和邮箱', + tags: ['Auth'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + required: ['username', 'password', 'email'], + properties: [ + new OA\Property(property: 'username', type: 'string', minLength: 3, maxLength: 20, example: 'new_user'), + new OA\Property(property: 'password', type: 'string', minLength: 6, maxLength: 32, example: 'Pass_1234'), + new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'user@example.com'), + ] + ) + ), + responses: [ + new OA\Response( + response: 200, + description: '注册成功', + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'code', type: 'integer', example: 0), + new OA\Property(property: 'message', type: 'string', example: '注册成功'), + new OA\Property(property: 'data', properties: [ + new OA\Property(property: 'id', type: 'integer', example: 1), + new OA\Property(property: 'username', type: 'string', example: 'new_user'), + new OA\Property(property: 'email', type: 'string', example: 'user@example.com'), + ], type: 'object'), + ]) + ), + new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + ] + )] + #[RequestMapping(path: "register", methods: "POST")] + public function register(RequestInterface $request, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array { $username = $request->input('username'); $password = $request->input('password'); $email = $request->input('email'); - // 验证用户是否已存在 + // 校验 username + if (!is_string($username) || trim($username) === '') { + return $response->json([ + 'code' => 400, + 'message' => '用户名不能为空', + ])->withStatus(400); + } + + $username = trim($username); + $username_length = strlen($username); + if ($username_length < 3 || $username_length > 20) { + return $response->json([ + 'code' => 400, + 'message' => '用户名长度需在 3-20 个字符', + ])->withStatus(400); + } + + // 校验 password + if (!is_string($password) || $password === '') { + return $response->json([ + 'code' => 400, + 'message' => '密码不能为空', + ])->withStatus(400); + } + + $password_length = strlen($password); + if ($password_length < 6 || $password_length > 32) { + return $response->json([ + 'code' => 400, + 'message' => '密码长度需在 6-32 个字符', + ])->withStatus(400); + } + + // 校验 email + if (!is_string($email) || trim($email) === '') { + return $response->json([ + 'code' => 400, + 'message' => '邮箱不能为空', + ])->withStatus(400); + } + + $email = trim($email); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + return $response->json([ + 'code' => 400, + 'message' => '邮箱格式不正确', + ])->withStatus(400); + } + + if (strlen($email) > 100) { + return $response->json([ + 'code' => 400, + 'message' => '邮箱长度不能超过 100 个字符', + ])->withStatus(400); + } + + // 唯一性检查 if (User::query()->where('username', $username)->exists()) { return $response->json([ 'code' => 400, 'message' => '用户名已存在', - ]); + ])->withStatus(400); } if (User::query()->where('email', $email)->exists()) { return $response->json([ 'code' => 400, 'message' => '邮箱已被注册', - ]); + ])->withStatus(400); } // 创建用户 $user = User::create([ 'username' => $username, - 'password' => $password, // 自动加密 + 'password' => $password, 'email' => $email, 'status' => 1, ]); - return $response->json([ + return [ 'code' => 0, 'message' => '注册成功', 'data' => [ @@ -60,26 +150,81 @@ class AuthController extends AbstractController 'username' => $user->username, 'email' => $user->email, ], - ]); + ]; } /** * 用户登录 */ - #[RequestMapping(path:'/login', methods:'post')] - public function login(RequestInterface $request, ResponseInterface $response, AuthManager $auth) + #[OA\Post( + path: '/login', + summary: '用户登录', + description: '使用用户名和密码登录,返回 access_token 和 refresh_token', + tags: ['Auth'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + required: ['username', 'password'], + properties: [ + new OA\Property(property: 'username', type: 'string', example: 'admin'), + new OA\Property(property: 'password', type: 'string', example: 'Pass_1234'), + ] + ) + ), + responses: [ + new OA\Response( + response: 200, + description: '登录成功', + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'code', type: 'integer', example: 0), + new OA\Property(property: 'message', type: 'string', example: '登录成功'), + new OA\Property(property: 'data', properties: [ + new OA\Property(property: 'access_token', type: 'string'), + new OA\Property(property: 'refresh_token', type: 'string'), + new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'), + new OA\Property(property: 'expires_in', type: 'integer', example: 7200), + new OA\Property(property: 'user', properties: [ + new OA\Property(property: 'id', type: 'integer'), + new OA\Property(property: 'username', type: 'string'), + new OA\Property(property: 'email', type: 'string'), + ], type: 'object'), + ], type: 'object'), + ]) + ), + new OA\Response(response: 400, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + new OA\Response(response: 401, description: '用户名或密码错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + new OA\Response(response: 403, description: '账号已被禁用', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + ] + )] + #[RequestMapping(path: "login", methods: "POST")] + public function login(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array { $username = $request->input('username'); $password = $request->input('password'); + // 校验参数 + if (!is_string($username) || trim($username) === '') { + return $response->json([ + 'code' => 400, + 'message' => '用户名不能为空', + ])->withStatus(400); + } + + if (!is_string($password) || $password === '') { + return $response->json([ + 'code' => 400, + 'message' => '密码不能为空', + ])->withStatus(400); + } + // 查找用户 - $user = User::query()->where('username', $username)->first(); + $user = User::query()->where('username', trim($username))->first(); if (!$user) { return $response->json([ 'code' => 401, 'message' => '用户名或密码错误', - ]); + ])->withStatus(401); } // 验证密码 @@ -87,7 +232,7 @@ class AuthController extends AbstractController return $response->json([ 'code' => 401, 'message' => '用户名或密码错误', - ]); + ])->withStatus(401); } // 检查用户状态 @@ -95,7 +240,7 @@ class AuthController extends AbstractController return $response->json([ 'code' => 403, 'message' => '账号已被禁用', - ]); + ])->withStatus(403); } // 生成 Access Token @@ -107,37 +252,66 @@ class AuthController extends AbstractController $user->refresh_token_expires_at = Carbon::now()->addDays(30); $user->save(); - return $response->json([ + return [ 'code' => 0, 'message' => '登录成功', 'data' => [ 'access_token' => $token, 'refresh_token' => $refreshToken, 'token_type' => 'Bearer', - 'expires_in' => 7200, // 2 小时 + 'expires_in' => 7200, 'user' => [ 'id' => $user->id, 'username' => $user->username, 'email' => $user->email, ], ], - ]); + ]; } /** * 刷新 Access Token */ - #[RequestMapping(path:'/refresh', methods:'get')] + #[OA\Get( + path: '/refresh', + summary: '刷新 Access Token', + description: '使用 refresh_token 获取新的 access_token,同时轮换 refresh_token', + security: [['bearerAuth' => []]], + tags: ['Auth'], + parameters: [ + new OA\Parameter(name: 'refresh_token', in: 'query', required: true, description: 'Refresh Token', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Token 刷新成功', + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'code', type: 'integer', example: 0), + new OA\Property(property: 'message', type: 'string', example: 'Token 刷新成功'), + new OA\Property(property: 'data', properties: [ + new OA\Property(property: 'access_token', type: 'string'), + new OA\Property(property: 'refresh_token', type: 'string'), + new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'), + new OA\Property(property: 'expires_in', type: 'integer', example: 7200), + ], type: 'object'), + ]) + ), + new OA\Response(response: 400, description: '缺少 refresh_token 参数', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + new OA\Response(response: 401, description: 'refresh_token 无效或已过期', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + new OA\Response(response: 403, description: '账号已被禁用', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + ] + )] + #[RequestMapping(path: "refresh", methods: "GET")] #[Middleware(AuthMiddleware::class)] - public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth) + public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array { $refreshToken = $request->input('refresh_token'); - if (!$refreshToken) { + if (!is_string($refreshToken) || $refreshToken === '') { return $response->json([ 'code' => 400, 'message' => '缺少 refresh_token 参数', - ]); + ])->withStatus(400); } // 查找用户 @@ -147,7 +321,7 @@ class AuthController extends AbstractController return $response->json([ 'code' => 401, 'message' => '无效的 refresh_token', - ]); + ])->withStatus(401); } // 验证 refresh token 是否过期 @@ -155,7 +329,7 @@ class AuthController extends AbstractController return $response->json([ 'code' => 401, 'message' => 'refresh_token 已过期,请重新登录', - ]); + ])->withStatus(401); } // 检查用户状态 @@ -163,19 +337,19 @@ class AuthController extends AbstractController return $response->json([ 'code' => 403, 'message' => '账号已被禁用', - ]); + ])->withStatus(403); } // 生成新的 Access Token $token = $auth->guard('jwt')->login($user); - // 可选:生成新的 Refresh Token(更安全) + // 生成新的 Refresh Token(轮换以提升安全性) $newRefreshToken = bin2hex(random_bytes(32)); $user->refresh_token = $newRefreshToken; $user->refresh_token_expires_at = Carbon::now()->addDays(30); $user->save(); - return $response->json([ + return [ 'code' => 0, 'message' => 'Token 刷新成功', 'data' => [ @@ -184,15 +358,40 @@ class AuthController extends AbstractController 'token_type' => 'Bearer', 'expires_in' => 7200, ], - ]); + ]; } /** * 获取当前用户信息 */ - #[RequestMapping(path:'/me', methods:'get')] + #[OA\Get( + path: '/me', + summary: '获取当前用户信息', + security: [['bearerAuth' => []]], + tags: ['Auth'], + responses: [ + new OA\Response( + response: 200, + description: '获取成功', + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'code', type: 'integer', example: 0), + new OA\Property(property: 'message', type: 'string', example: '获取成功'), + new OA\Property(property: 'data', properties: [ + new OA\Property(property: 'id', type: 'integer', example: 1), + new OA\Property(property: 'username', type: 'string', example: 'admin'), + new OA\Property(property: 'email', type: 'string', example: 'admin@example.com'), + new OA\Property(property: 'status', type: 'integer', example: 1), + new OA\Property(property: 'ext', type: 'object', nullable: true), + new OA\Property(property: 'created_at', type: 'string', format: 'date-time'), + ], type: 'object'), + ]) + ), + new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + ] + )] + #[RequestMapping(path: "me", methods: "GET")] #[Middleware(AuthMiddleware::class)] - public function me(AuthManager $auth, ResponseInterface $response) + public function me(AuthManager $auth, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array { $user = $auth->guard('jwt')->user(); @@ -200,10 +399,10 @@ class AuthController extends AbstractController return $response->json([ 'code' => 401, 'message' => '未授权', - ]); + ])->withStatus(401); } - return $response->json([ + return [ 'code' => 0, 'message' => '获取成功', 'data' => [ @@ -214,15 +413,251 @@ class AuthController extends AbstractController 'ext' => $user->ext, 'created_at' => $user->created_at->toDateTimeString(), ], - ]); + ]; + } + + /** + * 更新个人信息 + * + * 当前用户更新自己的 email 和 ext 字段 + */ + #[OA\Put( + path: '/me/profile', + summary: '更新个人信息', + description: '当前用户更新自己的 email 和 ext 字段', + security: [['bearerAuth' => []]], + tags: ['Auth'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'new@example.com'), + new OA\Property(property: 'ext', type: 'object', nullable: true, example: ['nickname' => 'user']), + ]) + ), + responses: [ + new OA\Response( + response: 200, + description: '更新成功', + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'code', type: 'integer', example: 0), + new OA\Property(property: 'message', type: 'string', example: '个人信息更新成功'), + new OA\Property(property: 'data', properties: [ + new OA\Property(property: 'id', type: 'integer'), + new OA\Property(property: 'username', type: 'string'), + new OA\Property(property: 'email', type: 'string'), + new OA\Property(property: 'status', type: 'integer'), + new OA\Property(property: 'ext', type: 'object', nullable: true), + new OA\Property(property: 'created_at', type: 'string', format: 'date-time'), + ], type: 'object'), + ]) + ), + new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + ] + )] + #[RequestMapping(path: "me/profile", methods: "PUT")] + #[Middleware(AuthMiddleware::class)] + public function updateProfile(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array + { + $user = $auth->guard('jwt')->user(); + + if (!$user instanceof User) { + return $response->json([ + 'code' => 401, + 'message' => '未授权', + ])->withStatus(401); + } + + $email = $request->input('email'); + $ext = $request->input('ext'); + $updates = []; + + if ($email !== null) { + if (!is_string($email) || trim($email) === '') { + return $response->json([ + 'code' => 400, + 'message' => '邮箱不能为空', + ])->withStatus(400); + } + + $email = trim($email); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + return $response->json([ + 'code' => 400, + 'message' => '邮箱格式不正确', + ])->withStatus(400); + } + + if (strlen($email) > 100) { + return $response->json([ + 'code' => 400, + 'message' => '邮箱长度不能超过 100 个字符', + ])->withStatus(400); + } + + if (User::query()->where('email', $email)->where('id', '!=', $user->id)->exists()) { + return $response->json([ + 'code' => 400, + 'message' => '邮箱已被注册', + ])->withStatus(400); + } + + $updates['email'] = $email; + } + + if ($ext !== null) { + if (!is_array($ext)) { + return $response->json([ + 'code' => 400, + 'message' => 'ext 必须为对象', + ])->withStatus(400); + } + + $updates['ext'] = $ext; + } + + if ($updates === []) { + return $response->json([ + 'code' => 400, + 'message' => '缺少可更新字段', + ])->withStatus(400); + } + + $user->fill($updates); + $user->save(); + $user->refresh(); + + return [ + 'code' => 0, + 'message' => '个人信息更新成功', + 'data' => [ + 'id' => $user->id, + 'username' => $user->username, + 'email' => $user->email, + 'status' => $user->status, + 'ext' => $user->ext, + 'created_at' => $user->created_at->toDateTimeString(), + ], + ]; + } + + /** + * 修改密码 + * + * 需验证旧密码,修改成功后清除 refresh_token 强制重新登录 + */ + #[OA\Put( + path: '/me/password', + summary: '修改密码', + description: '修改当前用户密码,需验证旧密码。修改成功后清除 refresh_token,需重新登录', + security: [['bearerAuth' => []]], + tags: ['Auth'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + required: ['old_password', 'new_password'], + properties: [ + new OA\Property(property: 'old_password', type: 'string', example: 'OldPass_1234'), + new OA\Property(property: 'new_password', type: 'string', minLength: 6, maxLength: 32, example: 'NewPass_5678'), + ] + ) + ), + responses: [ + new OA\Response( + response: 200, + description: '密码修改成功', + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'code', type: 'integer', example: 0), + new OA\Property(property: 'message', type: 'string', example: '密码修改成功,请重新登录'), + ]) + ), + new OA\Response(response: 400, description: '参数校验失败或旧密码不正确', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + ] + )] + #[RequestMapping(path: "me/password", methods: "PUT")] + #[Middleware(AuthMiddleware::class)] + public function changePassword(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array + { + $user = $auth->guard('jwt')->user(); + + if (!$user instanceof User) { + return $response->json([ + 'code' => 401, + 'message' => '未授权', + ])->withStatus(401); + } + + $old_password = $request->input('old_password'); + $new_password = $request->input('new_password'); + + // 验证旧密码 + if (!is_string($old_password) || $old_password === '') { + return $response->json([ + 'code' => 400, + 'message' => '旧密码不能为空', + ])->withStatus(400); + } + + if (!$user->verifyPassword($old_password)) { + return $response->json([ + 'code' => 400, + 'message' => '旧密码不正确', + ])->withStatus(400); + } + + // 验证新密码 + if (!is_string($new_password) || $new_password === '') { + return $response->json([ + 'code' => 400, + 'message' => '新密码不能为空', + ])->withStatus(400); + } + + $new_password_length = strlen($new_password); + if ($new_password_length < 6 || $new_password_length > 32) { + return $response->json([ + 'code' => 400, + 'message' => '新密码长度需在 6-32 个字符', + ])->withStatus(400); + } + + // 修改密码并清除 refresh_token + $user->password = $new_password; + $user->refresh_token = null; + $user->refresh_token_expires_at = null; + $user->save(); + + return [ + 'code' => 0, + 'message' => '密码修改成功,请重新登录', + ]; } /** * 退出登录 */ - #[RequestMapping(path:'/logout', methods:'get')] + #[OA\Get( + path: '/logout', + summary: '退出登录', + description: '退出登录,清除 refresh_token 并注销当前 JWT token', + security: [['bearerAuth' => []]], + tags: ['Auth'], + responses: [ + new OA\Response( + response: 200, + description: '退出成功', + content: new OA\JsonContent(properties: [ + new OA\Property(property: 'code', type: 'integer', example: 0), + new OA\Property(property: 'message', type: 'string', example: '退出成功'), + ]) + ), + new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + ] + )] + #[RequestMapping(path: "logout", methods: "GET")] #[Middleware(AuthMiddleware::class)] - public function logout(AuthManager $auth, ResponseInterface $response) + public function logout(AuthManager $auth): array { $user = $auth->guard('jwt')->user(); @@ -236,9 +671,9 @@ class AuthController extends AbstractController // 注销当前 token $auth->guard('jwt')->logout(); - return $response->json([ + return [ 'code' => 0, 'message' => '退出成功', - ]); + ]; } }