- requireAuth: /login 거치지 않고 loginRedirect로 직접 Keycloak으로 - /login 라우트도 oidcReady면 즉시 리다이렉트 - OIDC discovery 실패 시에만 안내 페이지 폴백, /api/*는 401 유지 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
4.0 KiB
JavaScript
122 lines
4.0 KiB
JavaScript
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();
|