diff --git a/Caddyfile b/Caddyfile index 2593d60..ddce513 100644 --- a/Caddyfile +++ b/Caddyfile @@ -17,9 +17,6 @@ hyonglin.top { path /xinquandashboard/* } handle @dq { - basic_auth { - xqzk $2a$14$GrTqs4WiYHepvOq/0CnOU.Zq2.cA4B2FcilgGIv/6Jbi9BZX/8rNK - } uri strip_prefix /xinquandashboard reverse_proxy xinquan:8000 } diff --git a/docker-compose.yml b/docker-compose.yml index 95351af..b29c389 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,10 @@ services: build: ./xinquan container_name: xinquan restart: unless-stopped + environment: + - XQ_USER=xqzk + - XQ_PASS=66668888 + - XQ_SECRET=bb8335b291300ea338373feea74af915e3346aedd52d7ac2fb66721a16b394e8 volumes: - ./xinquan_data:/data expose: diff --git a/xinquan/app.py b/xinquan/app.py index eb5aad3..944d900 100644 --- a/xinquan/app.py +++ b/xinquan/app.py @@ -16,6 +16,8 @@ 鉴权:由 Caddy basic_auth 在网关卡负责,本应用不鉴权。 """ import base64 +import hashlib +import hmac import json import os import re @@ -35,6 +37,33 @@ ALLOWED_IMG_EXT = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml"} +# ---- 应用内表单登录(替代 Caddy basic_auth,兼容微信内置浏览器)---- +AUTH_USER = os.environ.get("XQ_USER", "xqzk") +AUTH_PASS = os.environ.get("XQ_PASS", "66668888") +AUTH_SECRET = os.environ.get("XQ_SECRET", "xinquan-session-secret-change-me") +SESSION_DAYS = 7 + +RE_AUTH = re.compile(r"__AUTH__") + + +def _sign_token(user, exp): + payload = f"{user}|{exp}".encode() + sig = hmac.new(AUTH_SECRET.encode(), payload, hashlib.sha256).hexdigest() + return base64.urlsafe_b64encode(payload + b"|" + sig.encode()).decode() + + +def _verify_token(token): + try: + raw = base64.urlsafe_b64decode(token.encode()).decode() + user, exp, sig = raw.split("|") + if int(exp) < int(time.time()): + return None + expect = hmac.new(AUTH_SECRET.encode(), f"{user}|{exp}".encode(), + hashlib.sha256).hexdigest() + return user if hmac.compare_digest(expect, sig) else None + except Exception: + return None + def _ensure_dirs(): os.makedirs(DATA_DIR, exist_ok=True) @@ -110,6 +139,17 @@ INDEX_HTML = r""" display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .tile time{display:block;margin-top:10px;font-size:12px;color:var(--muted)} .empty{text-align:center;color:var(--muted);padding:60px 0;line-height:2} + .login{max-width:380px;margin:10vh auto 0;background:var(--card);border:1px solid var(--border); + border-radius:20px;padding:32px 30px;box-shadow:0 10px 30px rgba(20,30,70,.08)} + .login .lg{width:52px;height:52px;border-radius:14px;background:var(--grad);display:grid;place-items:center; + color:#fff;font-weight:800;font-size:24px;margin:0 auto 16px} + .login h2{text-align:center;font-size:20px;margin-bottom:22px} + .login input{width:100%;padding:11px 12px;margin-bottom:12px;border-radius:10px; + border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:15px;outline:none} + .login input:focus{border-color:var(--brand)} + .login button{width:100%;padding:11px;border:none;border-radius:10px;background:var(--grad); + color:#fff;font-size:15px;font-weight:600;cursor:pointer} + .login .err{color:#ef4444;font-size:13px;text-align:center;height:18px;margin-bottom:8px} .page{padding-top:8px} .page .title{font-size:26px;font-weight:800;letter-spacing:-.02em;margin-bottom:4px} .page .meta{color:var(--muted);font-size:13px;margin-bottom:18px} @@ -148,6 +188,7 @@ INDEX_HTML = r""" const BASE = (()=>{const p=location.pathname;const i=p.indexOf('/xinquandashboard'); return i>=0?p.slice(0,i+'/xinquandashboard'.length):'';})(); const $ = s => document.querySelector(s); +const AUTH = __AUTH__; function api(path, opts){ return fetch(BASE+path, opts).then(async r=>{ @@ -171,7 +212,8 @@ async function loadPages(){ renderList(); } function renderList(){ - $('#topActions').innerHTML = ``; + $('#topActions').innerHTML = ` + `; const a=$('#app'); if(!state.pages.length){ a.innerHTML=`