diff --git a/ops-auth.js b/ops-auth.js index a290bf7..c44acde 100644 --- a/ops-auth.js +++ b/ops-auth.js @@ -107,6 +107,18 @@ function getBaseUrl() { return `http://localhost:${port}`; } +/** + * 매직 링크 절대 URL — BASE_URL + `/auth/verify/:token` 만 조합 (본문 문장·조사와 문자열 결합 금지) + */ +function buildMagicVerifyUrl(baseUrl, token) { + const base = String(baseUrl || "").trim().replace(/\/$/, ""); + const t = String(token || "").trim(); + if (!base) throw new Error("BASE_URL이 비어 있습니다."); + if (!t) throw new Error("인증 토큰이 비어 있습니다."); + const path = `/auth/verify/${encodeURIComponent(t)}`; + return new URL(path, `${base}/`).href; +} + function sanitizeReturnTo(v) { const s = (v || "").toString().trim(); if (!s.startsWith("/") || s.startsWith("//")) return "/learning"; @@ -402,7 +414,7 @@ module.exports = function createOpsAuth(DATA_DIR, hooks = {}) { }); saveMagicLinks(MAGIC_LINK_PATH, list); /** 경로에 토큰만 두기 (쿼리 `?token=`는 일부 웹메일/Outlook에서 href가 잘림) */ - const linkUrl = `${BASE_URL}/auth/verify/${encodeURIComponent(token)}`; + const linkUrl = buildMagicVerifyUrl(BASE_URL, token); await sendMagicLinkEmail(email, linkUrl); if (typeof hooks.onMagicLinkRequested === "function") { try {