Compare commits
2 Commits
2b74b59f81
...
fdfd7dca98
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdfd7dca98 | ||
|
|
096f7fbbcb |
13
src/auth.js
13
src/auth.js
@@ -56,9 +56,22 @@ export async function handleCallback(req) {
|
||||
email: claims.email,
|
||||
roles,
|
||||
isAdmin: roles.includes(config.adminRole),
|
||||
// SSO 로그아웃(end-session) 시 id_token_hint 로 사용
|
||||
id_token: tokenSet.id_token,
|
||||
};
|
||||
}
|
||||
|
||||
// SSO 로그아웃 — Keycloak end_session_endpoint 로 보내 로컬뿐 아니라
|
||||
// Keycloak SSO 세션까지 종료한다. id_token_hint 가 있으면 확인 화면 없이 즉시 로그아웃.
|
||||
// 종료 후 post_logout_redirect_uri 로 복귀.
|
||||
export function logoutUrl(idToken) {
|
||||
if (!client) return `${config.baseUrl}/login`;
|
||||
return client.endSessionUrl({
|
||||
id_token_hint: idToken,
|
||||
post_logout_redirect_uri: `${config.baseUrl}/login`,
|
||||
});
|
||||
}
|
||||
|
||||
// 로그인 필수 가드
|
||||
export function requireAuth(req, res, next) {
|
||||
if (req.session.user) return next();
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// GitHub Octicon 16x16 패스 모음.
|
||||
const OCTICONS = {
|
||||
code: '<path d="m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z"/>',
|
||||
// </> (꺾쇠 + 슬래시) — 박스 없이 헤더 색 상속. Bootstrap code-slash 패스.
|
||||
codeslash: '<path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294l4-13zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0zm6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0z"/>',
|
||||
search: '<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/>',
|
||||
sun: '<path d="M8 12a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm0-1.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm5.657-8.157a.75.75 0 0 1 0 1.061l-1.061 1.06a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.06-1.061a.75.75 0 0 1 1.061 0Zm-9.193 9.193a.75.75 0 0 1 0 1.06l-1.06 1.061a.751.751 0 0 1-1.061-1.061l1.06-1.06a.75.75 0 0 1 1.061 0ZM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0ZM3 8a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 3 8Zm13 0a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 16 8Zm-8 5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13Zm3.536-1.464a.75.75 0 0 1 1.06 0l1.061 1.06a.751.751 0 0 1-1.061 1.061l-1.06-1.06a.75.75 0 0 1 0-1.061ZM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.751.751 0 0 1-1.06 1.06l-1.061-1.06a.75.75 0 0 1 0-1.06Z"/>',
|
||||
moon: '<path d="M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z"/>',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
import { config } from "./config.js";
|
||||
import { toolsForUser } from "./sites.js";
|
||||
import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady } from "./auth.js";
|
||||
import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady, logoutUrl } from "./auth.js";
|
||||
import { icon, identicon, langColor, langFromTags } from "./helpers.js";
|
||||
import * as db from "./db.js";
|
||||
|
||||
@@ -58,8 +58,11 @@ app.get("/auth/login", (req, res) => loginRedirect(req, res));
|
||||
app.get("/auth/callback", async (req, res) => {
|
||||
try {
|
||||
const u = await handleCallback(req);
|
||||
await db.upsertUser(u);
|
||||
req.session.user = u; // { sabun, name, email, roles, isAdmin }
|
||||
// id_token 은 SSO 로그아웃용으로만 세션에 따로 보관(뷰/DB 에 노출 안 함)
|
||||
const { id_token, ...user } = u;
|
||||
req.session.idToken = id_token;
|
||||
await db.upsertUser(user);
|
||||
req.session.user = user; // { sabun, name, email, roles, isAdmin }
|
||||
res.redirect("/");
|
||||
} catch (e) {
|
||||
console.error("oidc callback error", e);
|
||||
@@ -67,7 +70,13 @@ app.get("/auth/callback", async (req, res) => {
|
||||
}
|
||||
});
|
||||
app.get("/logout", (req, res) => {
|
||||
req.session.destroy(() => res.redirect("/login"));
|
||||
const idToken = req.session.idToken;
|
||||
// 로컬 세션을 먼저 destroy 한 뒤 Keycloak end-session 으로 보내
|
||||
// SSO 세션까지 종료한다. (로컬만 지우면 재접속 시 자동 재로그인됨)
|
||||
req.session.destroy(() => {
|
||||
if (oidcReady()) return res.redirect(logoutUrl(idToken));
|
||||
return res.redirect("/login");
|
||||
});
|
||||
});
|
||||
|
||||
// --- 대시보드 ---
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<header class="hdr">
|
||||
<div class="hdr-inner">
|
||||
<a class="brand" href="/" id="brandLink">
|
||||
<span class="brand-mark"><%- icon("code", { size: 22 }) %></span>
|
||||
<span class="brand-mark"><%- icon("codeslash", { size: 22 }) %></span>
|
||||
<span class="brand-name"><%= brand %></span>
|
||||
<span class="brand-sub">사내 개발 포털</span>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user