## 인증 모달 통합
- api-explorer, flow-tester, api-logs 3개 페이지의 인증 UI 통합
- 공유 컴포넌트 생성: auth-modal.blade.php, auth-scripts.blade.php
- sessionStorage 기반으로 페이지 간 인증 상태 공유
- DevToolsAuth 글로벌 JavaScript API 제공
## 테넌트 사용자 조회 개선
- 시스템 헤더에서 선택한 테넌트의 사용자 목록 표시
- 관리자가 모든 테넌트의 사용자 조회 가능 (소속 무관)
- session('selected_tenant_id')로 Tenant 모델 직접 조회
- 테넌트 미선택 시 안내 메시지 표시
## 버그 수정
- /users 페이지 HTMX swap 오류 수정 (JSON→HTML 직접 반환)
- 사용자 이름 JavaScript 이스케이프 처리 (@js() 사용)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
390 lines
13 KiB
PHP
390 lines
13 KiB
PHP
{{--
|
|
Dev Tools 공유 인증 스크립트
|
|
사용법: @include('dev-tools.partials.auth-scripts')
|
|
|
|
제공 API:
|
|
- DevToolsAuth.openModal() : 인증 모달 열기
|
|
- DevToolsAuth.closeModal() : 인증 모달 닫기
|
|
- DevToolsAuth.getToken() : 현재 토큰 반환
|
|
- DevToolsAuth.getUserId() : 현재 사용자 ID 반환
|
|
- DevToolsAuth.isAuthenticated() : 인증 여부 반환
|
|
- DevToolsAuth.getAuthPayload() : API 요청용 인증 페이로드 반환
|
|
- DevToolsAuth.onAuthChange(callback) : 인증 상태 변경 콜백 등록
|
|
--}}
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
const STORAGE_KEY = 'devToolsAuth';
|
|
const USERS_ENDPOINT = '{{ route("dev-tools.api-explorer.users") }}';
|
|
const CSRF_TOKEN = '{{ csrf_token() }}';
|
|
|
|
// 인증 상태
|
|
let state = {
|
|
token: null,
|
|
userId: null,
|
|
userName: null,
|
|
userEmail: null
|
|
};
|
|
|
|
// 사용자 목록 캐시
|
|
let usersLoaded = false;
|
|
let usersCache = [];
|
|
let currentTenantInfo = null;
|
|
|
|
// 상태 변경 콜백 목록
|
|
let changeCallbacks = [];
|
|
|
|
// sessionStorage에서 상태 로드
|
|
function loadState() {
|
|
try {
|
|
const saved = sessionStorage.getItem(STORAGE_KEY);
|
|
if (saved) {
|
|
state = JSON.parse(saved);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load auth state:', e);
|
|
}
|
|
}
|
|
|
|
// sessionStorage에 상태 저장
|
|
function saveState() {
|
|
try {
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
} catch (e) {
|
|
console.warn('Failed to save auth state:', e);
|
|
}
|
|
}
|
|
|
|
// UI 업데이트
|
|
function updateUI() {
|
|
const isAuth = !!(state.token || state.userId);
|
|
|
|
// 헤더의 인증 상태 버튼 업데이트 (id="auth-status" 또는 class="dev-tools-auth-status")
|
|
document.querySelectorAll('#auth-status, .dev-tools-auth-status').forEach(el => {
|
|
el.textContent = isAuth ? '인증됨' : '인증 필요';
|
|
el.classList.toggle('text-green-600', isAuth);
|
|
el.classList.toggle('text-gray-500', !isAuth);
|
|
});
|
|
|
|
// 모달 내 상태 표시
|
|
const modalStatus = document.getElementById('devToolsAuthStatusDisplay');
|
|
if (modalStatus) {
|
|
modalStatus.textContent = isAuth ? '인증됨' : '인증 필요';
|
|
modalStatus.classList.toggle('text-green-600', isAuth);
|
|
modalStatus.classList.toggle('text-gray-500', !isAuth);
|
|
}
|
|
|
|
// 사용자 정보 표시
|
|
const userInfo = document.getElementById('devToolsAuthUserInfo');
|
|
if (userInfo) {
|
|
if (state.userId && state.userName) {
|
|
userInfo.textContent = `사용자: ${state.userName} (${state.userEmail})`;
|
|
userInfo.classList.remove('hidden');
|
|
} else if (state.token) {
|
|
userInfo.textContent = '토큰으로 인증됨';
|
|
userInfo.classList.remove('hidden');
|
|
} else {
|
|
userInfo.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// 토큰 입력 필드 업데이트
|
|
const tokenInput = document.getElementById('devToolsAuthBearerToken');
|
|
if (tokenInput && state.token) {
|
|
tokenInput.value = state.token;
|
|
}
|
|
|
|
// 사용자 선택 업데이트
|
|
const userSelect = document.getElementById('devToolsAuthSelectedUser');
|
|
if (userSelect && state.userId) {
|
|
userSelect.value = state.userId;
|
|
}
|
|
}
|
|
|
|
// 콜백 호출
|
|
function notifyChange() {
|
|
const isAuth = !!(state.token || state.userId);
|
|
changeCallbacks.forEach(cb => {
|
|
try {
|
|
cb(isAuth, state);
|
|
} catch (e) {
|
|
console.error('Auth change callback error:', e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 사용자 목록 로드
|
|
async function loadUsers() {
|
|
if (usersLoaded) return usersCache;
|
|
|
|
const spinner = document.getElementById('devToolsAuthUserSpinner');
|
|
const select = document.getElementById('devToolsAuthSelectedUser');
|
|
const tenantLabel = document.getElementById('devToolsAuthTenantLabel');
|
|
|
|
if (spinner) spinner.classList.remove('hidden');
|
|
|
|
try {
|
|
const response = await fetch(USERS_ENDPOINT, {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': CSRF_TOKEN
|
|
}
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to load users');
|
|
|
|
const data = await response.json();
|
|
|
|
// 새 응답 형식: { tenant: {...}, users: [...] }
|
|
if (data.tenant) {
|
|
currentTenantInfo = data.tenant;
|
|
usersCache = data.users || [];
|
|
} else if (Array.isArray(data) && data.length === 0) {
|
|
// 빈 배열 응답 - 테넌트 미선택
|
|
currentTenantInfo = null;
|
|
usersCache = [];
|
|
} else {
|
|
// 기존 형식 호환성 유지
|
|
usersCache = Array.isArray(data) ? data : (data.data || []);
|
|
}
|
|
usersLoaded = true;
|
|
|
|
// 테넌트 정보 표시
|
|
if (tenantLabel) {
|
|
if (currentTenantInfo) {
|
|
tenantLabel.textContent = `테넌트: ${currentTenantInfo.name}`;
|
|
tenantLabel.classList.remove('hidden');
|
|
tenantLabel.className = 'mb-2 px-3 py-2 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-700';
|
|
} else {
|
|
tenantLabel.textContent = '시스템 헤더에서 테넌트를 먼저 선택해주세요.';
|
|
tenantLabel.classList.remove('hidden');
|
|
tenantLabel.className = 'mb-2 px-3 py-2 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-700';
|
|
}
|
|
}
|
|
|
|
if (select) {
|
|
select.innerHTML = '<option value="">사용자를 선택하세요</option>';
|
|
if (usersCache.length === 0 && currentTenantInfo) {
|
|
select.innerHTML = '<option value="">이 테넌트에 사용자가 없습니다</option>';
|
|
}
|
|
usersCache.forEach(user => {
|
|
const option = document.createElement('option');
|
|
option.value = user.id;
|
|
option.textContent = `${user.name || user.email} (${user.email})`;
|
|
option.dataset.name = user.name || user.email;
|
|
option.dataset.email = user.email;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
// 현재 선택된 사용자 복원
|
|
if (state.userId) {
|
|
select.value = state.userId;
|
|
}
|
|
}
|
|
|
|
return usersCache;
|
|
} catch (err) {
|
|
console.error('사용자 목록 로드 실패:', err);
|
|
if (typeof showToast === 'function') {
|
|
showToast('사용자 목록을 불러오는데 실패했습니다.', 'error');
|
|
}
|
|
return [];
|
|
} finally {
|
|
if (spinner) spinner.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// 공개 API
|
|
window.DevToolsAuth = {
|
|
// 모달 열기
|
|
openModal() {
|
|
const modal = document.getElementById('devToolsAuthModal');
|
|
if (modal) {
|
|
modal.classList.remove('hidden');
|
|
updateUI();
|
|
|
|
// 인증 방식 라디오 버튼 설정
|
|
const tokenRadio = document.querySelector('input[name="devToolsAuthType"][value="token"]');
|
|
const userRadio = document.querySelector('input[name="devToolsAuthType"][value="user"]');
|
|
if (state.userId && !state.token) {
|
|
if (userRadio) userRadio.checked = true;
|
|
this.toggleType();
|
|
} else {
|
|
if (tokenRadio) tokenRadio.checked = true;
|
|
this.toggleType();
|
|
}
|
|
}
|
|
},
|
|
|
|
// 모달 닫기
|
|
closeModal() {
|
|
const modal = document.getElementById('devToolsAuthModal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
}
|
|
},
|
|
|
|
// 인증 방식 토글
|
|
toggleType() {
|
|
const authType = document.querySelector('input[name="devToolsAuthType"]:checked')?.value || 'token';
|
|
const tokenSection = document.getElementById('devToolsAuthTokenSection');
|
|
const userSection = document.getElementById('devToolsAuthUserSection');
|
|
|
|
if (authType === 'token') {
|
|
if (tokenSection) tokenSection.classList.remove('hidden');
|
|
if (userSection) userSection.classList.add('hidden');
|
|
} else {
|
|
if (tokenSection) tokenSection.classList.add('hidden');
|
|
if (userSection) userSection.classList.remove('hidden');
|
|
// 사용자 선택 시 항상 새로 로드 (테넌트 변경 대응)
|
|
usersLoaded = false;
|
|
loadUsers();
|
|
}
|
|
},
|
|
|
|
// 인증 저장
|
|
save() {
|
|
const authType = document.querySelector('input[name="devToolsAuthType"]:checked')?.value || 'token';
|
|
|
|
if (authType === 'token') {
|
|
const tokenInput = document.getElementById('devToolsAuthBearerToken');
|
|
const token = tokenInput?.value?.trim();
|
|
|
|
if (!token) {
|
|
if (typeof showToast === 'function') {
|
|
showToast('토큰을 입력해주세요.', 'warning');
|
|
}
|
|
return;
|
|
}
|
|
|
|
state = {
|
|
token: token,
|
|
userId: null,
|
|
userName: null,
|
|
userEmail: null
|
|
};
|
|
|
|
if (typeof showToast === 'function') {
|
|
showToast('토큰이 적용되었습니다.', 'success');
|
|
}
|
|
} else {
|
|
const userSelect = document.getElementById('devToolsAuthSelectedUser');
|
|
const userId = userSelect?.value;
|
|
|
|
if (!userId) {
|
|
if (typeof showToast === 'function') {
|
|
showToast('사용자를 선택해주세요.', 'warning');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const selectedOption = userSelect.options[userSelect.selectedIndex];
|
|
|
|
state = {
|
|
token: null,
|
|
userId: userId,
|
|
userName: selectedOption?.dataset?.name || '',
|
|
userEmail: selectedOption?.dataset?.email || ''
|
|
};
|
|
|
|
if (typeof showToast === 'function') {
|
|
showToast('사용자가 선택되었습니다. API 실행 시 토큰이 자동 발급됩니다.', 'success');
|
|
}
|
|
}
|
|
|
|
saveState();
|
|
updateUI();
|
|
notifyChange();
|
|
this.closeModal();
|
|
},
|
|
|
|
// 인증 초기화
|
|
clear() {
|
|
state = {
|
|
token: null,
|
|
userId: null,
|
|
userName: null,
|
|
userEmail: null
|
|
};
|
|
|
|
const tokenInput = document.getElementById('devToolsAuthBearerToken');
|
|
const userSelect = document.getElementById('devToolsAuthSelectedUser');
|
|
|
|
if (tokenInput) tokenInput.value = '';
|
|
if (userSelect) userSelect.value = '';
|
|
|
|
saveState();
|
|
updateUI();
|
|
notifyChange();
|
|
|
|
if (typeof showToast === 'function') {
|
|
showToast('인증이 초기화되었습니다.', 'info');
|
|
}
|
|
},
|
|
|
|
// 현재 토큰 반환
|
|
getToken() {
|
|
return state.token;
|
|
},
|
|
|
|
// 현재 사용자 ID 반환
|
|
getUserId() {
|
|
return state.userId;
|
|
},
|
|
|
|
// 인증 여부 반환
|
|
isAuthenticated() {
|
|
return !!(state.token || state.userId);
|
|
},
|
|
|
|
// API 요청용 인증 페이로드 반환
|
|
getAuthPayload() {
|
|
if (state.token) {
|
|
return { token: state.token };
|
|
}
|
|
if (state.userId) {
|
|
return { user_id: state.userId };
|
|
}
|
|
return {};
|
|
},
|
|
|
|
// 현재 상태 반환
|
|
getState() {
|
|
return { ...state };
|
|
},
|
|
|
|
// 인증 상태 변경 콜백 등록
|
|
onAuthChange(callback) {
|
|
if (typeof callback === 'function') {
|
|
changeCallbacks.push(callback);
|
|
}
|
|
},
|
|
|
|
// 외부에서 상태 설정 (서버에서 저장된 토큰 로드 시 사용)
|
|
setToken(token) {
|
|
if (token) {
|
|
state.token = token;
|
|
state.userId = null;
|
|
state.userName = null;
|
|
state.userEmail = null;
|
|
saveState();
|
|
updateUI();
|
|
notifyChange();
|
|
}
|
|
}
|
|
};
|
|
|
|
// 초기화
|
|
loadState();
|
|
|
|
// DOM 로드 후 UI 업데이트
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', updateUI);
|
|
} else {
|
|
updateUI();
|
|
}
|
|
})();
|
|
</script>
|