authService->verifyCredentials($request->safe()->only(['email', 'password'])); if ($twoFactor->requiresChallenge($client, $request->input('device_token'))) { return response()->json([ 'error' => false, 'data' => ['two_factor' => $twoFactor->startLoginChallenge($client)], ]); } return $this->loginResponse($this->authService->issueTokens($client)); } /** * @param array{client: \App\Models\Customer, token: string, refresh_token: string} $result * @param array $extra */ protected function loginResponse(array $result, array $extra = []) { $client = $result['client']; $this->activityLogger->logAuth('login', 'customer', $client->id, $client->email); return response()->json([ 'error' => false, 'data' => [ 'client' => new ClientResource($client), 'token' => $result['token'], 'refresh_token' => $result['refresh_token'], 'expires_in' => config('jwt.ttl', 60) * 60, // seconds ...$extra, ], ]); } /** * @OA\Post( * path="/client/register", * operationId="clientRegister", * tags={"Client Authentication"}, * summary="Client registration", * description="Register a new client account", * @OA\RequestBody( * required=true, * @OA\JsonContent( * required={"fullname","phone","email","password","password_confirmation"}, * @OA\Property(property="fullname", type="string", example="Nguyen Van A"), * @OA\Property(property="phone", type="string", example="0987654321"), * @OA\Property(property="email", type="string", format="email", example="client@example.com"), * @OA\Property(property="password", type="string", format="password", example="password123"), * @OA\Property(property="password_confirmation", type="string", format="password", example="password123") * ) * ), * @OA\Response( * response=200, * description="Registration successful", * @OA\JsonContent( * @OA\Property(property="error", type="boolean", example=false), * @OA\Property(property="data", type="object", * @OA\Property(property="client", type="object"), * @OA\Property(property="token", type="string", example="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."), * @OA\Property(property="refresh_token", type="string", example="random64charstring..."), * @OA\Property(property="expires_in", type="integer", example=900) * ) * ) * ), * @OA\Response( * response=422, * description="Validation error" * ) * ) */ public function register(ClientRegisterRequest $request) { $data = $request->validated(); if (!empty($data['trial_config'])) { $decoded = json_decode((string) $data['trial_config'], true); $data['trial_config'] = is_array($decoded) ? $decoded : null; } $result = $this->authService->registerClient($data); $client = $result['client']; $this->activityLogger->logAuth('register', 'customer', $client->id, $client->email); return response()->json([ 'error' => false, 'message' => 'Đăng ký thành công. Vui lòng kiểm tra email và nhập mã xác thực để kích hoạt tài khoản.', ]); } public function verifyEmail(Request $request) { $request->validate([ 'email' => ['required', 'email', 'max:255'], 'code' => ['required', 'string', 'size:6'], ]); $result = $this->authService->verifyEmail( $request->input('email'), $request->input('code'), ); $client = $result['client']; $this->activityLogger->logAuth('email_verified', 'customer', $client->id, $client->email); return response()->json([ 'error' => false, 'data' => [ 'client' => new ClientResource($client), 'token' => $result['token'], 'refresh_token' => $result['refresh_token'], 'expires_in' => config('jwt.ttl', 60) * 60, ], ]); } public function resendVerifyCode(Request $request) { $request->validate([ 'email' => ['required', 'email', 'max:255'], ]); $this->authService->resendVerifyCode($request->input('email')); return response()->json([ 'error' => false, 'message' => 'Nếu email tồn tại và chưa xác thực, mã xác thực mới đã được gửi.', ]); } /** * @OA\Post( * path="/client/logout", * operationId="clientLogout", * tags={"Client Authentication"}, * summary="Client logout", * description="Logout the currently authenticated client and revoke refresh token", * security={{"bearerAuth":{}}}, * @OA\RequestBody( * @OA\JsonContent( * @OA\Property(property="refresh_token", type="string", example="random64charstring...") * ) * ), * @OA\Response( * response=200, * description="Logout successful", * @OA\JsonContent( * @OA\Property(property="error", type="boolean", example=false), * @OA\Property(property="message", type="string", example="Đã đăng xuất thành công.") * ) * ), * @OA\Response( * response=401, * description="Unauthenticated" * ) * ) */ public function logout(Request $request) { $customer = auth('client')->user(); if ($customer) { $this->activityLogger->logAuth('logout', 'customer', $customer->id, $customer->email); } if ($rawToken = $request->input('refresh_token')) { $this->authService->revokeRefreshToken($rawToken); } auth('client')->logout(); return response()->json([ 'error' => false, 'message' => 'Đã đăng xuất thành công.', ]); } /** * @OA\Post( * path="/client/refresh", * operationId="clientRefresh", * tags={"Client Authentication"}, * summary="Refresh access token", * description="Exchange a valid refresh token for a new access token + rotated refresh token", * @OA\RequestBody( * required=true, * @OA\JsonContent( * required={"refresh_token"}, * @OA\Property(property="refresh_token", type="string", example="random64charstring...") * ) * ), * @OA\Response( * response=200, * description="Token refreshed", * @OA\JsonContent( * @OA\Property(property="error", type="boolean", example=false), * @OA\Property(property="data", type="object", * @OA\Property(property="token", type="string"), * @OA\Property(property="refresh_token", type="string"), * @OA\Property(property="expires_in", type="integer", example=900), * @OA\Property(property="client", type="object") * ) * ) * ), * @OA\Response( * response=401, * description="Invalid or expired refresh token" * ) * ) */ public function refresh(Request $request) { $request->validate([ 'refresh_token' => ['required', 'string'], ]); try { $result = $this->authService->rotateRefreshToken($request->input('refresh_token')); return response()->json([ 'error' => false, 'data' => [ 'token' => $result['token'], 'refresh_token' => $result['refresh_token'], 'expires_in' => config('jwt.ttl', 60) * 60, 'client' => new ClientResource($result['client']), ], ]); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'error' => true, 'message' => collect($e->errors())->flatten()->first(), ], 401); } } /** * Đổi mã impersonate (do admin cấp) lấy access token + refresh token. * Endpoint public — code chỉ dùng được 1 lần và hết hạn sau 120s. */ public function impersonateExchange(Request $request) { $request->validate([ 'code' => ['required', 'string', 'max:128'], ]); $code = (string) $request->input('code'); $payload = \Illuminate\Support\Facades\Cache::pull("impersonate:{$code}"); if (!$payload) { return response()->json([ 'error' => true, 'message' => 'Mã đăng nhập không hợp lệ hoặc đã hết hạn.', ], 401); } return response()->json([ 'error' => false, 'data' => [ 'token' => $payload['token'], 'refresh_token' => $payload['refresh_token'], 'client' => $payload['client'], 'expires_in' => config('jwt.ttl', 60) * 60, ], ]); } }