diff --git a/app/Http/Controllers/DevTools/FlowTesterController.php b/app/Http/Controllers/DevTools/FlowTesterController.php index 711f5485..fa21a3ee 100644 --- a/app/Http/Controllers/DevTools/FlowTesterController.php +++ b/app/Http/Controllers/DevTools/FlowTesterController.php @@ -25,10 +25,12 @@ public function index(): View ->orderByDesc('created_at') ->paginate(20); - // 세션에 저장된 토큰 - $savedToken = session('flow_tester_token'); + // 세션에 저장된 토큰 (API Explorer와 공유) + $savedToken = session('api_explorer_token'); + $selectedUserId = session('api_explorer_user_id'); + $selectedUser = $selectedUserId ? \App\Models\User::find($selectedUserId) : null; - return view('dev-tools.flow-tester.index', compact('flows', 'savedToken')); + return view('dev-tools.flow-tester.index', compact('flows', 'savedToken', 'selectedUser')); } /** @@ -335,12 +337,68 @@ public function runDetail(int $runId): View /* |-------------------------------------------------------------------------- - | Token Management + | Token & User Management (API Explorer와 공유) |-------------------------------------------------------------------------- */ /** - * Bearer 토큰 저장 + * 현재 테넌트의 사용자 목록 + */ + public function users() + { + $tenantId = auth()->user()->tenant_id; + + $users = \App\Models\User::where('tenant_id', $tenantId) + ->select(['id', 'name', 'email', 'tenant_id']) + ->orderBy('name') + ->limit(100) + ->get(); + + return response()->json($users); + } + + /** + * 사용자 선택 (Sanctum 토큰 발급) + */ + public function selectUser(Request $request) + { + $validated = $request->validate([ + 'user_id' => 'required|integer', + ]); + + $user = \App\Models\User::find($validated['user_id']); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => '사용자를 찾을 수 없습니다.', + ], 404); + } + + // Sanctum 토큰 발급 + $token = $user->createToken('flow-tester', ['*'])->plainTextToken; + + // 세션에 저장 (API Explorer와 공유) + session([ + 'api_explorer_token' => $token, + 'api_explorer_user_id' => $user->id, + ]); + + return response()->json([ + 'success' => true, + 'message' => "'{$user->name}' 사용자로 인증되었습니다.", + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'tenant_id' => $user->tenant_id, + ], + 'token_preview' => substr($token, 0, 20).'...', + ]); + } + + /** + * Bearer 토큰 저장 (직접 입력) */ public function saveToken(Request $request) { @@ -348,7 +406,11 @@ public function saveToken(Request $request) 'token' => 'required|string', ]); - session(['flow_tester_token' => $validated['token']]); + // API Explorer와 같은 세션 키 사용 + session([ + 'api_explorer_token' => $validated['token'], + 'api_explorer_user_id' => null, // 직접 입력 시 사용자 정보 없음 + ]); return response()->json([ 'success' => true, @@ -361,11 +423,11 @@ public function saveToken(Request $request) */ public function clearToken() { - session()->forget('flow_tester_token'); + session()->forget(['api_explorer_token', 'api_explorer_user_id']); return response()->json([ 'success' => true, - 'message' => '토큰이 초기화되었습니다.', + 'message' => '인증이 초기화되었습니다.', ]); } @@ -374,11 +436,19 @@ public function clearToken() */ public function tokenStatus() { - $token = session('flow_tester_token'); + $token = session('api_explorer_token'); + $userId = session('api_explorer_user_id'); + $user = $userId ? \App\Models\User::find($userId) : null; return response()->json([ 'has_token' => ! empty($token), 'token_preview' => $token ? substr($token, 0, 20).'...' : null, + 'user' => $user ? [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'tenant_id' => $user->tenant_id, + ] : null, ]); } } diff --git a/app/Services/FlowTester/VariableBinder.php b/app/Services/FlowTester/VariableBinder.php index 5e186337..d1fe459e 100644 --- a/app/Services/FlowTester/VariableBinder.php +++ b/app/Services/FlowTester/VariableBinder.php @@ -17,6 +17,10 @@ * - {{$uuid}} - 랜덤 UUID * - {{$random:N}} - N자리 랜덤 숫자 * - {{$session.token}} - 세션에 저장된 Bearer 토큰 + * - {{$hmac.exp}} - HMAC 만료 시간 (현재 + 300초) + * - {{$hmac.signature}} - HMAC-SHA256 서명 + * - {{$hmac.user_id}} - 현재 로그인 사용자 ID + * - {{$hmac.tenant_id}} - 현재 테넌트 ID * - {{$faker.xxx}} - Faker 기반 랜덤 데이터 생성 */ class VariableBinder @@ -152,12 +156,15 @@ function ($m) { // {{$auth.apiKey}} → .env의 API Key $input = str_replace('{{$auth.apiKey}}', env('FLOW_TESTER_API_KEY', ''), $input); - // {{$session.token}} → 세션에 저장된 Bearer 토큰 + // {{$session.token}} → 세션에 저장된 Bearer 토큰 (API Explorer와 공유) if (str_contains($input, '{{$session.token}}')) { - $token = session('flow_tester_token', ''); + $token = session('api_explorer_token', ''); $input = str_replace('{{$session.token}}', $token, $input); } + // {{$hmac.xxx}} → HMAC 인증용 동적 값 생성 + $input = $this->resolveHmac($input); + // {{$faker.xxx}} → Faker 기반 랜덤 데이터 생성 $input = $this->resolveFaker($input); @@ -339,6 +346,58 @@ private function getAuthToken(): string return env('FLOW_TESTER_API_TOKEN', ''); } + /** + * HMAC 인증용 동적 값 생성 + * + * 지원 패턴: + * - {{$hmac.exp}} - 만료 시간 (현재 + 300초) + * - {{$hmac.user_id}} - 선택된 사용자 ID (또는 현재 로그인 사용자) + * - {{$hmac.tenant_id}} - 선택된 사용자의 테넌트 ID + * - {{$hmac.signature}} - HMAC-SHA256 서명 + * + * 서명 생성 방식: + * - payload: "{user_id}:{tenant_id}:{exp}" + * - signature: hash_hmac('sha256', payload, exchange_secret) + * + * 세션에서 선택된 사용자 정보를 우선 사용 (API Explorer와 공유) + */ + private function resolveHmac(string $input): string + { + // HMAC 변수가 없으면 조기 반환 + if (! str_contains($input, '{{$hmac.')) { + return $input; + } + + // 세션에서 선택된 사용자 ID 가져오기 (API Explorer와 공유) + $selectedUserId = session('api_explorer_user_id'); + + // 선택된 사용자가 있으면 해당 사용자 정보 사용, 없으면 현재 로그인 사용자 + if ($selectedUserId) { + $user = \App\Models\User::find($selectedUserId); + } else { + $user = auth()->user(); + } + + $userId = $user?->id ?? env('FLOW_TESTER_USER_ID', ''); + $tenantId = $user?->tenant_id ?? env('FLOW_TESTER_TENANT_ID', ''); + + // 만료 시간 (현재 + 300초) + $exp = time() + 300; + + // 서명 생성 + $exchangeSecret = config('services.api.exchange_secret', ''); + $payload = "{$userId}:{$tenantId}:{$exp}"; + $signature = hash_hmac('sha256', $payload, $exchangeSecret); + + // 각 HMAC 변수 치환 + $input = str_replace('{{$hmac.exp}}', (string) $exp, $input); + $input = str_replace('{{$hmac.user_id}}', (string) $userId, $input); + $input = str_replace('{{$hmac.tenant_id}}', (string) $tenantId, $input); + $input = str_replace('{{$hmac.signature}}', $signature, $input); + + return $input; + } + /** * 참조 경로 해석 (step1.pageId 또는 page_create_1.pageId → 실제 값) * diff --git a/resources/views/dev-tools/flow-tester/index.blade.php b/resources/views/dev-tools/flow-tester/index.blade.php index 98f32fc2..e346785d 100644 --- a/resources/views/dev-tools/flow-tester/index.blade.php +++ b/resources/views/dev-tools/flow-tester/index.blade.php @@ -13,7 +13,13 @@ - {{ $savedToken ? '인증됨' : '인증 필요' }} + @if($selectedUser ?? null) + {{ $selectedUser->name }} + @elseif($savedToken) + 인증됨 + @else + 인증 필요 + @endif @@ -448,16 +454,40 @@ class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition" - +
- + +
+ + +
+

사용자를 선택하면 해당 사용자의 Sanctum 토큰이 자동 발급됩니다.

+
+ + +
+
+
+
+
+ 또는 +
+
+ + +
+ - @if($savedToken) -

✅ 세션에 저장된 토큰이 있습니다.

- @endif -

플로우에서 \{\{$session.token\}\}로 참조할 수 있습니다.

+

플로우에서 @{{$session.token}}로 참조할 수 있습니다.

@@ -465,9 +495,20 @@ class="w-full border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-5
현재 상태: - {{ $savedToken ? '인증됨' : '인증 필요' }} + @if($selectedUser ?? null) + ✅ {{ $selectedUser->name }} ({{ $selectedUser->email }}) + @elseif($savedToken) + 인증됨 + @else + 인증 필요 + @endif
+ @if($selectedUser ?? null) +
+ tenant_id: {{ $selectedUser->tenant_id }}, user_id: {{ $selectedUser->id }} +
+ @endif @@ -479,7 +520,7 @@ class="w-full border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-5 닫기 @@ -729,16 +770,85 @@ function confirmDelete(id, name) { // 현재 토큰 상태 let currentAuthToken = @json($savedToken ?? ''); + let usersLoaded = false; function openAuthModal() { document.getElementById('authModal').classList.remove('hidden'); document.getElementById('authBearerToken').value = currentAuthToken || ''; + + // 사용자 목록 로딩 (최초 1회) + if (!usersLoaded) { + loadUsers(); + } } function closeAuthModal() { document.getElementById('authModal').classList.add('hidden'); } + // 사용자 목록 로딩 + function loadUsers() { + const select = document.getElementById('userSelect'); + select.innerHTML = ''; + + fetch('{{ route("dev-tools.flow-tester.users") }}', { + headers: { + 'Accept': 'application/json', + }, + }) + .then(response => response.json()) + .then(users => { + select.innerHTML = ''; + users.forEach(user => { + const option = document.createElement('option'); + option.value = user.id; + option.textContent = `${user.name} (${user.email}) - tenant: ${user.tenant_id}`; + select.appendChild(option); + }); + usersLoaded = true; + }) + .catch(error => { + select.innerHTML = ''; + console.error('사용자 목록 로딩 실패:', error); + }); + } + + // 사용자 선택 (Sanctum 토큰 발급) + function selectUser() { + const select = document.getElementById('userSelect'); + const userId = select.value; + + if (!userId) { + showToast('사용자를 선택해주세요.', 'warning'); + return; + } + + fetch('{{ route("dev-tools.flow-tester.user.select") }}', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ user_id: parseInt(userId) }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // 페이지 새로고침하여 상태 반영 + showToast(data.message, 'success'); + setTimeout(() => { + location.reload(); + }, 500); + } else { + showToast(data.message || '사용자 선택 실패', 'error'); + } + }) + .catch(error => { + showToast('오류 발생: ' + error.message, 'error'); + }); + } + function saveAuth() { const token = document.getElementById('authBearerToken').value.trim(); @@ -761,7 +871,7 @@ function saveAuth() { .then(data => { if (data.success) { currentAuthToken = token; - updateAuthStatus(true); + updateAuthStatus(true, null); showToast('토큰이 저장되었습니다.', 'success'); closeAuthModal(); } else { @@ -787,8 +897,11 @@ function clearAuth() { if (data.success) { currentAuthToken = ''; document.getElementById('authBearerToken').value = ''; - updateAuthStatus(false); + updateAuthStatus(false, null); showToast('인증이 초기화되었습니다.', 'info'); + setTimeout(() => { + location.reload(); + }, 500); } else { showToast(data.message || '초기화 실패', 'error'); } @@ -798,17 +911,17 @@ function clearAuth() { }); } - function updateAuthStatus(isAuthenticated) { + function updateAuthStatus(isAuthenticated, userName) { const statusEl = document.getElementById('auth-status'); const modalStatusEl = document.getElementById('authStatusDisplay'); if (isAuthenticated) { - statusEl.textContent = '인증됨'; + statusEl.textContent = userName || '인증됨'; statusEl.classList.remove('text-gray-500'); statusEl.classList.add('text-green-600'); if (modalStatusEl) { - modalStatusEl.textContent = '인증됨'; + modalStatusEl.textContent = userName ? `✅ ${userName}` : '인증됨'; modalStatusEl.classList.remove('text-gray-500'); modalStatusEl.classList.add('text-green-600'); } diff --git a/routes/web.php b/routes/web.php index bf4c843d..3f8a4d79 100644 --- a/routes/web.php +++ b/routes/web.php @@ -283,7 +283,9 @@ Route::post('/', [FlowTesterController::class, 'store'])->name('store'); Route::post('/validate-json', [FlowTesterController::class, 'validateJson'])->name('validate-json'); - // 토큰 관리 라우트 + // 토큰 및 사용자 관리 라우트 (API Explorer와 공유) + Route::get('/users', [FlowTesterController::class, 'users'])->name('users'); + Route::post('/user/select', [FlowTesterController::class, 'selectUser'])->name('user.select'); Route::post('/token/save', [FlowTesterController::class, 'saveToken'])->name('token.save'); Route::post('/token/clear', [FlowTesterController::class, 'clearToken'])->name('token.clear'); Route::get('/token/status', [FlowTesterController::class, 'tokenStatus'])->name('token.status');