링크 공유 저장(links.json) 지원
- Flask에 /api/links 추가: GET은 links.json 로드, PUT은 관리자만 links.json 저장 - 프론트는 /api/links 사용 가능 시 serverMode로 전환하여 추가/편집/삭제/즐겨찾기/가져오기를 서버에 저장 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
73
flask_app.py
73
flask_app.py
@@ -169,6 +169,35 @@ def bearer_token() -> str:
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
|
||||
def verify_admin_from_request() -> Tuple[bool, str]:
|
||||
"""
|
||||
Returns (is_admin, email_lowercase).
|
||||
Uses the same headers as /api/auth/sync:
|
||||
- Authorization: Bearer <id_token>
|
||||
- X-Auth0-Issuer
|
||||
- X-Auth0-ClientId
|
||||
"""
|
||||
id_token = bearer_token()
|
||||
if not id_token:
|
||||
return (False, "")
|
||||
|
||||
issuer = str(request.headers.get("X-Auth0-Issuer", "")).strip()
|
||||
audience = str(request.headers.get("X-Auth0-ClientId", "")).strip()
|
||||
if not issuer or not audience:
|
||||
return (False, "")
|
||||
|
||||
payload = verify_id_token(id_token, issuer=issuer, audience=audience)
|
||||
email = (str(payload.get("email")).strip().lower() if payload.get("email") else "")
|
||||
return (bool(email and is_admin_email(email)), email)
|
||||
|
||||
|
||||
def safe_write_json(path: Path, data: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JwksCacheEntry:
|
||||
jwks_url: str
|
||||
@@ -232,6 +261,7 @@ def verify_id_token(id_token: str, issuer: str, audience: str) -> dict:
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
LINKS_FILE = ROOT_DIR / "links.json"
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@@ -467,6 +497,49 @@ def api_config_auth_get() -> Response:
|
||||
return jsonify({"ok": False, "error": "server_error"}), 500
|
||||
|
||||
|
||||
@app.get("/api/links")
|
||||
def api_links_get() -> Response:
|
||||
"""
|
||||
Shared links source for all browsers.
|
||||
Reads from links.json on disk (same directory).
|
||||
"""
|
||||
try:
|
||||
if not LINKS_FILE.exists():
|
||||
return jsonify({"ok": True, "links": []})
|
||||
raw = LINKS_FILE.read_text(encoding="utf-8")
|
||||
data = json.loads(raw) if raw.strip() else []
|
||||
links = data if isinstance(data, list) else data.get("links") if isinstance(data, dict) else []
|
||||
if not isinstance(links, list):
|
||||
links = []
|
||||
return jsonify({"ok": True, "links": links})
|
||||
except Exception:
|
||||
return jsonify({"ok": False, "error": "server_error"}), 500
|
||||
|
||||
|
||||
@app.put("/api/links")
|
||||
def api_links_put() -> Response:
|
||||
"""
|
||||
Admin-only: overwrite shared links.json with provided array.
|
||||
Body can be:
|
||||
- JSON array
|
||||
- {"links":[...]}
|
||||
"""
|
||||
try:
|
||||
ok_admin, _email = verify_admin_from_request()
|
||||
if not ok_admin:
|
||||
return jsonify({"ok": False, "error": "forbidden"}), 403
|
||||
|
||||
body = request.get_json(silent=True)
|
||||
links = body if isinstance(body, list) else body.get("links") if isinstance(body, dict) else None
|
||||
if not isinstance(links, list):
|
||||
return jsonify({"ok": False, "error": "invalid_body"}), 400
|
||||
|
||||
safe_write_json(LINKS_FILE, links)
|
||||
return jsonify({"ok": True, "count": len(links)})
|
||||
except Exception:
|
||||
return jsonify({"ok": False, "error": "server_error"}), 500
|
||||
|
||||
|
||||
@app.post("/api/config/auth")
|
||||
def api_config_auth_post() -> Response:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user