Files
ai_platform/views/admin-users.ejs
dsyoon 073a8343dd 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>
2026-05-26 22:27:48 +09:00

133 lines
5.5 KiB
Plaintext

<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%- include('partials/favicon') %>
<title>사용자 현황관리 - XAVIS</title>
<link rel="stylesheet" href="/public/styles.css" />
</head>
<body>
<div class="app-shell">
<%- include('partials/nav', { activeMenu: 'admin-users', adminMode: true }) %>
<div class="content-area">
<header class="topbar">
<h1>사용자 현황관리</h1>
<a class="top-action-link" href="/learning">학습센터</a>
</header>
<main class="container">
<section class="panel">
<p class="subtitle" style="margin-bottom: 16px">
OPS 이메일(<strong>@ncue.net</strong>) 매직 링크로 <strong>로그인에 성공한</strong> 사용자 목록입니다. 이메일과 최근 접속일(마지막 로그인 시각)을 표시합니다.
</p>
<p class="admin-hint" style="margin-bottom: 16px">
<strong>전체 로그아웃</strong>을 실행하면 해당 사용자의 모든 디바이스에서 OPS 세션이 즉시 무효화됩니다. 이후 다시 매직 링크로 로그인해야 합니다.
</p>
<p id="adminUsersFlash" class="form-message" hidden role="status"></p>
<% if (typeof dbError !== 'undefined' && dbError) { %>
<p class="admin-error">목록을 불러오지 못했습니다: <%= dbError %></p>
<% } else if (!pgConnected) { %>
<p class="admin-warn">PostgreSQL이 비활성화되어 있어 사용자 목록을 조회할 수 없습니다.</p>
<% } else if (!users || users.length === 0) { %>
<p class="admin-hint">아직 로그인 기록이 없습니다.</p>
<% } else { %>
<div class="table-wrap">
<table class="data-table" aria-label="인증 사용자 목록">
<thead>
<tr>
<th scope="col">이메일</th>
<th scope="col">최근 접속일</th>
<th scope="col">세션 무효화</th>
<th scope="col">작업</th>
</tr>
</thead>
<tbody>
<% users.forEach(function (u) { %>
<tr data-email="<%= u.email %>">
<td><%= u.email %></td>
<td>
<% if (u.lastLoginAt) { %>
<%= new Date(u.lastLoginAt).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) %>
<% } else { %>
<% } %>
</td>
<td class="js-sessions-revoked-at">
<% if (u.sessionsRevokedAt) { %>
<%= new Date(u.sessionsRevokedAt).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) %>
<% } else { %>
<% } %>
</td>
<td>
<button
type="button"
class="danger btn-sm js-revoke-sessions-btn"
data-email="<%= u.email %>"
>
전체 로그아웃
</button>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<p class="admin-hint" style="margin-top: 12px">총 <%= users.length %>명</p>
<% } %>
</section>
</main>
</div>
</div>
<script>
(function () {
const flash = document.getElementById("adminUsersFlash");
function showFlash(text, isError) {
if (!flash) return;
flash.textContent = text;
flash.hidden = false;
flash.style.color = isError ? "#b91c1c" : "#059669";
}
document.querySelectorAll(".js-revoke-sessions-btn").forEach(function (btn) {
btn.addEventListener("click", async function () {
const email = btn.getAttribute("data-email") || "";
if (!email) return;
const ok = window.confirm(
email + " 사용자의 모든 디바이스에서 OPS 세션을 만료(전체 로그아웃)시키겠습니까?"
);
if (!ok) return;
btn.disabled = true;
try {
const res = await fetch("/api/admin/users/revoke-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email }),
});
const data = await res.json().catch(function () {
return {};
});
if (!res.ok) {
throw new Error(data.error || "요청 실패");
}
const row = btn.closest("tr");
const cell = row && row.querySelector(".js-sessions-revoked-at");
if (cell && data.revokedAt) {
cell.textContent = new Date(data.revokedAt).toLocaleString("ko-KR", {
timeZone: "Asia/Seoul",
});
}
showFlash(email + " 사용자의 세션이 무효화되었습니다.");
} catch (err) {
showFlash(err.message || "세션 만료 처리 실패", true);
} finally {
btn.disabled = false;
}
});
});
})();
</script>
</body>
</html>