feat(ops-auth): 이메일 로그인 세션 만료를 로그인일+15일 달력 끝까지로 변경

- OPS_SESSION_TTL_DAYS 환경변수 추가(기본 15)
- .env.example 주석 갱신

Made-with: Cursor
This commit is contained in:
2026-04-03 22:48:54 +09:00
parent 3dd980474f
commit 1baf13abf6
2 changed files with 52 additions and 16 deletions

View File

@@ -18,8 +18,9 @@ ADMIN_TOKEN=xavis-admin
# SMTP_FROM=noreply@xavis.co.kr # SMTP_FROM=noreply@xavis.co.kr
# 선택: 587에서 STARTTLS 강제(기본 on). 특수 서버만 0 # 선택: 587에서 STARTTLS 강제(기본 on). 특수 서버만 0
# SMTP_REQUIRE_TLS=1 # SMTP_REQUIRE_TLS=1
# 이메일 로그인 세션 만료: 해당 일자 23:59:59까지(달력은 OPS_SESSION_TZ 기준, 기본 Asia/Seoul) # 이메일 로그인 세션: 로그인한 달력일(OPS_SESSION_TZ) + OPS_SESSION_TTL_DAYS일의 23:59:59까지(기본 15일)
# OPS_SESSION_TZ=Asia/Seoul # OPS_SESSION_TZ=Asia/Seoul
# OPS_SESSION_TTL_DAYS=15
PAGE_SIZE=9 PAGE_SIZE=9
# 학습센터 동영상 파일 업로드 최대 크기(MB, 기본 500). 리버스 프록시(Nginx 등)의 client_max_body_size도 같이 늘려야 합니다. # 학습센터 동영상 파일 업로드 최대 크기(MB, 기본 500). 리버스 프록시(Nginx 등)의 client_max_body_size도 같이 늘려야 합니다.
LECTURE_VIDEO_MAX_MB=500 LECTURE_VIDEO_MAX_MB=500

View File

@@ -33,30 +33,65 @@ function calendarDateKeyInTz(tsMs, tz) {
return `${y}-${mo}-${da}`; return `${y}-${mo}-${da}`;
} }
/** /** 그레고리력 YYYY-MM-DD 문자열에 일수 더하기(타임존 무관, 달력 날짜만) */
* 로그인 세션 만료: OPS_SESSION_TZ(기본 Asia/Seoul)에서 해당 일자의 마지막 순간(23:59:59.999에 해당하는 epoch ms) function addCalendarDaysToKey(key, days) {
*/ const n = Math.floor(Number(days) || 0);
function getOpsSessionExpiresAtMs(nowMs = Date.now()) { const [y, m, d] = key.split("-").map(Number);
const tz = (process.env.OPS_SESSION_TZ || "Asia/Seoul").trim() || "Asia/Seoul"; const base = new Date(Date.UTC(y, m - 1, d));
const cur = calendarDateKeyInTz(nowMs, tz); base.setUTCDate(base.getUTCDate() + n);
let lo = nowMs; const yy = base.getUTCFullYear();
let hi = nowMs + 48 * 60 * 60 * 1000; const mm = String(base.getUTCMonth() + 1).padStart(2, "0");
let guard = 0; const dd = String(base.getUTCDate()).padStart(2, "0");
while (calendarDateKeyInTz(hi, tz) === cur && guard < 400) { return `${yy}-${mm}-${dd}`;
hi += 24 * 60 * 60 * 1000; }
guard++;
/** dateKey 달력 날짜에 속하는 임의 시각(ms) 찾기 */
function findMsOnCalendarDay(dateKey, tz) {
const [y, m, d] = dateKey.split("-").map(Number);
const start = Date.UTC(y, m - 1, d, 0, 0, 0, 0);
for (let k = -24 * 4; k < 96 * 4; k += 1) {
const ms = start + k * 15 * 60 * 1000;
if (calendarDateKeyInTz(ms, tz) === dateKey) return ms;
} }
if (calendarDateKeyInTz(hi, tz) === cur) { throw new Error(`findMsOnCalendarDay: ${dateKey} (${tz})`);
hi = nowMs + 400 * 24 * 60 * 60 * 1000; }
/**
* OPS_SESSION_TZ에서 dateKey 해당 날의 마지막 순간(그날 23:59:59.999에 해당하는 epoch ms)
*/
function getLastMsOfCalendarDayInTz(dateKey, tz) {
const anchor = findMsOnCalendarDay(dateKey, tz);
let lo = anchor;
let hi = anchor + 48 * 60 * 60 * 1000;
let guard = 0;
while (calendarDateKeyInTz(hi, tz) === dateKey && guard < 400) {
hi += 24 * 60 * 60 * 1000;
guard += 1;
}
if (calendarDateKeyInTz(hi, tz) === dateKey) {
hi = anchor + 400 * 24 * 60 * 60 * 1000;
} }
while (hi - lo > 1) { while (hi - lo > 1) {
const mid = Math.floor((lo + hi) / 2); const mid = Math.floor((lo + hi) / 2);
if (calendarDateKeyInTz(mid, tz) === cur) lo = mid; if (calendarDateKeyInTz(mid, tz) === dateKey) lo = mid;
else hi = mid; else hi = mid;
} }
return hi - 1; return hi - 1;
} }
/**
* 로그인 세션 만료: 로그인일(OPS_SESSION_TZ 달력) + OPS_SESSION_TTL_DAYS(기본 15)일의 마지막 순간
*/
function getOpsSessionExpiresAtMs(nowMs = Date.now()) {
const tz = (process.env.OPS_SESSION_TZ || "Asia/Seoul").trim() || "Asia/Seoul";
const raw = (process.env.OPS_SESSION_TTL_DAYS || "15").trim();
const parsed = parseInt(raw, 10);
const ttlDays = Number.isFinite(parsed) && parsed >= 0 ? parsed : 15;
const loginDayKey = calendarDateKeyInTz(nowMs, tz);
const targetKey = addCalendarDaysToKey(loginDayKey, ttlDays);
return getLastMsOfCalendarDayInTz(targetKey, tz);
}
function isOpsProd() { function isOpsProd() {
return isOpsProdMode(); return isOpsProdMode();
} }