diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index 582a50e..33de1e9 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -117,6 +117,7 @@ ### HAL 전용 호스트 부트스트랩(2026-10-27, bear `hal21.ariweb.net`) - **`deploy/bootstrap-hal.sh --portal-host none`**: 포털을 다른 호스트에서 운영하는 HAL 전용 호스트에는 `my.ariweb.net` nginx 접근 경로를 만들지 않는다(이전에는 항상 만들어 포털이 없는 호스트에도 죽은 vhost 가 남았다). 기본값(`my.ariweb.net`)의 동작은 그대로다. - bear(Ubuntu 24.04, nginx 1.24, Node 24.22, certbot nginx 플러그인 운영)에 HAL 만 최초 부트스트랩: 인증서는 호스트별 certbot(webroot `/var/www/html`, ECDSA, 갱신 훅 `systemctl reload nginx`) — `--cert /etc/letsencrypt/live/hal21.ariweb.net --acme /var/www/html`. 기존 Webmaker·Gitea·Matrix·기본 서버는 건드리지 않았다. 절차·검증·되돌리기는 `docs/RUNBOOK-0.1.1.md` §7. 배포 artifact: main `ab75b7d`, hal sha256 `f574d875…`(deploy/ 변경으로 ariweb.net 호스트의 `e7741899…` 와 다름). 알려진 사소한 결함: 부트스트랩 5단계의 HTTPS 자체 확인이 nginx reload 직후 실행되어 거짓 경고가 날 수 있다(결과에 영향 없음). +- **HAL 기본 주소 안내**(`hal/app.mjs`): `https://hal21.ariweb.net/` 접속이 JSON 404를 반환하던 동작을 운영자 콘솔 표준 경로 `/ops/`로 이동하도록 수정했다. `/ops`의 기존 사설망/VPN·기기 접근권 관문과 별도 운영자 로그인은 그대로 적용된다. ### 도입 소스 - 가져오기: `node scripts/import-sources.mjs` 가 고정 커밋(Webmaker `3b10859884ef2fd0f2af11934ab6c16370fd64b8`, AriMail `a07f69f1ffb6667de6e66b3d1ec43c1c66fab924`)을 검사하고 실행에 필요 없는 문서·보고서·샘플을 제외한 전체를 `modules/webmaker`(176 파일)·`modules/arimail`(88 파일)로 복사, 파일별 원본 경로·대상 경로·수정 여부·원본 해시·라이선스 검토를 `IMPORTED_SOURCES.md`/`.json` 에 기록한다. diff --git a/hal/app.mjs b/hal/app.mjs index bf185e1..7465212 100644 --- a/hal/app.mjs +++ b/hal/app.mjs @@ -36,6 +36,8 @@ export function buildApp(d) { const uiDir = join(ROOT, 'hal', 'ui', 'public'); if (existsSync(uiDir)) app.static('/ops/ui', uiDir); // ---------------- 공개 + /** 기본 주소는 운영자가 실제로 사용하는 콘솔 경로로 안내한다. 접근 통제는 이동 후 /ops 미들웨어가 동일하게 적용한다. */ + app.get('/', (ctx) => ctx.redirect('/ops/')); app.get('/api/health', () => core.selfHealth(d)); app.get('/api/status', () => publicStatus(d)); app.get('/status', (ctx) => ctx.html(statusPage(d))); diff --git a/test/hal-ui.test.mjs b/test/hal-ui.test.mjs index 9841ce6..e534226 100644 --- a/test/hal-ui.test.mjs +++ b/test/hal-ui.test.mjs @@ -22,6 +22,12 @@ function makeHal(over = {}) { /** 운영자 계정 + 로그인 세션 */ async function loginOp(h) { ops.createAccount(h.d, { login: 'ozops', password: 'password-example-1', displayName: '오즈' }); const r = await invoke(h.app, 'POST', '/ops/api/login', { body: { login: 'ozops', password: 'password-example-1' } }); ok(r.json.ok, JSON.stringify(r.json)); return { cookie: r.cookies.aw_ops, csrf: r.json.csrf, as: (method, path, body, extra = {}) => invoke(h.app, method, path, { body, cookies: { aw_ops: r.cookies.aw_ops, ...(extra.cookies || {}) }, headers: { 'x-csrf': r.json.csrf, ...(extra.headers || {}) }, ip: extra.ip }) }; } +await test('HAL 루트 주소는 운영자 콘솔 기본 경로로 안내한다', async () => { + const h = makeHal(); + const r = await invoke(h.app, 'GET', '/'); + eq(r.status, 302, '루트 주소는 404가 아니라 운영자 콘솔로 이동'); + eq(r.headers.Location, '/ops/', '운영자 콘솔의 표준 경로로 이동'); +}); await test('화면 접근: 허용 네트워크 밖 403, 세션 없음/고객 쿠키 → 로그인 화면, 운영자 세션 → 콘솔(메뉴 14개 + 계정)', async () => { const h = makeHal(); let r = await invoke(h.app, 'GET', '/ops/', { ip: '203.0.113.9' }); eq(r.status, 403); eq(r.json.error, 'access_denied');