feat(flow-tester): 세션 인증 모드(useSessionAuth) 지원

- VariableBinder: {{$session.xxx}} 변수 지원 추가
  - {{$session.token}}: 세션 Bearer 토큰
  - {{$session.user_id}}: 세션 사용자 ID
  - {{$session.user.email/name}}: 사용자 정보
  - {{$session.tenant_id}}: 테넌트 ID
  - getSessionAuth() 메서드 추가

- FlowExecutor: useSessionAuth 옵션 처리
  - login 스텝에서 API 호출 대신 세션 토큰 사용
  - {{login.token}} 등 자동 바인딩
  - 세션 토큰 없을 시 명확한 에러 메시지

페이지 인증 완료 시 플로우의 login 스텝에서
실제 API 호출 없이 세션 인증 정보를 활용할 수 있음

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 23:02:21 +09:00
parent 00a4920b7a
commit c71f619743
2 changed files with 180 additions and 5 deletions

View File

@@ -213,6 +213,11 @@ private function executeStep(array $step): array
$startTime = microtime(true);
try {
// 0. useSessionAuth 옵션 처리 - 세션 인증 정보로 대체
if (! empty($step['useSessionAuth'])) {
return $this->executeSessionAuthStep($step, $stepId, $stepName, $startTime);
}
// 1. 딜레이 적용
$delay = $step['delay'] ?? 0;
if ($delay > 0) {
@@ -302,6 +307,74 @@ private function executeStep(array $step): array
}
}
/**
* 세션 인증 스텝 실행
*
* useSessionAuth: true 옵션이 있는 login 스텝에서 사용
* 실제 API 호출 없이 세션에 저장된 토큰을 바인딩합니다.
*
* @param array $step 스텝 정의
* @param string $stepId 스텝 ID
* @param string $stepName 스텝 이름
* @param float $startTime 시작 시간
* @return array 스텝 결과
*/
private function executeSessionAuthStep(array $step, string $stepId, string $stepName, float $startTime): array
{
// 세션 인증 정보 조회
$sessionAuth = $this->binder->getSessionAuth();
// 세션 토큰이 없으면 실패
if (empty($sessionAuth['token'])) {
return $this->buildStepResult($stepId, $stepName, $startTime, false, [
'error' => '세션 인증 정보가 없습니다. 페이지에서 먼저 인증을 완료해주세요.',
'reason' => '✗ 세션 토큰 없음 - 페이지 인증 필요',
'useSessionAuth' => true,
]);
}
// 세션 토큰을 스텝 결과로 바인딩 ({{login.token}} 등에서 사용 가능)
$extracted = [
'token' => $sessionAuth['token'],
'access_token' => $sessionAuth['token'], // 호환성을 위해 추가
];
// 사용자 정보도 바인딩
if ($sessionAuth['user']) {
$extracted['user_id'] = $sessionAuth['user']['id'];
$extracted['user_name'] = $sessionAuth['user']['name'];
$extracted['user_email'] = $sessionAuth['user']['email'];
}
if ($sessionAuth['tenant_id']) {
$extracted['tenant_id'] = $sessionAuth['tenant_id'];
}
// 스텝 결과 저장 (다음 스텝에서 {{login.token}} 등으로 참조 가능)
$this->binder->setStepResult($stepId, $extracted, [
'message' => '세션 인증 사용',
'user' => $sessionAuth['user'],
'tenant_id' => $sessionAuth['tenant_id'],
]);
return $this->buildStepResult($stepId, $stepName, $startTime, true, [
'description' => $step['description'] ?? '세션 인증 정보 사용',
'reason' => '✓ 세션 인증 사용 (API 호출 생략)',
'useSessionAuth' => true,
'sessionUser' => $sessionAuth['user'],
'extracted' => $extracted,
'response' => [
'status' => 200,
'body' => [
'message' => '세션 인증 정보를 사용합니다.',
'access_token' => substr($sessionAuth['token'], 0, 20).'...',
'user' => $sessionAuth['user'],
'tenant_id' => $sessionAuth['tenant_id'],
],
'duration' => 0,
],
]);
}
/**
* 플로우 정의 검증
*/

View File

@@ -156,11 +156,8 @@ function ($m) {
// {{$auth.apiKey}} → .env의 API Key
$input = str_replace('{{$auth.apiKey}}', env('FLOW_TESTER_API_KEY', ''), $input);
// {{$session.token}} → 세션에 저장된 Bearer 토큰 (API Explorer와 공유)
if (str_contains($input, '{{$session.token}}')) {
$token = session('api_explorer_token', '');
$input = str_replace('{{$session.token}}', $token, $input);
}
// {{$session.xxx}} → 세션에 저장된 인증 정보 (API Explorer와 공유)
$input = $this->resolveSessionVariables($input);
// {{$hmac.xxx}} → HMAC 인증용 동적 값 생성
$input = $this->resolveHmac($input);
@@ -493,4 +490,109 @@ public function getContext(): array
{
return $this->context;
}
/**
* 세션 변수 처리
*
* 지원 패턴:
* - {{$session.token}} - 세션에 저장된 Bearer 토큰
* - {{$session.user_id}} - 세션 사용자 ID
* - {{$session.user.email}} - 세션 사용자 이메일
* - {{$session.user.name}} - 세션 사용자 이름
* - {{$session.tenant_id}} - 세션 사용자의 기본 테넌트 ID
*/
private function resolveSessionVariables(string $input): string
{
// 세션 변수가 없으면 조기 반환
if (! str_contains($input, '{{$session.')) {
return $input;
}
// {{$session.token}} → 세션에 저장된 Bearer 토큰
if (str_contains($input, '{{$session.token}}')) {
$token = session('api_explorer_token', '');
$input = str_replace('{{$session.token}}', $token, $input);
}
// {{$session.user_id}} → 세션 사용자 ID
if (str_contains($input, '{{$session.user_id}}')) {
$userId = session('api_explorer_user_id', '');
$input = str_replace('{{$session.user_id}}', (string) $userId, $input);
}
// 사용자 상세 정보가 필요한 경우 (user.email, user.name, tenant_id)
$needsUserDetails = str_contains($input, '{{$session.user.') || str_contains($input, '{{$session.tenant_id}}');
if ($needsUserDetails) {
$userId = session('api_explorer_user_id');
$user = $userId ? \App\Models\User::find($userId) : null;
// {{$session.user.email}}
if (str_contains($input, '{{$session.user.email}}')) {
$email = $user?->email ?? '';
$input = str_replace('{{$session.user.email}}', $email, $input);
}
// {{$session.user.name}}
if (str_contains($input, '{{$session.user.name}}')) {
$name = $user?->name ?? '';
$input = str_replace('{{$session.user.name}}', $name, $input);
}
// {{$session.tenant_id}} → 사용자의 기본 테넌트 ID
if (str_contains($input, '{{$session.tenant_id}}')) {
$tenantId = '';
if ($user) {
$userTenant = \Illuminate\Support\Facades\DB::table('user_tenants')
->where('user_id', $user->id)
->where('is_default', true)
->whereNull('deleted_at')
->first();
$tenantId = $userTenant?->tenant_id ?? '';
}
$input = str_replace('{{$session.tenant_id}}', (string) $tenantId, $input);
}
}
return $input;
}
/**
* 세션 인증 정보 조회 (FlowExecutor에서 사용)
*
* @return array{token: string, user_id: int|null, user: array|null, tenant_id: int|null}
*/
public function getSessionAuth(): array
{
$token = session('api_explorer_token', '');
$userId = session('api_explorer_user_id');
$user = null;
$tenantId = null;
if ($userId) {
$userModel = \App\Models\User::find($userId);
if ($userModel) {
$user = [
'id' => $userModel->id,
'name' => $userModel->name,
'email' => $userModel->email,
];
// 기본 테넌트 조회
$userTenant = \Illuminate\Support\Facades\DB::table('user_tenants')
->where('user_id', $userModel->id)
->where('is_default', true)
->whereNull('deleted_at')
->first();
$tenantId = $userTenant?->tenant_id;
}
}
return [
'token' => $token,
'user_id' => $userId,
'user' => $user,
'tenant_id' => $tenantId,
];
}
}