205 lines
8.9 KiB
PHP
205 lines
8.9 KiB
PHP
{{-- resources/views/admin/partials/dev_session_overlay.blade.php --}}
|
|
@php
|
|
// ✅ 개발 모드에서만 노출
|
|
$show = (config('app.debug') || app()->environment('local'));
|
|
|
|
// ✅ 관리자 도메인에서만 노출(실수로 web 도메인에 붙는 것 방지)
|
|
$adminHost = parse_url(env('APP_ADMIN_URL'), PHP_URL_HOST);
|
|
if ($adminHost) {
|
|
$show = $show && (request()->getHost() === $adminHost);
|
|
}
|
|
|
|
// ✅ 세션 전체
|
|
$sess = session()->all();
|
|
|
|
// ✅ admin은 기본 마스킹을 켜는 게 안전 (원하면 비워도 됨)
|
|
$maskKeys = [
|
|
'password','passwd','pw',
|
|
'token','access_token','refresh_token',
|
|
'api_key','secret','authorization',
|
|
'csrf','_token',
|
|
'g-recaptcha-response','recaptcha',
|
|
'otp','admin_otp',
|
|
'ci','di','phone','mobile','email',
|
|
'session','cookie',
|
|
];
|
|
|
|
$mask = function ($key, $val) use ($maskKeys) {
|
|
$k = strtolower((string)$key);
|
|
foreach ($maskKeys as $mk) {
|
|
if (str_contains($k, $mk)) return '***';
|
|
}
|
|
return $val;
|
|
};
|
|
|
|
// ✅ key:value 라인 생성(재귀)
|
|
$lines = [];
|
|
$dump = function ($data, $prefix = '') use (&$dump, &$lines, $mask) {
|
|
foreach ((array)$data as $k => $v) {
|
|
$key = $prefix . (string)$k;
|
|
|
|
if (is_array($v)) {
|
|
$lines[] = $key . ' : [';
|
|
$dump($v, $prefix . ' ');
|
|
$lines[] = $prefix . ']';
|
|
} else {
|
|
if (is_bool($v)) $v = $v ? 'true' : 'false';
|
|
if ($v === null) $v = 'null';
|
|
|
|
$vv = is_string($v) ? (mb_strlen($v) > 260 ? mb_substr($v, 0, 260) . '…' : $v) : (string)$v;
|
|
$vv = $mask($k, $vv);
|
|
|
|
$lines[] = $key . ' : ' . $vv;
|
|
}
|
|
}
|
|
};
|
|
$dump($sess);
|
|
$text = implode("\n", $lines);
|
|
|
|
// ✅ dev route 이름 (너가 만든 이름에 맞춰 사용)
|
|
// - 내가 권장했던 방식이면 admin.dev.session
|
|
$devRouteName = 'admin.dev.session';
|
|
@endphp
|
|
|
|
@if($show)
|
|
<div id="dev-session-overlay" style="
|
|
position: fixed; left: 12px; bottom: 12px; z-index: 999999;
|
|
width: 620px; max-width: calc(100vw - 24px);
|
|
background: rgba(10,10,10,.92); color: #eaeaea;
|
|
border: 1px solid rgba(255,255,255,.14);
|
|
border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.35);
|
|
font: 12px/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
|
|
">
|
|
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; gap:8px;">
|
|
<div style="font-weight:800;">
|
|
ADMIN SESSION
|
|
<span style="opacity:.65; font-weight:500;">({{ count($sess) }} keys)</span>
|
|
</div>
|
|
<div style="display:flex; gap:6px;">
|
|
<button type="button" id="devSessCopy" style="
|
|
border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.06);
|
|
color:#fff; padding:5px 10px; border-radius:10px; cursor:pointer;
|
|
">Copy</button>
|
|
<button type="button" id="devSessToggle" style="
|
|
border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.06);
|
|
color:#fff; padding:5px 10px; border-radius:10px; cursor:pointer;
|
|
">Hide</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="devSessBody" style="padding:0 12px 12px 12px;">
|
|
<div style="opacity:.7; margin-bottom:8px;">
|
|
{{ request()->method() }} {{ request()->path() }}
|
|
</div>
|
|
|
|
{{-- ✅ Controls --}}
|
|
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px;">
|
|
<form method="POST" action="{{ route($devRouteName) }}" style="display:flex; gap:6px; align-items:center; margin:0;">
|
|
@csrf
|
|
<input type="hidden" name="_dev_sess_action" value="flush">
|
|
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
|
|
@if(env('DEV_LAB_TOKEN'))
|
|
<input type="hidden" name="_dev_token" value="{{ env('DEV_LAB_TOKEN') }}">
|
|
@endif
|
|
|
|
<button type="submit" style="
|
|
border: 1px solid rgba(255,90,90,.35);
|
|
background: rgba(255,90,90,.14);
|
|
color:#fff; padding:6px 10px; border-radius:10px; cursor:pointer;
|
|
" onclick="return confirm('세션을 초기화할까요? (dev 전용)');">세션 초기화</button>
|
|
</form>
|
|
|
|
<form method="POST" action="{{ route($devRouteName) }}" style="display:flex; gap:6px; align-items:center; margin:0; flex:1;">
|
|
@csrf
|
|
<input type="hidden" name="_dev_sess_action" value="put">
|
|
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
|
|
@if(env('DEV_LAB_TOKEN'))
|
|
<input type="hidden" name="_dev_token" value="{{ env('DEV_LAB_TOKEN') }}">
|
|
@endif
|
|
|
|
<input name="_dev_sess_key" placeholder="key (예: admin_otp.expires_at)" style="
|
|
flex: 0 0 240px; max-width: 45%;
|
|
border: 1px solid rgba(255,255,255,.16);
|
|
background: rgba(255,255,255,.06);
|
|
color:#fff; padding:6px 8px; border-radius:10px; outline:none;
|
|
">
|
|
<input name="_dev_sess_value" placeholder="value (기본은 문자열, route에서 파싱 가능)" style="
|
|
flex: 1 1 auto;
|
|
border: 1px solid rgba(255,255,255,.16);
|
|
background: rgba(255,255,255,.06);
|
|
color:#fff; padding:6px 8px; border-radius:10px; outline:none;
|
|
">
|
|
<button type="submit" style="
|
|
border: 1px solid rgba(90,180,255,.35);
|
|
background: rgba(90,180,255,.14);
|
|
color:#fff; padding:6px 10px; border-radius:10px; cursor:pointer;
|
|
">등록</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div style="
|
|
padding:10px; border-radius:12px;
|
|
background: rgba(255,255,255,.05);
|
|
max-height: 360px; overflow:auto; white-space: pre;
|
|
border: 1px solid rgba(255,255,255,.10);
|
|
">
|
|
<pre id="devSessPre" style="margin:0;">{!! e($text) !!}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
const box = document.getElementById('dev-session-overlay');
|
|
const body = document.getElementById('devSessBody');
|
|
const toggle = document.getElementById('devSessToggle');
|
|
const copyBtn = document.getElementById('devSessCopy');
|
|
const pre = document.getElementById('devSessPre');
|
|
|
|
// 상태 기억 (도메인별 분리)
|
|
const key = 'devSessOverlayCollapsed:' + location.host;
|
|
const collapsed = localStorage.getItem(key) === '1';
|
|
if (collapsed) { body.style.display='none'; toggle.textContent='Show'; }
|
|
|
|
toggle.addEventListener('click', () => {
|
|
const isHidden = body.style.display === 'none';
|
|
body.style.display = isHidden ? '' : 'none';
|
|
toggle.textContent = isHidden ? 'Hide' : 'Show';
|
|
localStorage.setItem(key, isHidden ? '0' : '1');
|
|
});
|
|
|
|
copyBtn.addEventListener('click', async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(pre.innerText);
|
|
copyBtn.textContent = 'Copied';
|
|
setTimeout(()=>copyBtn.textContent='Copy', 800);
|
|
} catch(e) {
|
|
alert('복사 실패(브라우저 권한 확인)');
|
|
}
|
|
});
|
|
|
|
// 드래그 이동
|
|
let dragging=false, sx=0, sy=0, ox=0, oy=0;
|
|
box.firstElementChild.style.cursor = 'move';
|
|
box.firstElementChild.addEventListener('mousedown', (e)=>{
|
|
dragging=true; sx=e.clientX; sy=e.clientY;
|
|
const r = box.getBoundingClientRect();
|
|
ox=r.left; oy=r.top;
|
|
box.style.right='auto'; box.style.bottom='auto';
|
|
document.body.style.userSelect='none';
|
|
});
|
|
window.addEventListener('mousemove', (e)=>{
|
|
if(!dragging) return;
|
|
const nx = ox + (e.clientX - sx);
|
|
const ny = oy + (e.clientY - sy);
|
|
box.style.left = Math.max(0, nx) + 'px';
|
|
box.style.top = Math.max(0, ny) + 'px';
|
|
});
|
|
window.addEventListener('mouseup', ()=>{
|
|
dragging=false;
|
|
document.body.style.userSelect='';
|
|
});
|
|
})();
|
|
</script>
|
|
@endif
|