feat: add account menu and redirect after Q&A logout
Deploy EG-BIM QA Gateway / deploy (push) Successful in 54s
Deploy EG-BIM QA Gateway / deploy (push) Successful in 54s
Add the top-right account icon with the signed-in name and logout link. Expire the Q&A session before redirecting to the configured inquiry homepage in both environments. Preserve login return URL checks and ignore request-controlled logout destinations. Validation: 106 tests, syntax checks, and mocked Chromium desktop/mobile regressions pass, including session-cookie removal and the destination navigation request with external networking blocked.
This commit is contained in:
1 parent
2b37bed2b8
commit
beaa2fc423
13 files changed
+135
-13
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# EG-BIM Q&A static pages
|
||||
|
||||
`egbim/index.html`의 목록 위에 작성·상세 모달을 여는 EG-BIM Q&A UI입니다. `SW문의_소스패키지`의 히어로·제품 탭·목록·모달 디자인을 적용하고, 기존 첨부·댓글·비밀글·작성자 권한·검색을 유지합니다. 회사 홈페이지의 헤더·메뉴·푸터는 포함하지 않습니다. 기존 `write.html`, `detail.html?id=...` 주소는 같은 목록의 모달로 연결됩니다.
|
||||
`egbim/index.html`의 목록 위에 작성·상세 모달을 여는 EG-BIM Q&A UI입니다. `SW문의_소스패키지`의 히어로·제품 탭·목록·모달 디자인을 적용하고, 기존 첨부·댓글·비밀글·작성자 권한·검색을 유지합니다. 회사 홈페이지의 헤더·메뉴·푸터는 포함하지 않습니다. 기존 `write.html`, `detail.html?id=...` 주소는 같은 목록의 모달로 연결됩니다. 상단 오른쪽 사람 아이콘의 계정 메뉴에서 로그인 사용자 이름을 확인하고 로그아웃할 수 있습니다.
|
||||
|
||||
EG-BIM 탭만 가운데 표시하며 TOVA·GAIA는 오픈 전까지 숨깁니다. 원격지원은 `https://939.co.kr/saman/`으로 열립니다. UI 적용 범위·검증·남은 배포 확인은 [작업 목록](docs/qa-inquiry-ui-redesign-tasks.md)에 정리했습니다.
|
||||
|
||||
@@ -78,6 +78,8 @@ npx wrangler secret bulk qa-secrets.json --name baron-qa-gateway-test
|
||||
|
||||
Worker는 OAuth Authorization Code + PKCE를 사용하고, callback에서 검증한 사용자 claim을 서명된 HttpOnly 세션 쿠키에 저장합니다. 브라우저의 `GET /auth/session`은 정규화된 작성자 정보만 반환합니다.
|
||||
|
||||
로그아웃 메뉴는 `/auth/logout`으로 이동해 해당 환경의 Q&A 세션 쿠키를 삭제한 뒤 `https://test.baroncs.co.kr/ko/sw-inquiry.html`로 돌아갑니다. 스테이징·운영의 `AUTH_POST_LOGOUT_REDIRECT_URI`에 같은 목적지를 설정합니다. 목적지는 서버 배포 설정에서만 읽으며 요청 쿼리로 변경할 수 없습니다. 중앙 SSO의 로그인 종료 요청은 별도로 보내지 않습니다.
|
||||
|
||||
## Gitea Actions 등록값
|
||||
|
||||
운영 `qa.baroncs.co.kr` 배포는 [production 파이프라인 안내](docs/qa-production-pipeline.md)를 따른다. 운영 workflow는 `Deploy EG-BIM QA Production`이며 `main`에서 수동으로 실행한다. 아래 기존 등록값은 스테이징 배포용이다.
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
.inquiry-hero__copy { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; text-align: center; padding: 40px 24px; }
|
||||
.inquiry-hero h1 { font-size: 70px; font-weight: 700; line-height: 1.4; letter-spacing: -0.04em; }
|
||||
.inquiry-hero p { font-size: 22px; line-height: 1.6; }
|
||||
.inquiry-account { position: absolute; top: 24px; right: 32px; z-index: 3; color: #fff; }
|
||||
.inquiry-account summary { display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border: 1px solid #ffffff99; border-radius: 50%; background: #0b274366; cursor: pointer; list-style: none; }
|
||||
.inquiry-account summary::-webkit-details-marker { display: none; }
|
||||
.inquiry-account summary:hover, .inquiry-account[open] summary { background: #0b2743b3; }
|
||||
.inquiry-account summary:focus-visible { outline-color: #fff; }
|
||||
.inquiry-account svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.inquiry-account__panel { position: absolute; top: 54px; right: 0; width: 192px; padding: 8px; border: 1px solid #e5e9ef; border-radius: 10px; background: #fff; color: #26374f; box-shadow: 0 8px 28px #0003; }
|
||||
.inquiry-hero .inquiry-account__panel p { overflow: hidden; padding: 10px 12px; color: #697386; font-size: 13px; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.inquiry-account__panel a { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 10px 12px; border-radius: 6px; font-size: 14px; }
|
||||
.inquiry-account__panel a:hover { background: #edf3fd; color: #164b91; }
|
||||
.inquiry-account__panel a svg { width: 18px; height: 18px; }
|
||||
.inquiry-workspace { position: relative; z-index: 1; width: min(1248px, 100%); padding: 0 24px; margin: -98px auto 120px; }
|
||||
.inquiry-tabs-shell { --inquiry-tab-curve: 28px; padding: 19px 32px 0; border-radius: 48px 48px 0 0; background: linear-gradient(180deg, #174b64a6, #082f43d9); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); box-shadow: inset 0 1px 0 #74c7f4b3, inset 0 0 0 1px #5fadd666, inset 0 8px 16px #ffffff12, 0 -18px 12px #0a2f4843; }
|
||||
.inquiry-tabs { display: flex; justify-content: center; gap: 60px; position: relative; isolation: isolate; }
|
||||
@@ -116,6 +127,7 @@ html.inquiry-modal-open body { overflow: hidden !important; }
|
||||
.inquiry-hero { --inquiry-hero-shade: rgba(5, 24, 44, 0.38); height: 340px; background-position: 22% center; }
|
||||
.inquiry-hero h1 { font-size: 48px; }
|
||||
.inquiry-hero p { font-size: 17px; }
|
||||
.inquiry-account { top: 16px; right: 16px; }
|
||||
.inquiry-workspace { margin: -96px auto 72px; padding: 0 16px; }
|
||||
.inquiry-tabs-shell { --inquiry-tab-curve: 13px; padding: 12px 13px 0; border-radius: 30px 30px 0 0; }
|
||||
.inquiry-tabs { gap: 5px; }
|
||||
|
||||
@@ -228,7 +228,7 @@ flowchart LR
|
||||
|
||||
## 10. 구현·검증 결과
|
||||
|
||||
변경은 정적 HTML/CSS/JS와 자산 업로드 MIME·검증·문서에 한정했다. Worker·API 계약·DB·SSO 설정·기존 배포 트리거는 변경하지 않았다. 기존 주소는 프론트의 `assets/inquiry/entry.js`로 연결하며, 새 화면은 기존 API를 그대로 호출한다. 원본 패키지 전체를 서비스 자산으로 복사하지 않았다.
|
||||
초기 목록·모달 UI 변경은 정적 HTML/CSS/JS와 자산 업로드 MIME·검증·문서에 한정했다. 해당 작업에서 Worker·API 계약·DB·SSO 설정·기존 배포 트리거는 변경하지 않았다. 기존 주소는 프론트의 `assets/inquiry/entry.js`로 연결하며, 새 화면은 기존 API를 그대로 호출한다. 원본 패키지 전체를 서비스 자산으로 복사하지 않았다.
|
||||
|
||||
- `npm run check`, `npm test`: 기존 94개와 구형 주소 연결 4개, 총 98개 테스트 통과.
|
||||
- R2 업로드 검사: 새 하위 경로의 진입 JS와 WebP 이미지가 기존 배포 경로에 포함되며 MIME은 각각 `application/javascript`, `image/webp`다.
|
||||
@@ -254,4 +254,13 @@ CHROMIUM_EXECUTABLE_PATH=/absolute/path/to/chromium \
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
기본 스크린샷 출력 위치는 임시 디렉토리의 `qa-inquiry-ui/`다. `UI_SCREENSHOT_DIR`로 바꿀 수 있으며, 데스크톱·모바일 목록/작성/상세 6장을 생성한다. 실제 사용자 세션·API Key는 필요하지 않다.
|
||||
기본 스크린샷 출력 위치는 임시 디렉토리의 `qa-inquiry-ui/`다. `UI_SCREENSHOT_DIR`로 바꿀 수 있으며, 데스크톱·모바일 목록/작성/상세 6장과 모바일 계정 메뉴 화면을 생성한다. 실제 사용자 세션·API Key는 필요하지 않다.
|
||||
|
||||
|
||||
## 11. 후속 계정 메뉴
|
||||
|
||||
- 상단 오른쪽 사람 아이콘으로 로그인 사용자 이름·로그아웃 메뉴를 연다. 바깥 클릭·ESC로 닫는다.
|
||||
- 로그아웃은 기존 `/auth/logout`에서 Q&A 세션 쿠키를 삭제한 뒤 `https://test.baroncs.co.kr/ko/sw-inquiry.html`로 이동한다.
|
||||
- 스테이징·운영의 `AUTH_POST_LOGOUT_REDIRECT_URI`를 위 주소로 설정했다. 서버 설정의 HTTPS 목적지로 이동할 수 있게 기존 로그아웃 처리를 조정했으며 로그인 `return_url` 정책은 유지한다.
|
||||
- 중앙 SSO 세션 종료 API는 호출하지 않는다. 추가 Secret·SSO client/callback 변경은 없다.
|
||||
- 검증: 스테이징/운영 세션 쿠키 삭제·고정 목적지·쿼리 덮어쓰기 방지·잘못된 설정 처리 테스트, 브라우저 메뉴/키보드/모바일 표시 및 실제 Worker 핸들러를 통한 쿠키 삭제·목적지 이동 요청을 확인한다. 리다이렉트 목적지로의 실제 외부 접속은 Chromium 네트워크 차단으로 막는다. 기본 테스트 총 106개와 브라우저 회귀 검사를 통과했다.
|
||||
@@ -1,6 +1,6 @@
|
||||
# EG-BIM Q&A 로그인·화면·저장 흐름
|
||||
|
||||
기준일: 2026-11-02. 스테이징 `qa-test.baroncs.co.kr`, 운영 `qa.baroncs.co.kr`의 현재 구조를 설명한다. 이번 UI 개편은 프론트에 한정하며 Worker API·DB·SSO 설정을 변경하지 않는다.
|
||||
기준일: 2026-11-02. 스테이징 `qa-test.baroncs.co.kr`, 운영 `qa.baroncs.co.kr`의 현재 구조를 설명한다. 목록·모달 UI는 기존 API·DB·SSO 로그인 흐름을 유지한다. 후속 계정 메뉴 작업에서는 기존 로그아웃 경로의 이동 목적지를 홈페이지 문의 페이지로 연결했다.
|
||||
|
||||
## 로그인과 화면 진입
|
||||
|
||||
@@ -36,6 +36,10 @@ flowchart TD
|
||||
M -->|아니오| O[모달 오류 표시·입력과 파일 유지]
|
||||
```
|
||||
|
||||
## 계정 메뉴와 로그아웃
|
||||
|
||||
상단 오른쪽 사람 아이콘을 누르면 로그인 사용자 이름과 로그아웃 링크가 나온다. 바깥 클릭이나 ESC로 메뉴를 닫을 수 있다. `/auth/logout`은 현재 환경의 Q&A 세션 쿠키를 만료시키고 `https://test.baroncs.co.kr/ko/sw-inquiry.html`로 302 이동한다. 목적지는 스테이징·운영 Wrangler 설정의 `AUTH_POST_LOGOUT_REDIRECT_URI`에서 읽으며 사용자 쿼리로 덮어쓸 수 없다. 중앙 SSO 세션 종료 API는 호출하지 않는다.
|
||||
|
||||
## 작성·첨부 저장
|
||||
|
||||
브라우저는 기존 UUID v7 생성 방식으로 `feedbackId`를 만들고 아래 필드를 보낸다. 회사·부서·작성자·이메일 칸은 읽기 전용이며, 서버가 기존 세션·조직 조회 결과에서 작성자 정보를 결정한다.
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
### 2. SSO 운영 등록
|
||||
|
||||
- [x] 운영 callback URI를 SSO에 등록했다: `https://qa.baroncs.co.kr/auth/callback`. 사용자 완료 보고 기준이다.
|
||||
- [x] 로그아웃 후 이동 URI를 SSO에 등록했다: `https://qa.baroncs.co.kr/`. 사용자 완료 보고 기준이다.
|
||||
- [x] 초기 SSO 등록의 로그아웃 후 URI는 `https://qa.baroncs.co.kr/`로 등록했다. 사용자 완료 보고 기준이다. 현재 계정 메뉴의 Q&A 로그아웃은 Worker에서 세션을 삭제하고 별도 설정된 홈페이지 문의 주소로 직접 이동한다.
|
||||
- [x] 운영 SSO 설정에 맞는 `PROD_AUTH_CLIENT_ID` 등 관련 변수 등록을 완료했다. 실제 로그인 동작 확인은 운영 배포 후 수행한다.
|
||||
- [ ] 운영 도메인에서 실제 사내 사용자로 로그인·로그아웃할 수 있는지 확인한다.
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
| Worker 이름 | `baron-qa-gateway-prod` |
|
||||
| Worker custom domain | `qa.baroncs.co.kr` |
|
||||
| AUTH redirect URI | `https://qa.baroncs.co.kr/auth/callback` |
|
||||
| AUTH post logout URI | `https://qa.baroncs.co.kr/` |
|
||||
| Q&A 로그아웃 후 이동 URI | `https://test.baroncs.co.kr/ko/sw-inquiry.html` (`AUTH_POST_LOGOUT_REDIRECT_URI`) |
|
||||
| AUTH client ID | 운영 SSO client ID |
|
||||
| ABC API base URL | `https://feedback.hmac.kr` 또는 운영 API 주소 |
|
||||
| ABC project ID / channel ID | 운영 대상 ID |
|
||||
|
||||
@@ -61,7 +61,7 @@ R2 권한은 계정 범위의 **Workers R2 Storage Write/Edit**를 선택한다.
|
||||
| `PROD_ORG_CONTEXT_EXPOSURE` | 선택 | 기본 `identifiers` |
|
||||
| `PROD_ORG_CONTEXT_CACHE_TTL_SECONDS` | 선택 | 기본 `300`, 허용 범위 1~3600 |
|
||||
|
||||
SSO에는 `https://qa.baroncs.co.kr/auth/callback`을 허용하고, 로그아웃 후 이동 URI로 `https://qa.baroncs.co.kr/`을 확인한다. 등록된 SSO client나 ABC project/channel이 운영 대상인지 확정한 후 배포한다.
|
||||
SSO에는 `https://qa.baroncs.co.kr/auth/callback`을 허용한다. Q&A 로그아웃은 Worker가 세션 쿠키를 삭제한 뒤 `AUTH_POST_LOGOUT_REDIRECT_URI`에 설정된 `https://test.baroncs.co.kr/ko/sw-inquiry.html`로 이동한다. 이 이동은 중앙 SSO 로그아웃 API를 사용하지 않는다. 등록된 SSO client나 ABC project/channel이 운영 대상인지 확정한 후 배포한다.
|
||||
|
||||
지금 준비할 Variables는 `PROD_AUTH_CLIENT_ID`와 `PROD_ORG_CONTEXT_*` 6개다. `PROD_AUTH_CLIENT_ID`의 참고값은 기존 스테이징 client `e16898a7-acd2-420a-a224-cdcd89ff66cd`이며, 이 client에 운영 callback이 허용된 경우에만 운영에서도 재사용한다. 별도 운영 client를 만들었다면 그 ID를 등록한다. 기준 tenant UUID와 조직 연동 키는 실제 발급받은 값을 사용한다. 기존 `STAGING_*` 등록은 `PROD_*` 등록으로 간주하지 않는다.
|
||||
|
||||
|
||||
@@ -501,6 +501,18 @@
|
||||
console.error('[QA] SSO session load failed:', error);
|
||||
return getAuthUser();
|
||||
}).then(function (user) { renderAuth(user); return user; });
|
||||
const accountMenu = qs('#accountMenu');
|
||||
if (accountMenu) {
|
||||
document.addEventListener('click', function (event) {
|
||||
if (!accountMenu.contains(event.target)) accountMenu.open = false;
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape' && accountMenu.open) {
|
||||
accountMenu.open = false;
|
||||
qs('#accountButton').focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
const menuButton = qs('.menu-button');
|
||||
const nav = qs('.global-nav');
|
||||
if (menuButton && nav) {
|
||||
|
||||
+11
-2
@@ -1,7 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="EG-BIM 소프트웨어 문의"><title>S/W 문의 | 바론컨설턴트</title><link rel="stylesheet" href="../assets/styles.css?v=20261102-ui-1"><link rel="stylesheet" href="../assets/inquiry/inquiry.css?v=20261102-ui-2"></head><body>
|
||||
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="EG-BIM 소프트웨어 문의"><title>S/W 문의 | 바론컨설턴트</title><link rel="stylesheet" href="../assets/styles.css?v=20261102-ui-1"><link rel="stylesheet" href="../assets/inquiry/inquiry.css?v=20261102-ui-3"></head><body>
|
||||
<main class="inquiry-page">
|
||||
<section class="inquiry-hero" aria-labelledby="inquiry-title">
|
||||
<details class="inquiry-account" id="accountMenu">
|
||||
<summary aria-label="내 계정" id="accountButton" aria-controls="accountPanel">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2a8 8 0 0 1 16 0v2"/></svg>
|
||||
</summary>
|
||||
<div class="inquiry-account__panel" id="accountPanel">
|
||||
<p data-auth-name>로그인 확인 중</p>
|
||||
<a href="/auth/logout" id="logoutLink"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 4H4v16h6M14 8l4 4-4 4m-5-4h12"/></svg>로그아웃</a>
|
||||
</div>
|
||||
</details>
|
||||
<div class="inquiry-hero__copy">
|
||||
<h1 id="inquiry-title">S/W 문의</h1>
|
||||
<p>소프트웨어 관련 문의하기</p>
|
||||
@@ -204,5 +213,5 @@
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div><script src="config.js?v=20260921-3"></script><script src="app.js?v=20261102-ui-1"></script>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div><script src="config.js?v=20260921-3"></script><script src="app.js?v=20261102-ui-3"></script>
|
||||
</body></html>
|
||||
+7
-2
@@ -766,6 +766,11 @@ function isUuidv7(value) {
|
||||
|
||||
function logout(request, env) {
|
||||
const url = new URL(request.url);
|
||||
const target = safeReturnUrl(env.AUTH_POST_LOGOUT_REDIRECT_URI || '/', url.origin);
|
||||
return redirect(new URL(target, url.origin).toString(), { 'set-cookie': cookie(env.SESSION_COOKIE_NAME, '', 0, { delete: true }) });
|
||||
let target = new URL('/', url.origin);
|
||||
try {
|
||||
// External destinations come only from trusted deployment config, never request parameters.
|
||||
const configured = new URL(env.AUTH_POST_LOGOUT_REDIRECT_URI || '/', url.origin);
|
||||
if ((configured.protocol === 'https:' || configured.origin === url.origin) && !configured.username && !configured.password) target = configured;
|
||||
} catch (error) {}
|
||||
return redirect(target.toString(), { 'set-cookie': cookie(env.SESSION_COOKIE_NAME, '', 0, { delete: true }) });
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {createRequire} from 'node:module';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {join} from 'node:path';
|
||||
import {tmpdir} from 'node:os';
|
||||
import worker from '../../src/index.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
const {chromium} = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url)).replace(/\/$/, '');
|
||||
@@ -17,7 +18,12 @@ await mkdir(images,{recursive:true});
|
||||
const errors = [];
|
||||
try {
|
||||
const context = await browser.newContext({viewport:{width:1900,height:1000}});
|
||||
await context.addCookies([{name:'fixture_session',value:'existing-session',url:'https://qa.example.test',httpOnly:true,secure:true}]);
|
||||
const page = await context.newPage();page.setDefaultTimeout(8000);page.setDefaultNavigationTimeout(8000);
|
||||
// Redirect continuations bypass Playwright route handlers; block the real destinations in Chromium.
|
||||
const network = await context.newCDPSession(page);
|
||||
await network.send('Network.enable');
|
||||
await network.send('Network.setBlockedURLs',{urls:['https://test.baroncs.co.kr/*','https://sso.hmac.kr/*']});
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
const records = Array.from({length:15},(_,i)=>({id:id(i+1),requester_id:OWNER,requester_name:'송대일',requester_affiliation:'총괄기획&기술개발센터',requester_department:'IS3',title:['PDF 출력 시 이미지 표시 관련 문의','EG-BIM 로그인 오류 문의','모델링 기능 개선 요청'][i%3] + (i>2?' '+(i+1):''),contents:'EG-BIM 이용 중 궁금한 사항을 문의합니다.\n\n이 영역에 문의 내용과 첨부파일, 답변이 표시됩니다.',Category:['ERROR_QNA','GENERAL_QNA','IMPROVEMENT_QNA'][i%3],created_at:'2026-11-02T01:14:00Z',feedback_status:['NEW','RECEIVED','IN_PROGRESS','PENDING','COMPLETED'][i%5],is_secret:i===0,attachments:i===0?[{url:'/fixture/manual.pdf',original_file_name:'화면 설명.pdf'}]:[]}));
|
||||
const comments = new Map(records.map(r=>[r.id,[{id:id(101),author_id:OWNER,author_name:'송대일',content:'확인 부탁드립니다.',created_at:'2026-11-02T02:00:00Z'},{id:id(102),author_id:'other',author_name:'고객지원',content:'접수되었습니다.',created_at:'2026-11-02T02:15:00Z'}]]));
|
||||
@@ -26,6 +32,10 @@ try {
|
||||
await page.route('**/*',async route=>{
|
||||
const request = route.request(); const url = new URL(request.url());
|
||||
if(url.origin!=='https://qa.example.test') return route.abort();
|
||||
if(url.pathname==='/auth/logout') {
|
||||
const response=await worker.fetch(new Request(url),{SESSION_COOKIE_NAME:'fixture_session',AUTH_POST_LOGOUT_REDIRECT_URI:'https://test.baroncs.co.kr/ko/sw-inquiry.html'});
|
||||
return route.fulfill({status:response.status,headers:Object.fromEntries(response.headers),body:''});
|
||||
}
|
||||
const send = (body,status=200)=>route.fulfill({status,contentType:'application/json',body:JSON.stringify(body)});
|
||||
if(url.pathname==='/auth/session') return send({authenticated:true,user:{requesterId:OWNER,userUuid:OWNER,tenantId:id(555),name:'송대일',email:'fixture@example.test',company:'총괄기획&기술개발센터',department:'IS3'}});
|
||||
if(url.pathname.startsWith('/api/')) {
|
||||
@@ -65,6 +75,14 @@ try {
|
||||
await page.waitForFunction(()=>document.querySelector('#resultCount').textContent==='총 15건');
|
||||
assert.equal(await page.locator('#tab-tova').isVisible(),false);assert.equal(await page.locator('#tab-gaia').isVisible(),false);
|
||||
assert.equal(await page.locator('#tab-egbim').evaluate(e=>Math.abs(e.getBoundingClientRect().x+e.getBoundingClientRect().width/2-innerWidth/2)<1),true);assert.equal(await page.locator('.inquiry-remote').getAttribute('href'),'https://939.co.kr/saman/');
|
||||
await page.locator('#accountButton').click();
|
||||
assert.equal(await page.locator('#logoutLink').isVisible(),true);
|
||||
assert.equal(await page.locator('#accountPanel [data-auth-name]').textContent(),'송대일');
|
||||
await page.keyboard.press('Escape');
|
||||
assert.equal(await page.locator('#logoutLink').isVisible(),false);
|
||||
assert.equal(await page.locator('#accountButton').evaluate(e=>e===document.activeElement),true);
|
||||
await page.locator('#accountButton').click();await page.locator('#inquiry-title').click();
|
||||
assert.equal(await page.locator('#logoutLink').isVisible(),false);
|
||||
await page.screenshot({path:images+'/desktop-list.png',fullPage:true});
|
||||
await page.locator('#query').fill('로그임');await page.locator('#searchForm').evaluate(f=>f.requestSubmit());assert.equal(await page.locator('#qaRows tr').count(),5);
|
||||
await page.locator('#qaRows tr a').first().click();await page.waitForSelector('#detailModal[open]');await close();assert.equal(await page.locator('#query').inputValue(),'로그임');
|
||||
@@ -117,6 +135,13 @@ try {
|
||||
assert.equal(await page.locator('#editButton').isVisible(),false);
|
||||
assert.equal(await page.locator('#commentForm').isVisible(),false);
|
||||
await close();
|
||||
await page.locator('#accountButton').click();
|
||||
assert.equal(await page.locator('#accountPanel').evaluate(e=>e.getBoundingClientRect().right<=innerWidth&&e.getBoundingClientRect().left>=0),true);
|
||||
await page.screenshot({path:images+'/mobile-account.png'});
|
||||
const logoutDestination = page.waitForRequest('https://test.baroncs.co.kr/ko/sw-inquiry.html');
|
||||
await page.locator('#logoutLink').click({noWaitAfter:true});
|
||||
await logoutDestination;
|
||||
assert.equal((await context.cookies('https://qa.example.test')).some(c=>c.name==='fixture_session'),false);
|
||||
assert.deepEqual(errors,[]);console.log(JSON.stringify({result:'PASS',mutations:mutations.length,screenshots:images}));await context.close();
|
||||
for (const scenario of ['owner', 'other', 'same-name-no-id', 'anonymous-with-stale-user', 'session-unavailable', 'delayed-session']) {
|
||||
const context = await browser.newContext();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import worker from '../src/index.js';
|
||||
|
||||
const destination = 'https://test.baroncs.co.kr/ko/sw-inquiry.html';
|
||||
|
||||
for (const configFile of ['wrangler.toml', 'wrangler.production.toml']) {
|
||||
test(configFile + ' logout expires its own session and redirects to the inquiry homepage', async () => {
|
||||
const config = readFileSync(new URL('../' + configFile, import.meta.url), 'utf8');
|
||||
const setting = name => config.match(new RegExp('^' + name + ' = "([^"]+)"', 'm'))[1];
|
||||
const env = {
|
||||
SESSION_COOKIE_NAME: setting('SESSION_COOKIE_NAME'),
|
||||
AUTH_POST_LOGOUT_REDIRECT_URI: setting('AUTH_POST_LOGOUT_REDIRECT_URI')
|
||||
};
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/auth/logout?return_url=https://untrusted.example/&redirect_uri=https://untrusted.example/', {
|
||||
headers: { cookie: env.SESSION_COOKIE_NAME + '=existing-session' }
|
||||
}), env);
|
||||
assert.equal(response.status, 302);
|
||||
assert.equal(response.headers.get('location'), destination);
|
||||
const cookie = response.headers.get('set-cookie');
|
||||
assert.ok(cookie.startsWith(env.SESSION_COOKIE_NAME + '=;'));
|
||||
for (const attribute of ['Max-Age=0', 'Path=/', 'Secure', 'HttpOnly', 'SameSite=Lax', 'Expires=Thu, 01 Jan 1970 00:00:00 GMT']) assert.ok(cookie.includes(attribute));
|
||||
assert.match(response.headers.get('cache-control'), /no-store/);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [label, value, expected] of [
|
||||
['no configured destination', undefined, '/'],
|
||||
['same-origin relative destination', '/signed-out', '/signed-out'],
|
||||
['invalid URL', 'https://[invalid', '/'],
|
||||
['script URL', 'javascript:alert(1)', '/'],
|
||||
['external insecure URL', 'http://untrusted.example/', '/'],
|
||||
['URL containing credentials', 'https://user:pass@untrusted.example/', '/']
|
||||
]) {
|
||||
test('logout handles ' + label + ' while clearing the session', async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/auth/logout'), {
|
||||
SESSION_COOKIE_NAME: 'fixture_session', AUTH_POST_LOGOUT_REDIRECT_URI: value
|
||||
});
|
||||
assert.equal(response.status, 302);
|
||||
assert.equal(response.headers.get('location'), 'https://qa.example.test' + expected);
|
||||
assert.match(response.headers.get('set-cookie'), /fixture_session=; Max-Age=0/);
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,7 @@ preview_urls = false
|
||||
|
||||
[vars]
|
||||
AUTH_REDIRECT_URI = "https://qa.baroncs.co.kr/auth/callback"
|
||||
AUTH_POST_LOGOUT_REDIRECT_URI = "https://qa.baroncs.co.kr/"
|
||||
AUTH_POST_LOGOUT_REDIRECT_URI = "https://test.baroncs.co.kr/ko/sw-inquiry.html"
|
||||
AUTH_SCOPE = "openid tenants profile email"
|
||||
AUTH_AUTHORIZE_URL = "https://sso.hmac.kr/oidc/oauth2/auth"
|
||||
AUTH_TOKEN_URL = "https://sso.hmac.kr/oidc/oauth2/token"
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ routes = [
|
||||
|
||||
[vars]
|
||||
AUTH_REDIRECT_URI = "https://qa-test.baroncs.co.kr/auth/callback"
|
||||
AUTH_POST_LOGOUT_REDIRECT_URI = "https://qa-test.baroncs.co.kr/"
|
||||
AUTH_POST_LOGOUT_REDIRECT_URI = "https://test.baroncs.co.kr/ko/sw-inquiry.html"
|
||||
AUTH_SCOPE = "openid tenants profile email"
|
||||
AUTH_CLIENT_ID = "e16898a7-acd2-420a-a224-cdcd89ff66cd"
|
||||
AUTH_AUTHORIZE_URL = "https://sso.hmac.kr/oidc/oauth2/auth"
|
||||
|
||||
Reference in new issue
Block a user