import express from "express"; import session from "express-session"; import cookieParser from "cookie-parser"; import { fileURLToPath } from "url"; import path from "path"; import { config } from "./config.js"; import { siteGroups } from "./sites.js"; import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady } from "./auth.js"; import * as db from "./db.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = express(); app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "..", "views")); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(cookieParser()); app.use("/public", express.static(path.join(__dirname, "..", "public"))); app.use( session({ secret: config.sessionSecret, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: "lax", maxAge: 8 * 3600 * 1000 }, }) ); // 모든 뷰에 공통 노출 app.use((req, res, next) => { res.locals.brand = config.brand; res.locals.user = req.session.user || null; next(); }); app.get("/healthz", (_req, res) => res.json({ ok: true })); // --- 인증 --- // SSO 세션이 없으면 중간 페이지 없이 바로 Keycloak(커스텀 테마) 로그인으로 보낸다. // (OIDC 초기화 실패 등으로 로그인 불가하면 안내용 login.ejs 로 폴백) app.get("/login", (req, res) => { if (req.session.user) return res.redirect("/"); if (req.query.error) return res.render("login", { error: req.query.error }); // OIDC 준비됐으면 중간 화면 없이 곧장 Keycloak 로그인 폼으로 if (oidcReady()) return loginRedirect(req, res); return res.render("login", { error: "로그인 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해 주세요." }); }); 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; res.redirect("/"); } catch (e) { console.error("oidc callback error", e); res.status(500).render("login", { error: "로그인 처리 중 오류가 발생했습니다." }); } }); app.get("/logout", (req, res) => { req.session.destroy(() => res.redirect("/login")); }); // --- 대시보드 (로그인 필요) --- app.get("/", requireAuth, async (req, res) => { const projects = await db.listProjects(); res.render("dashboard", { siteGroups, projects }); }); // --- 프로젝트 --- app.post("/projects", requireAuth, async (req, res) => { const { title, description, repo_url, app_url, tags } = req.body; if (!title || !title.trim()) return res.redirect("/"); await db.addProject({ owner: req.session.user.sabun, title: title.trim(), description, repoUrl: repo_url, appUrl: app_url, tags, }); res.redirect("/"); }); app.get("/projects/:id", requireAuth, async (req, res) => { const project = await db.getProject(req.params.id); if (!project) return res.status(404).send("not found"); const comments = await db.listComments(project.id); const starred = await db.isStarred({ projectId: project.id, sabun: req.session.user.sabun }); res.render("project", { project, comments, starred }); }); app.post("/projects/:id/comments", requireAuth, async (req, res) => { if (req.body.body && req.body.body.trim()) { await db.addComment({ projectId: req.params.id, author: req.session.user.sabun, body: req.body.body.trim(), }); } res.redirect(`/projects/${req.params.id}`); }); app.post("/projects/:id/star", requireAuth, async (req, res) => { await db.toggleStar({ projectId: req.params.id, sabun: req.session.user.sabun }); res.redirect(`/projects/${req.params.id}`); }); // --- 부팅 --- const start = async () => { try { await initOidc(); console.log("OIDC discovery 완료:", config.oidc.issuer); } catch (e) { console.error("OIDC 초기화 실패(로그인 비활성):", e.message); } app.listen(config.port, "0.0.0.0", () => console.log(`${config.brand} portal listening on :${config.port}`) ); }; start();