From 2ed273097e885cdc952cca032d3e6db3db557b90 Mon Sep 17 00:00:00 2001 From: hskwon Date: Thu, 18 Dec 2025 14:21:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20API=20=ED=86=A0=ED=81=B0=20=EA=B5=90?= =?UTF-8?q?=ED=99=98=20=EC=97=B0=EB=8F=99=20(FCM=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ApiTokenService: API 서버 토큰 교환 서비스 추가 - AuthService: 로그인 성공 시 API 토큰 교환 연동 - 레이아웃: 세션 토큰을 localStorage에 동기화 (FCM 사용) - config/services.php: exchange_secret 설정 추가 환경변수 필요: INTERNAL_EXCHANGE_SECRET (API와 동일) --- app/Services/ApiTokenService.php | 141 ++++++++++++++++++++++++++ app/Services/AuthService.php | 43 ++++++++ config/services.php | 1 + resources/views/layouts/app.blade.php | 33 ++++++ 4 files changed, 218 insertions(+) create mode 100644 app/Services/ApiTokenService.php diff --git a/app/Services/ApiTokenService.php b/app/Services/ApiTokenService.php new file mode 100644 index 00000000..b36730ea --- /dev/null +++ b/app/Services/ApiTokenService.php @@ -0,0 +1,141 @@ + false, 'error' => 'API 서버 URL이 설정되지 않았습니다.']; + } + + if (empty($exchangeSecret)) { + Log::error('[ApiTokenService] Exchange secret not configured'); + + return ['success' => false, 'error' => '토큰 교환 비밀키가 설정되지 않았습니다.']; + } + + // 만료 시간 계산 (현재 시간 + 유효 시간) + $exp = time() + self::SIGNATURE_VALID_DURATION; + + // HMAC 서명 생성 + $payload = "{$userId}:{$tenantId}:{$exp}"; + $signature = hash_hmac('sha256', $payload, $exchangeSecret); + + try { + $response = Http::timeout(10) + ->post("{$baseUrl}/api/v1/internal/exchange-token", [ + 'user_id' => $userId, + 'tenant_id' => $tenantId, + 'exp' => $exp, + 'signature' => $signature, + ]); + + if ($response->successful()) { + $data = $response->json('data'); + Log::info('[ApiTokenService] Token exchanged successfully', [ + 'user_id' => $userId, + 'tenant_id' => $tenantId, + ]); + + return [ + 'success' => true, + 'data' => [ + 'access_token' => $data['access_token'], + 'token_type' => $data['token_type'] ?? 'Bearer', + 'expires_in' => $data['expires_in'] ?? 3600, + ], + ]; + } + + $error = $response->json('message') ?? '토큰 교환에 실패했습니다.'; + Log::warning('[ApiTokenService] Token exchange failed', [ + 'user_id' => $userId, + 'tenant_id' => $tenantId, + 'status' => $response->status(), + 'error' => $error, + ]); + + return ['success' => false, 'error' => $error]; + } catch (\Exception $e) { + Log::error('[ApiTokenService] Token exchange exception', [ + 'user_id' => $userId, + 'tenant_id' => $tenantId, + 'exception' => $e->getMessage(), + ]); + + return ['success' => false, 'error' => 'API 서버 연결에 실패했습니다.']; + } + } + + /** + * 세션에 저장된 API 토큰 조회 + */ + public function getSessionToken(): ?string + { + return session('api_access_token'); + } + + /** + * API 토큰을 세션에 저장 + * + * @param string $token Bearer 토큰 + * @param int $expiresIn 만료 시간 (초) + */ + public function storeTokenInSession(string $token, int $expiresIn): void + { + session([ + 'api_access_token' => $token, + 'api_token_expires_at' => now()->addSeconds($expiresIn)->timestamp, + ]); + } + + /** + * 세션의 API 토큰 삭제 + */ + public function clearSessionToken(): void + { + session()->forget(['api_access_token', 'api_token_expires_at']); + } + + /** + * 토큰이 만료되었는지 확인 + */ + public function isTokenExpired(): bool + { + $expiresAt = session('api_token_expires_at'); + + if (! $expiresAt) { + return true; + } + + // 5분 전에 미리 갱신하도록 (버퍼) + return time() > ($expiresAt - 300); + } +} diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php index aa2e5eb2..cfb8b40a 100644 --- a/app/Services/AuthService.php +++ b/app/Services/AuthService.php @@ -5,6 +5,7 @@ use App\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; class AuthService { @@ -13,6 +14,10 @@ class AuthService */ private ?string $loginError = null; + public function __construct( + private readonly ApiTokenService $apiTokenService + ) {} + /** * 웹 세션 로그인 * - 본사(HQ) 테넌트 소속만 로그인 허용 @@ -50,11 +55,46 @@ public function login(array $credentials, bool $remember = false): bool $hqTenant = $user->getHQTenant(); if ($hqTenant) { session(['selected_tenant_id' => $hqTenant->id]); + + // 5. API 서버 토큰 교환 (FCM 등 API 호출용) + $this->exchangeApiToken($user->id, $hqTenant->id); } return true; } + /** + * API 서버 토큰 교환 + * 실패해도 로그인은 허용 (FCM 기능만 제한됨) + * + * @param int $userId 사용자 ID + * @param int $tenantId 테넌트 ID + */ + private function exchangeApiToken(int $userId, int $tenantId): void + { + try { + $result = $this->apiTokenService->exchangeToken($userId, $tenantId); + + if ($result['success']) { + $this->apiTokenService->storeTokenInSession( + $result['data']['access_token'], + $result['data']['expires_in'] + ); + Log::info('[AuthService] API token exchanged', ['user_id' => $userId]); + } else { + Log::warning('[AuthService] API token exchange failed', [ + 'user_id' => $userId, + 'error' => $result['error'] ?? 'Unknown error', + ]); + } + } catch (\Exception $e) { + Log::error('[AuthService] API token exchange exception', [ + 'user_id' => $userId, + 'exception' => $e->getMessage(), + ]); + } + } + /** * 마지막 로그인 실패 사유 조회 */ @@ -68,6 +108,9 @@ public function getLoginError(): ?string */ public function logout(): void { + // API 토큰 정리 + $this->apiTokenService->clearSessionToken(); + Auth::logout(); } diff --git a/config/services.php b/config/services.php index 48c183b6..8ab33f31 100644 --- a/config/services.php +++ b/config/services.php @@ -61,6 +61,7 @@ 'api' => [ 'base_url' => env('API_BASE_URL'), 'key' => env('FLOW_TESTER_API_KEY'), + 'exchange_secret' => env('INTERNAL_EXCHANGE_SECRET'), ], ]; diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 4fe2b73a..104e0aa6 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -6,6 +6,37 @@ @yield('title', 'Dashboard') - {{ config('app.name') }} @vite(['resources/css/app.css', 'resources/js/app.js']) + + + +