diff --git a/app/Http/Controllers/Api/Admin/UserController.php b/app/Http/Controllers/Api/Admin/UserController.php index 5e9a6543..2a27b70d 100644 --- a/app/Http/Controllers/Api/Admin/UserController.php +++ b/app/Http/Controllers/Api/Admin/UserController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\StoreUserRequest; use App\Http\Requests\UpdateUserRequest; +use App\Models\LoginToken; use App\Services\UserService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -292,4 +293,47 @@ public function forceDestroy(Request $request, int $id): JsonResponse 'message' => '사용자가 영구 삭제되었습니다.', ]); } + + /** + * DEV 사이트 자동 로그인 토큰 생성 + * MNG → DEV 자동 로그인용 One-Time Token 발급 + */ + public function loginToken(Request $request, int $id): JsonResponse + { + try { + // 슈퍼관리자 보호: 일반관리자가 슈퍼관리자로 로그인 불가 + if (! $this->userService->canAccessUser($id)) { + return response()->json([ + 'success' => false, + 'message' => '사용자를 찾을 수 없습니다.', + ], 404); + } + + $user = $this->userService->getUserById($id); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => '사용자를 찾을 수 없습니다.', + ], 404); + } + + // One-Time Token 생성 + $loginToken = LoginToken::createForUser($user->id); + + return response()->json([ + 'success' => true, + 'message' => 'DEV 접속 토큰이 생성되었습니다.', + 'data' => [ + 'url' => $loginToken->getAutoLoginUrl(), + 'expires_at' => $loginToken->expires_at->toIso8601String(), + ], + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'DEV 접속 토큰 생성에 실패했습니다: '.$e->getMessage(), + ], 500); + } + } } diff --git a/app/Models/LoginToken.php b/app/Models/LoginToken.php new file mode 100644 index 00000000..6c9f36b4 --- /dev/null +++ b/app/Models/LoginToken.php @@ -0,0 +1,61 @@ + 'datetime', + ]; + + /** + * 토큰 만료 시간 (분) + */ + public const EXPIRES_IN_MINUTES = 5; + + /** + * 사용자 관계 + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * 새 토큰 생성 + */ + public static function createForUser(int $userId): self + { + // 기존 토큰 삭제 (해당 사용자) + self::where('user_id', $userId)->delete(); + + return self::create([ + 'user_id' => $userId, + 'token' => Str::random(64), + 'expires_at' => now()->addMinutes(self::EXPIRES_IN_MINUTES), + ]); + } + + /** + * DEV 사이트 자동 로그인 URL 생성 + */ + public function getAutoLoginUrl(): string + { + $devAppUrl = config('services.dev.app_url', 'https://dev.sam.kr'); + + return "{$devAppUrl}/auto-login?token={$this->token}"; + } +} diff --git a/config/services.php b/config/services.php index 8ab33f31..c96836c1 100644 --- a/config/services.php +++ b/config/services.php @@ -64,4 +64,16 @@ 'exchange_secret' => env('INTERNAL_EXCHANGE_SECRET'), ], + /* + |-------------------------------------------------------------------------- + | DEV Site (React Frontend) + |-------------------------------------------------------------------------- + | MNG → DEV 자동 로그인 시 사용할 DEV 사이트 URL + | - 로컬: https://dev.sam.kr + | - 개발: https://dev.codebridge-x.com + */ + 'dev' => [ + 'app_url' => env('DEV_APP_URL', 'https://dev.sam.kr'), + ], + ]; diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index 4d1bb881..1710f741 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -115,5 +115,32 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }); }); }; + + // DEV 사이트 접속 (자동 로그인) + window.openDevSite = function(id, name) { + showConfirm(`"${name}" 사용자로 DEV 사이트에 접속하시겠습니까?`, () => { + // 토큰 생성 API 호출 + fetch(`/api/admin/users/${id}/login-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success && data.data?.url) { + // 새 창에서 DEV 사이트 열기 + window.open(data.data.url, '_blank'); + } else { + showAlert(data.message || 'DEV 접속 토큰 생성에 실패했습니다.', 'error'); + } + }) + .catch(error => { + console.error('DEV 접속 오류:', error); + showAlert('DEV 접속 중 오류가 발생했습니다.', 'error'); + }); + }, { title: 'DEV 사이트 접속', icon: 'question' }); + }; @endpush \ No newline at end of file diff --git a/resources/views/users/partials/table.blade.php b/resources/views/users/partials/table.blade.php index 81ee0c9c..e097942d 100644 --- a/resources/views/users/partials/table.blade.php +++ b/resources/views/users/partials/table.blade.php @@ -115,6 +115,11 @@ class="text-red-600 hover:text-red-900"> @endif @elseif($canModify) + diff --git a/routes/api.php b/routes/api.php index 79121a18..0965d4e8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -108,6 +108,9 @@ Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy'); }); + // DEV 사이트 자동 로그인 토큰 생성 (MNG → DEV) + Route::post('/{id}/login-token', [UserController::class, 'loginToken'])->name('loginToken'); + // 모달 관련 API Route::get('/{id}/modal', [UserController::class, 'modal'])->name('modal'); });