fix: 워크스페이스 탭 중첩 셸 방지 + 세션 만료 시 로그인 리다이렉트
- EmbedRedirectFilter 추가: 임베드(embed=1) 요청의 리다이렉트 Location에 embed 유지(중첩 셸 방지) - bag/* 전체에 loginAuth 적용, 임베드 대시보드 로그아웃 시 로그인으로 이동 - 기본코드 종류 선택 시 embed 유지, 일괄입고 오류 복귀를 명시 URL로(back() 제거) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ class Filters extends BaseFilters
|
|||||||
public array $aliases = [
|
public array $aliases = [
|
||||||
'adminAuth' => \App\Filters\AdminAuthFilter::class,
|
'adminAuth' => \App\Filters\AdminAuthFilter::class,
|
||||||
'loginAuth' => \App\Filters\LoginAuthFilter::class,
|
'loginAuth' => \App\Filters\LoginAuthFilter::class,
|
||||||
|
'embedRedirect' => \App\Filters\EmbedRedirectFilter::class,
|
||||||
'csrf' => CSRF::class,
|
'csrf' => CSRF::class,
|
||||||
'toolbar' => DebugToolbar::class,
|
'toolbar' => DebugToolbar::class,
|
||||||
'honeypot' => Honeypot::class,
|
'honeypot' => Honeypot::class,
|
||||||
@@ -81,6 +82,7 @@ class Filters extends BaseFilters
|
|||||||
'after' => [
|
'after' => [
|
||||||
// 'honeypot',
|
// 'honeypot',
|
||||||
// 'secureheaders',
|
// 'secureheaders',
|
||||||
|
'embedRedirect', // 임베드(탭) 리다이렉트에 embed=1 유지 → 중첩 셸 방지
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -108,5 +110,10 @@ class Filters extends BaseFilters
|
|||||||
*
|
*
|
||||||
* @var array<string, array<string, list<string>>>
|
* @var array<string, array<string, list<string>>>
|
||||||
*/
|
*/
|
||||||
public array $filters = [];
|
public array $filters = [
|
||||||
|
// 모든 업무(bag) 화면은 로그인 필요. 세션 만료 후 어떤 버튼을 눌러도
|
||||||
|
// 깨진 화면 대신 로그인으로 리다이렉트되도록 일괄 보호한다.
|
||||||
|
// (login/logout/register 는 bag/* 가 아니므로 영향 없음. 관리자 화면은 adminAuth 가 별도 처리)
|
||||||
|
'loginAuth' => ['before' => ['bag', 'bag/*']],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -592,6 +592,7 @@ class Bag extends BaseController
|
|||||||
'selectedKind' => $selectedKind,
|
'selectedKind' => $selectedKind,
|
||||||
'detailList' => $detailList,
|
'detailList' => $detailList,
|
||||||
'rowCanEdit' => $rowCanEdit,
|
'rowCanEdit' => $rowCanEdit,
|
||||||
|
'isEmbed' => $this->isEmbeddedRequest(), // 행 클릭 시 embed 유지(중첩 셸 방지)
|
||||||
], true); // 본문이 이미 카드 2개라 바깥 래퍼 생략
|
], true); // 본문이 이미 카드 2개라 바깥 래퍼 생략
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4768,14 +4769,18 @@ SQL);
|
|||||||
$receiverIdx = $this->parseReceiverRefToStoredIdx($lgIdx, $receiverRef);
|
$receiverIdx = $this->parseReceiverRefToStoredIdx($lgIdx, $receiverRef);
|
||||||
$senderIdx = (int) ($this->request->getPost('br_sender_idx') ?? 0);
|
$senderIdx = (int) ($this->request->getPost('br_sender_idx') ?? 0);
|
||||||
|
|
||||||
|
// 오류 시 명시적으로 일괄 입고 화면으로 복귀(back()은 도움말 드로어 등이 남긴
|
||||||
|
// previous_url 로 잘못 이동할 수 있어 사용하지 않는다)
|
||||||
|
$batchUrl = site_url('bag/receiving/batch?company_idx=' . $companyIdx . '&bag_code=' . rawurlencode($bagCode));
|
||||||
|
|
||||||
if (empty($selected)) {
|
if (empty($selected)) {
|
||||||
return redirect()->back()->withInput()->with('error', '일괄 입고할 행을 선택해 주세요.');
|
return redirect()->to($batchUrl)->withInput()->with('error', '일괄 입고할 행을 선택해 주세요.');
|
||||||
}
|
}
|
||||||
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $receiveDate)) {
|
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $receiveDate)) {
|
||||||
return redirect()->back()->withInput()->with('error', '입고일 형식을 확인해 주세요.');
|
return redirect()->to($batchUrl)->withInput()->with('error', '입고일 형식을 확인해 주세요.');
|
||||||
}
|
}
|
||||||
if ($receiverIdx <= 0) {
|
if ($receiverIdx <= 0) {
|
||||||
return redirect()->back()->withInput()->with('error', '인수자를 선택해 주세요.');
|
return redirect()->to($batchUrl)->withInput()->with('error', '인수자를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$senderResolved = $this->resolveCompanySenderName($lgIdx, $senderIdx);
|
$senderResolved = $this->resolveCompanySenderName($lgIdx, $senderIdx);
|
||||||
@@ -4816,7 +4821,7 @@ SQL);
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (empty($insertRows)) {
|
if (empty($insertRows)) {
|
||||||
return redirect()->back()->withInput()->with('error', '선택한 행에 입고할 수량이 없습니다.');
|
return redirect()->to($batchUrl)->withInput()->with('error', '선택한 행에 입고할 수량이 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$recvModel = model(BagReceivingModel::class);
|
$recvModel = model(BagReceivingModel::class);
|
||||||
@@ -4846,7 +4851,7 @@ SQL);
|
|||||||
$db->transComplete();
|
$db->transComplete();
|
||||||
|
|
||||||
if (! $db->transStatus()) {
|
if (! $db->transStatus()) {
|
||||||
return redirect()->back()->withInput()->with('error', '일괄 입고 처리 중 오류가 발생했습니다.');
|
return redirect()->to($batchUrl)->withInput()->with('error', '일괄 입고 처리 중 오류가 발생했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->to(site_url('bag/receiving/batch?company_idx=' . $companyIdx . '&bag_code=' . rawurlencode($bagCode)))
|
return redirect()->to(site_url('bag/receiving/batch?company_idx=' . $companyIdx . '&bag_code=' . rawurlencode($bagCode)))
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ class Home extends BaseController
|
|||||||
return view('bag/layout/workspace');
|
return view('bag/layout/workspace');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 워크스페이스 탭(iframe) 안에서 세션이 만료된 경우: 공개 랜딩 대신 로그인으로 보내
|
||||||
|
// 로그인 페이지의 프레임 이탈 스크립트가 상위 창 전체를 로그인으로 전환하게 한다.
|
||||||
|
if ($this->isEmbeddedRequest()) {
|
||||||
|
return redirect()->to(base_url('login'));
|
||||||
|
}
|
||||||
|
|
||||||
return view('welcome_message');
|
return view('welcome_message');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
app/Filters/EmbedRedirectFilter.php
Normal file
54
app/Filters/EmbedRedirectFilter.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filters;
|
||||||
|
|
||||||
|
use CodeIgniter\Filters\FilterInterface;
|
||||||
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 워크스페이스 탭(iframe, embed=1) 안에서 발생한 리다이렉트가 embed 파라미터를 잃지 않도록
|
||||||
|
* 응답 Location 헤더에 embed=1 을 유지시킨다.
|
||||||
|
*
|
||||||
|
* 배경: 폼 전송 후 redirect()->to(...) 하면 Location 에 embed 가 빠지고, iframe 안에서
|
||||||
|
* 그 주소로 다시 GET 하면(특히 HTTP 환경에서 Sec-Fetch-Dest 미전송) 전체 셸(헤더·사이드바)이
|
||||||
|
* 한 번 더 렌더되어 "화면 안에 전체 화면이 또" 생긴다. 이를 전역에서 한 번에 방지한다.
|
||||||
|
*/
|
||||||
|
class EmbedRedirectFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
|
{
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string>|null $arguments
|
||||||
|
*/
|
||||||
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||||
|
{
|
||||||
|
// 임베드 요청에서 시작된 리다이렉트만 처리
|
||||||
|
if ($request->getGet('embed') === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$location = $response->getHeaderLine('Location');
|
||||||
|
if ($location === '') {
|
||||||
|
return; // 리다이렉트 응답이 아님
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 embed 파라미터가 있으면 그대로 둔다
|
||||||
|
if (preg_match('/[?&]embed=/', $location) === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증 흐름(로그인/로그아웃/회원가입)은 상위 프레임에서 처리하므로 embed 를 붙이지 않는다
|
||||||
|
if (preg_match('#/(login|logout|register)(/|\?|$)#', $location) === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sep = strpos($location, '?') !== false ? '&' : '?';
|
||||||
|
$response->setHeader('Location', $location . $sep . 'embed=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ $showKindActions = $canManageKinds;
|
|||||||
$selectedKindId = (int) ($selectedKind->ck_idx ?? 0);
|
$selectedKindId = (int) ($selectedKind->ck_idx ?? 0);
|
||||||
$colCount = 6 + ($showKindActions ? 1 : 0);
|
$colCount = 6 + ($showKindActions ? 1 : 0);
|
||||||
$detailColCount = 7 + ($canManageDetails ? 1 : 0);
|
$detailColCount = 7 + ($canManageDetails ? 1 : 0);
|
||||||
|
$embedQs = ! empty($isEmbed) ? '&embed=1' : ''; // 워크스페이스 탭 안에서는 embed 유지(중첩 셸 방지)
|
||||||
|
|
||||||
/** 상태 배지 (업무현황 스타일의 가벼운 pill) */
|
/** 상태 배지 (업무현황 스타일의 가벼운 pill) */
|
||||||
$stateBadge = static function (int $state): string {
|
$stateBadge = static function (int $state): string {
|
||||||
@@ -51,7 +52,7 @@ $stateBadge = static function (int $state): string {
|
|||||||
<?php $i = 0; foreach ($codeKinds as $row): $i++; ?>
|
<?php $i = 0; foreach ($codeKinds as $row): $i++; ?>
|
||||||
<?php
|
<?php
|
||||||
$isSelected = (int) $row->ck_idx === $selectedKindId;
|
$isSelected = (int) $row->ck_idx === $selectedKindId;
|
||||||
$detailUrl = base_url('bag/code-kinds?ck_idx=' . (int) $row->ck_idx);
|
$detailUrl = base_url('bag/code-kinds?ck_idx=' . (int) $row->ck_idx . $embedQs);
|
||||||
?>
|
?>
|
||||||
<tr class="border-b border-gray-200 last:border-0 cursor-pointer hover:bg-blue-50/60 <?= $isSelected ? 'bg-blue-50' : '' ?>"
|
<tr class="border-b border-gray-200 last:border-0 cursor-pointer hover:bg-blue-50/60 <?= $isSelected ? 'bg-blue-50' : '' ?>"
|
||||||
onclick="window.location.href='<?= esc($detailUrl, 'attr') ?>'">
|
onclick="window.location.href='<?= esc($detailUrl, 'attr') ?>'">
|
||||||
|
|||||||
Reference in New Issue
Block a user