feat: xavis ai_platform 기능 이전 및 ncue 환경 전환
xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영. @xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
115
lib/ops-session-revoke.js
Normal file
115
lib/ops-session-revoke.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* OPS 이메일 세션 전체 무효화(모든 디바이스 로그아웃)
|
||||
* 쿠키에 iat(발급 시각)를 넣고, DB sessions_revoked_at 이후 iat만 유효하게 한다.
|
||||
*/
|
||||
|
||||
const REVOKE_FILE = "ops-session-revocations.json";
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeEmail(email) {
|
||||
return String(email || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.slice(0, 320);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('pg').Pool | null | undefined} pgPool
|
||||
* @param {string} dataDir
|
||||
* @param {string} email
|
||||
* @returns {Promise<number>} revoked at epoch ms, 0 if never revoked
|
||||
*/
|
||||
async function getSessionsRevokedAtMs(pgPool, dataDir, email) {
|
||||
const e = normalizeEmail(email);
|
||||
if (!e) return 0;
|
||||
|
||||
if (pgPool) {
|
||||
try {
|
||||
const r = await pgPool.query(
|
||||
`SELECT sessions_revoked_at FROM ops_email_users WHERE email = $1`,
|
||||
[e]
|
||||
);
|
||||
const ts = r.rows?.[0]?.sessions_revoked_at;
|
||||
return ts ? new Date(ts).getTime() : 0;
|
||||
} catch (err) {
|
||||
console.error("[ops-session-revoke] get failed:", err.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fp = path.join(dataDir, REVOKE_FILE);
|
||||
if (!fs.existsSync(fp)) return 0;
|
||||
const raw = fs.readFileSync(fp, "utf8");
|
||||
const map = JSON.parse(raw);
|
||||
const v = map?.[e];
|
||||
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('pg').Pool | null | undefined} pgPool
|
||||
* @param {string} dataDir
|
||||
* @param {string} email
|
||||
* @param {number} iatMs
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isOpsSessionRevoked(pgPool, dataDir, email, iatMs) {
|
||||
const revokedAt = await getSessionsRevokedAtMs(pgPool, dataDir, email);
|
||||
if (!revokedAt) return false;
|
||||
const iat = typeof iatMs === "number" && Number.isFinite(iatMs) ? iatMs : 0;
|
||||
return iat <= revokedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('pg').Pool | null | undefined} pgPool
|
||||
* @param {string} dataDir
|
||||
* @param {string} email
|
||||
* @returns {Promise<{ revokedAtMs: number }>}
|
||||
*/
|
||||
async function revokeAllOpsSessionsForEmail(pgPool, dataDir, email) {
|
||||
const e = normalizeEmail(email);
|
||||
if (!e) {
|
||||
throw new Error("email is required");
|
||||
}
|
||||
const revokedAtMs = Date.now();
|
||||
|
||||
if (pgPool) {
|
||||
await pgPool.query(
|
||||
`INSERT INTO ops_email_users (email, first_seen_at, last_login_at, login_count, sessions_revoked_at)
|
||||
VALUES ($1, NOW(), NOW(), 0, to_timestamp($2 / 1000.0))
|
||||
ON CONFLICT (email) DO UPDATE SET sessions_revoked_at = EXCLUDED.sessions_revoked_at`,
|
||||
[e, revokedAtMs]
|
||||
);
|
||||
return { revokedAtMs };
|
||||
}
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fp = path.join(dataDir, REVOKE_FILE);
|
||||
let map = {};
|
||||
try {
|
||||
if (fs.existsSync(fp)) {
|
||||
map = JSON.parse(fs.readFileSync(fp, "utf8")) || {};
|
||||
}
|
||||
} catch {
|
||||
map = {};
|
||||
}
|
||||
map[e] = revokedAtMs;
|
||||
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
||||
fs.writeFileSync(fp, JSON.stringify(map, null, 2), "utf8");
|
||||
return { revokedAtMs };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSessionsRevokedAtMs,
|
||||
isOpsSessionRevoked,
|
||||
revokeAllOpsSessionsForEmail,
|
||||
};
|
||||
Reference in New Issue
Block a user