AI DEV portal 초기 커밋

Express+EJS+Postgres 사내 개발 포털.
- Keycloak OIDC(사번 로그인), GitHub 감성 UI
- 사이트 모음 + 프로젝트 공유/코멘트/스타
- DB: OpenEverest appdb portal schema (portal_app role)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
0310700
2026-06-22 16:16:40 +09:00
commit 9eb3fd1b4c
18 changed files with 1919 additions and 0 deletions

53
src/auth.js Normal file
View File

@@ -0,0 +1,53 @@
// Keycloak OIDC (Authorization Code). openid-client v5.
import { Issuer, generators } from "openid-client";
import { config } from "./config.js";
let client;
export async function initOidc() {
const issuer = await Issuer.discover(config.oidc.issuer);
client = new issuer.Client({
client_id: config.oidc.clientId,
client_secret: config.oidc.clientSecret,
redirect_uris: [`${config.baseUrl}/auth/callback`],
response_types: ["code"],
});
return client;
}
// 로그인 시작 — state/nonce 를 세션에 저장하고 Keycloak 으로 리다이렉트
export function loginRedirect(req, res) {
const state = generators.state();
const nonce = generators.nonce();
req.session.oidc = { state, nonce };
const url = client.authorizationUrl({
scope: "openid profile email",
state,
nonce,
});
res.redirect(url);
}
// 콜백 처리 — 토큰 교환 후 사용자 클레임 반환
export async function handleCallback(req) {
const params = client.callbackParams(req);
const { state, nonce } = req.session.oidc || {};
const tokenSet = await client.callback(`${config.baseUrl}/auth/callback`, params, {
state,
nonce,
});
const claims = tokenSet.claims();
return {
sabun: claims.preferred_username, // 사번
name: claims.name || claims.given_name || claims.preferred_username,
email: claims.email,
};
}
// 로그인 필수 가드
export function requireAuth(req, res, next) {
if (req.session.user) return next();
// API 요청은 401, 페이지 요청은 로그인으로
if (req.path.startsWith("/api/")) return res.status(401).json({ error: "unauthorized" });
return res.redirect("/login");
}