관리자 페이지 IP 허용 목록 적용

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-17 10:54:19 +09:00
parent 290cebd2cc
commit 033c73f67c
3 changed files with 37 additions and 0 deletions
+1
View File
@@ -32,6 +32,7 @@ The server cron runs this once per minute.
- DB: `data/db.json` - DB: `data/db.json`
- Deploy log: `data/deploy.log` - Deploy log: `data/deploy.log`
- Uploads: `/volume2/AKA Drive/사진 받기` - Uploads: `/volume2/AKA Drive/사진 받기`
- Admin allowlist: `ADMIN_ALLOWED_IPS` comma-separated IPs, default `112.159.64.27`
- Upload page template: `src/public/upload.html` - Upload page template: `src/public/upload.html`
- Upload page style: `src/public/upload.css` - Upload page style: `src/public/upload.css`
- Upload page illustration: `src/public/wedding-illustration.png` - Upload page illustration: `src/public/wedding-illustration.png`
+1
View File
@@ -11,6 +11,7 @@ UPLOAD_DIR="/volume2/AKA Drive/사진 받기"
-p 18082:8080 \ -p 18082:8080 \
--env-file "$APP_DIR/.env" \ --env-file "$APP_DIR/.env" \
-e PUBLIC_BASE_URL="https://photoreq.mokaya.org" \ -e PUBLIC_BASE_URL="https://photoreq.mokaya.org" \
-e ADMIN_ALLOWED_IPS="${ADMIN_ALLOWED_IPS:-112.159.64.27}" \
-e MAX_FIELDS_SIZE_MB="2" \ -e MAX_FIELDS_SIZE_MB="2" \
-v "$APP_DIR/data:/data" \ -v "$APP_DIR/data:/data" \
-v "$UPLOAD_DIR:/uploads" \ -v "$UPLOAD_DIR:/uploads" \
+35
View File
@@ -15,6 +15,7 @@ const publicDir = path.resolve("src", "public");
const adminPassword = process.env.ADMIN_PASSWORD; const adminPassword = process.env.ADMIN_PASSWORD;
const sessionSecret = process.env.SESSION_SECRET; const sessionSecret = process.env.SESSION_SECRET;
const publicBaseUrl = (process.env.PUBLIC_BASE_URL || "").replace(/\/$/, ""); const publicBaseUrl = (process.env.PUBLIC_BASE_URL || "").replace(/\/$/, "");
const adminAllowedIps = parseIpList(process.env.ADMIN_ALLOWED_IPS || "112.159.64.27");
if (!adminPassword || !sessionSecret) { if (!adminPassword || !sessionSecret) {
throw new Error("ADMIN_PASSWORD and SESSION_SECRET are required."); throw new Error("ADMIN_PASSWORD and SESSION_SECRET are required.");
@@ -175,6 +176,38 @@ function wantsJson(req) {
return String(req.get("accept") || "").includes("application/json"); return String(req.get("accept") || "").includes("application/json");
} }
function parseIpList(value) {
return String(value || "")
.split(/[\s,;]+/)
.map(normalizeIp)
.filter(Boolean);
}
function normalizeIp(value) {
let ip = String(value || "").trim();
if (!ip) return "";
if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
if (ip.startsWith("::ffff:")) ip = ip.slice("::ffff:".length);
return ip;
}
function clientIp(req) {
const forwarded = String(req.get("x-forwarded-for") || "")
.split(",")
.map(normalizeIp)
.filter(Boolean);
const proxyAppendedIp = forwarded.at(-1) || "";
return normalizeIp(proxyAppendedIp || req.get("x-real-ip") || req.ip || req.socket.remoteAddress || "");
}
function requireAdminIp(req, res, next) {
if (adminAllowedIps.includes(clientIp(req))) {
next();
return;
}
res.status(404).send(page("페이지 없음", `<section class="panel"><h1>페이지를 찾을 수 없어</h1></section>`));
}
function uploadResultPage(request, { heading, message, isError = false }) { function uploadResultPage(request, { heading, message, isError = false }) {
const slug = escapeHtml(request.slug); const slug = escapeHtml(request.slug);
return `<!doctype html> return `<!doctype html>
@@ -320,6 +353,8 @@ app.get("/", (req, res) => {
res.status(404).send(page("페이지 없음", `<section class="panel"><h1>페이지를 찾을 수 없어</h1></section>`)); res.status(404).send(page("페이지 없음", `<section class="panel"><h1>페이지를 찾을 수 없어</h1></section>`));
}); });
app.use("/admin", requireAdminIp);
app.get("/admin/login", (req, res) => { app.get("/admin/login", (req, res) => {
res.send(page("관리자 로그인", ` res.send(page("관리자 로그인", `
<section class="panel"> <section class="panel">