열기 링크에 사용자 식별값 전달
- /go 리다이렉트 엔드포인트 추가: 로그인 시 email, 비로그인 시 IP를 쿼리에 부착 - ncue.net 및 하위 도메인 링크에만 적용(안전한 allowlist) - script.js 및 index.html 폴백에서 열기 버튼을 /go 경유로 변경 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
63
flask_app.py
63
flask_app.py
@@ -7,13 +7,14 @@ import time
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import psycopg2.pool
|
import psycopg2.pool
|
||||||
import requests
|
import requests
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from flask import Flask, Response, jsonify, request, send_from_directory
|
from flask import Flask, Response, jsonify, redirect, request, send_from_directory
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
|
||||||
@@ -248,6 +249,37 @@ ALLOWED_STATIC = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def client_ip() -> str:
|
||||||
|
# Prefer proxy header (Apache ProxyPass sets this)
|
||||||
|
xff = str(request.headers.get("X-Forwarded-For", "")).strip()
|
||||||
|
if xff:
|
||||||
|
return xff.split(",")[0].strip()
|
||||||
|
return str(request.remote_addr or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_http_url(u: str) -> bool:
|
||||||
|
try:
|
||||||
|
p = urlparse(u)
|
||||||
|
return p.scheme in ("http", "https") and bool(p.netloc)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_ncue_host(host: str) -> bool:
|
||||||
|
h = str(host or "").strip().lower()
|
||||||
|
return h == "ncue.net" or h.endswith(".ncue.net")
|
||||||
|
|
||||||
|
|
||||||
|
def add_ref_params(target_url: str, ref_type: str, ref_value: str) -> str:
|
||||||
|
p = urlparse(target_url)
|
||||||
|
q = dict(parse_qsl(p.query, keep_blank_values=True))
|
||||||
|
# Keep names short + explicit
|
||||||
|
q["ref_type"] = ref_type
|
||||||
|
q["ref"] = ref_value
|
||||||
|
new_query = urlencode(q, doseq=True)
|
||||||
|
return urlunparse((p.scheme, p.netloc, p.path, p.params, new_query, p.fragment))
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def home() -> Response:
|
def home() -> Response:
|
||||||
return send_from_directory(ROOT_DIR, "index.html")
|
return send_from_directory(ROOT_DIR, "index.html")
|
||||||
@@ -261,6 +293,35 @@ def static_files(filename: str) -> Response:
|
|||||||
return send_from_directory(ROOT_DIR, filename)
|
return send_from_directory(ROOT_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/go")
|
||||||
|
def go() -> Response:
|
||||||
|
"""
|
||||||
|
Redirect helper to pass identity to internal apps.
|
||||||
|
- Logged-in user: pass email (from query param)
|
||||||
|
- Anonymous user: pass client IP (server-side)
|
||||||
|
|
||||||
|
For safety, only redirects to ncue.net / *.ncue.net targets.
|
||||||
|
"""
|
||||||
|
u = str(request.args.get("u", "")).strip()
|
||||||
|
if not u or not is_http_url(u):
|
||||||
|
return jsonify({"ok": False, "error": "invalid_url"}), 400
|
||||||
|
|
||||||
|
p = urlparse(u)
|
||||||
|
if not is_ncue_host(p.hostname or ""):
|
||||||
|
return jsonify({"ok": False, "error": "host_not_allowed"}), 400
|
||||||
|
|
||||||
|
email = str(request.args.get("e", "") or request.args.get("email", "")).strip().lower()
|
||||||
|
if email:
|
||||||
|
target = add_ref_params(u, "email", email)
|
||||||
|
else:
|
||||||
|
ip = client_ip()
|
||||||
|
target = add_ref_params(u, "ip", ip)
|
||||||
|
|
||||||
|
resp = redirect(target, code=302)
|
||||||
|
resp.headers["Cache-Control"] = "no-store"
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.get("/healthz")
|
@app.get("/healthz")
|
||||||
def healthz() -> Response:
|
def healthz() -> Response:
|
||||||
try:
|
try:
|
||||||
|
|||||||
21
index.html
21
index.html
@@ -516,8 +516,27 @@
|
|||||||
const favTag = link.favorite ? `<span class="tag fav">★ 즐겨찾기</span>` : "";
|
const favTag = link.favorite ? `<span class="tag fav">★ 즐겨찾기</span>` : "";
|
||||||
const lockTag = accessible ? "" : `<span class="tag lock">접근 제한</span>`;
|
const lockTag = accessible ? "" : `<span class="tag lock">접근 제한</span>`;
|
||||||
const letter = esc((link.title || d || "L").trim().slice(0, 1).toUpperCase());
|
const letter = esc((link.title || d || "L").trim().slice(0, 1).toUpperCase());
|
||||||
|
function buildOpenUrl(rawUrl) {
|
||||||
|
const url0 = String(rawUrl || "").trim();
|
||||||
|
if (!url0) return "";
|
||||||
|
let host = "";
|
||||||
|
try {
|
||||||
|
host = new URL(url0).hostname.toLowerCase();
|
||||||
|
} catch {
|
||||||
|
return url0;
|
||||||
|
}
|
||||||
|
const isNcue = host === "ncue.net" || host.endsWith(".ncue.net");
|
||||||
|
if (!isNcue) return url0;
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
qs.set("u", url0);
|
||||||
|
const email = String(sessionEmail || "").trim().toLowerCase();
|
||||||
|
if (email) qs.set("e", email);
|
||||||
|
return `/go?${qs.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openHref = esc(buildOpenUrl(link.url));
|
||||||
const openHtml = accessible
|
const openHtml = accessible
|
||||||
? `<a class="btn mini" href="${u}" target="_blank" rel="noopener noreferrer">열기</a>`
|
? `<a class="btn mini" href="${openHref}" target="_blank" rel="noopener noreferrer">열기</a>`
|
||||||
: `<button class="btn mini" type="button" disabled aria-disabled="true" title="이 링크는 현재 권한으로 접근할 수 없습니다.">열기</button>`;
|
: `<button class="btn mini" type="button" disabled aria-disabled="true" title="이 링크는 현재 권한으로 접근할 수 없습니다.">열기</button>`;
|
||||||
const copyAttrs = accessible ? "" : ` disabled aria-disabled="true" title="이 링크는 현재 권한으로 접근할 수 없습니다."`;
|
const copyAttrs = accessible ? "" : ` disabled aria-disabled="true" title="이 링크는 현재 권한으로 접근할 수 없습니다."`;
|
||||||
return `
|
return `
|
||||||
|
|||||||
22
script.js
22
script.js
@@ -98,6 +98,25 @@
|
|||||||
return ACCESS_ANON_IDS.has(id);
|
return ACCESS_ANON_IDS.has(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOpenUrl(rawUrl) {
|
||||||
|
const url = String(rawUrl || "").trim();
|
||||||
|
if (!url) return "";
|
||||||
|
let host = "";
|
||||||
|
try {
|
||||||
|
host = new URL(url).hostname.toLowerCase();
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
const isNcue = host === "ncue.net" || host.endsWith(".ncue.net");
|
||||||
|
if (!isNcue) return url;
|
||||||
|
|
||||||
|
const email = getUserEmail();
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
qs.set("u", url);
|
||||||
|
if (email) qs.set("e", email);
|
||||||
|
return `/go?${qs.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
const auth = {
|
const auth = {
|
||||||
client: null,
|
client: null,
|
||||||
user: null,
|
user: null,
|
||||||
@@ -308,8 +327,9 @@
|
|||||||
const accessDisabledAttr = accessible ? "" : " disabled aria-disabled=\"true\"";
|
const accessDisabledAttr = accessible ? "" : " disabled aria-disabled=\"true\"";
|
||||||
const accessDisabledTitle = accessible ? "" : " title=\"이 링크는 현재 권한으로 접근할 수 없습니다.\"";
|
const accessDisabledTitle = accessible ? "" : " title=\"이 링크는 현재 권한으로 접근할 수 없습니다.\"";
|
||||||
|
|
||||||
|
const openHref = escapeHtml(buildOpenUrl(link.url));
|
||||||
const openHtml = accessible
|
const openHtml = accessible
|
||||||
? `<a class="btn mini" href="${url}" target="_blank" rel="noopener noreferrer" data-act="open">열기</a>`
|
? `<a class="btn mini" href="${openHref}" target="_blank" rel="noopener noreferrer" data-act="open">열기</a>`
|
||||||
: `<button class="btn mini" type="button"${accessDisabledAttr}${accessDisabledTitle}>열기</button>`;
|
: `<button class="btn mini" type="button"${accessDisabledAttr}${accessDisabledTitle}>열기</button>`;
|
||||||
|
|
||||||
const copyDisabledAttr = accessible ? "" : " disabled aria-disabled=\"true\"";
|
const copyDisabledAttr = accessible ? "" : " disabled aria-disabled=\"true\"";
|
||||||
|
|||||||
Reference in New Issue
Block a user