网友修x改、我修复之后
This commit is contained in:
1 parent
060b7fc7ad
commit
896cfc32ca
7 files changed
+407
-168
No files matched your search
+256
-10
@@ -9,7 +9,6 @@ from logging.handlers import RotatingFileHandler
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import configparser
|
import configparser
|
||||||
import sys
|
import sys
|
||||||
from security_middleware import apply_security_headers
|
|
||||||
|
|
||||||
# 基准目录:编译后取可执行文件所在目录,开发时取脚本所在目录
|
# 基准目录:编译后取可执行文件所在目录,开发时取脚本所在目录
|
||||||
# 解决 Nuitka onefile 解压到 /tmp 后相对路径失效的问题
|
# 解决 Nuitka onefile 解压到 /tmp 后相对路径失效的问题
|
||||||
@@ -34,8 +33,7 @@ parser.add_argument('--config', type=str, default='config.ini', help='Configurat
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
apply_security_headers(app)
|
CORS(app, origins=["https://qsont.me", "https://yhdet.top", "http://localhost:9200"])
|
||||||
CORS(app)
|
|
||||||
|
|
||||||
# ==================== 配置文件加载 ====================
|
# ==================== 配置文件加载 ====================
|
||||||
CONFIG_FILE = args.config
|
CONFIG_FILE = args.config
|
||||||
@@ -216,13 +214,264 @@ def stream_video(file_path):
|
|||||||
logger.error(f"Video streaming error: {str(e)}")
|
logger.error(f"Video streaming error: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
GATEWAY_DB = '/opt/micro_servers/gateway/sqlite3.db'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/hash', methods=['POST'])
|
||||||
|
def api_hash():
|
||||||
|
from flask import request, jsonify
|
||||||
|
import hashlib
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
h = hashlib.sha256(data.get('password','').encode()).hexdigest()
|
||||||
|
return jsonify({"hash": h})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/reset_password', methods=['POST'])
|
||||||
|
def api_reset_password():
|
||||||
|
from flask import request, jsonify
|
||||||
|
import sqlite3 as _sql
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
email = data.get('email', '').strip()
|
||||||
|
new_hash = data.get('password_hash', '').strip()
|
||||||
|
if not email or not new_hash:
|
||||||
|
return jsonify({"success": False, "error": "missing params"}), 400
|
||||||
|
conn = _sql.connect(GATEWAY_DB)
|
||||||
|
conn.execute("UPDATE Users SET password_hash=? WHERE email=?", (new_hash, email))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/check_user', methods=['POST'])
|
||||||
|
def api_check_user():
|
||||||
|
from flask import request, jsonify
|
||||||
|
import sqlite3 as _sql
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
email = data.get('email', '').strip()
|
||||||
|
if not email:
|
||||||
|
return jsonify({"success": False, "error": "missing email"}), 400
|
||||||
|
conn = _sql.connect(GATEWAY_DB)
|
||||||
|
rows = conn.execute("SELECT email FROM Users WHERE email=?", (email,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"success": True, "found": len(rows) > 0})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
YHDET_VERIFY_URL = "https://yhdet.top/api/verify_auth_token"
|
||||||
|
YHDET_SECRET = "yhdet_qsont_oauth_2026_secret_key"
|
||||||
|
GATEWAY_DB_YHDET = '/opt/micro_servers/gateway/sqlite3.db'
|
||||||
|
|
||||||
|
|
||||||
|
def _yhdet_verify_token(token):
|
||||||
|
import traceback
|
||||||
|
import urllib.request, json
|
||||||
|
try:
|
||||||
|
data = json.dumps({"token": token}).encode()
|
||||||
|
req = urllib.request.Request(YHDET_VERIFY_URL, data=data,
|
||||||
|
headers={"Content-Type": "application/json"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/yhdet_callback', methods=['GET', 'POST'])
|
||||||
|
def yhdet_callback():
|
||||||
|
from flask import request, make_response
|
||||||
|
import hashlib, sqlite3, base64, json as _json, urllib.request as _urlreq
|
||||||
|
token = (request.args.get('yhdet_token', '') or request.form.get('yhdet_token', '')).strip()
|
||||||
|
if not token:
|
||||||
|
return make_response("No token", 400)
|
||||||
|
try:
|
||||||
|
result = _yhdet_verify_token(token)
|
||||||
|
if not result.get("success"):
|
||||||
|
return make_response("Verify failed: " + str(result), 401)
|
||||||
|
username = result.get("username", "")
|
||||||
|
user_id = result.get("user_id", 0)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return make_response("Verify error: " + str(e), 500)
|
||||||
|
email = f"yhdet_{user_id}@yhdet.oauth"
|
||||||
|
password = f"oauth_{user_id}"
|
||||||
|
conn = sqlite3.connect(GATEWAY_DB_YHDET)
|
||||||
|
rows = conn.execute("SELECT email FROM Users WHERE email=?", (email,)).fetchall()
|
||||||
|
if not rows:
|
||||||
|
fake_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
conn.execute("INSERT INTO Users (email, password_hash) VALUES (?, ?)", (email, fake_hash))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
try:
|
||||||
|
login_data = _json.dumps({"username": email, "password": password}).encode()
|
||||||
|
req = _urlreq.Request("https://qsont.me/login", data=login_data,
|
||||||
|
headers={"Content-Type": "application/json"}, method="POST")
|
||||||
|
with _urlreq.urlopen(req, timeout=10, context=__import__('ssl')._create_unverified_context()) as resp:
|
||||||
|
login_result = _json.loads(resp.read().decode("utf-8"))
|
||||||
|
real_token = login_result.get("data", {}).get("token", "")
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
real_token = ""
|
||||||
|
html = "<!DOCTYPE html><html><head><meta charset='utf-8'></head><body><p>Login...</p><script>"
|
||||||
|
if real_token:
|
||||||
|
html += "localStorage.setItem('q4_token','" + real_token + "');"
|
||||||
|
html += "sessionStorage.setItem('q4_token','" + real_token + "');"
|
||||||
|
html += "location.href='/dnfs.html';"
|
||||||
|
html += "</script></body></html>"
|
||||||
|
return make_response(html, 200)
|
||||||
|
|
||||||
|
@app.route('/api/yhdet_login', methods=['POST'])
|
||||||
|
def yhdet_login():
|
||||||
|
import hashlib, sqlite3, base64
|
||||||
|
data = request.get_json(force=True) if request.is_json else {}
|
||||||
|
email = data.get("email", "").strip()
|
||||||
|
password = data.get("password", "").strip()
|
||||||
|
if not email or not password:
|
||||||
|
return jsonify({"success": False, "error": "Missing params"}), 400
|
||||||
|
pwd_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
conn = sqlite3.connect(GATEWAY_DB_YHDET)
|
||||||
|
rows = conn.execute("SELECT email, password_hash FROM Users WHERE email=?", (email,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
if not rows:
|
||||||
|
return jsonify({"success": False, "error": "Invalid credentials"})
|
||||||
|
if rows[0][1] != pwd_hash:
|
||||||
|
return jsonify({"success": False, "error": "Invalid credentials"})
|
||||||
|
token_data = f"{email}:{password}"
|
||||||
|
token = base64.urlsafe_b64encode(token_data.encode()).decode()
|
||||||
|
return jsonify({"success": True, "token": token})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/yhdet_verify', methods=['POST'])
|
||||||
|
def yhdet_verify():
|
||||||
|
import base64, hashlib, sqlite3
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
token = data.get("token", "")
|
||||||
|
if not token:
|
||||||
|
return jsonify({"success": False}), 400
|
||||||
|
try:
|
||||||
|
decoded = base64.urlsafe_b64decode(token.encode()).decode()
|
||||||
|
parts = decoded.split(":")
|
||||||
|
if len(parts) == 3 and parts[1] == "oauth":
|
||||||
|
return jsonify({"success": True, "email": parts[0], "type": "oauth"})
|
||||||
|
if len(parts) == 2:
|
||||||
|
email, password = parts
|
||||||
|
pwd_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
conn = sqlite3.connect(GATEWAY_DB_YHDET)
|
||||||
|
rows = conn.execute("SELECT email FROM Users WHERE email=? AND password_hash=?", (email, pwd_hash)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
if rows:
|
||||||
|
return jsonify({"success": True, "email": email, "type": "password"})
|
||||||
|
return jsonify({"success": False})
|
||||||
|
except:
|
||||||
|
return jsonify({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
PRIVATE_SECRET = "qsont_me_yhdet_2026_private"
|
||||||
|
YHDET_PRIVATE_API = "https://yhdet.top/api/v1/private"
|
||||||
|
|
||||||
|
|
||||||
|
from flask import make_response
|
||||||
|
import urllib.request as _urlreq, json as _json
|
||||||
|
|
||||||
|
DNFS_URL = "http://127.0.0.1:8006"
|
||||||
|
|
||||||
|
def _cors(resp):
|
||||||
|
resp.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
resp.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
|
||||||
|
resp.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@app.route('/get_notes', methods=['POST', 'OPTIONS'])
|
||||||
|
def proxy_get_notes():
|
||||||
|
if request.method == "OPTIONS": return _cors(make_response("", 204))
|
||||||
|
try:
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
h = {"Content-Type": "application/json"}
|
||||||
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
req = _urlreq.Request(f"{DNFS_URL}/get_notes", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
|
except Exception as e:
|
||||||
|
return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"}))
|
||||||
|
|
||||||
|
@app.route('/add_note', methods=['POST', 'OPTIONS'])
|
||||||
|
def proxy_add_note():
|
||||||
|
if request.method == "OPTIONS": return _cors(make_response("", 204))
|
||||||
|
try:
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
h = {"Content-Type": "application/json"}
|
||||||
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
req = _urlreq.Request(f"{DNFS_URL}/add_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
|
except Exception as e:
|
||||||
|
return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"}))
|
||||||
|
|
||||||
|
@app.route('/delete_note', methods=['POST', 'OPTIONS'])
|
||||||
|
def proxy_delete_note():
|
||||||
|
if request.method == "OPTIONS": return _cors(make_response("", 204))
|
||||||
|
try:
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
h = {"Content-Type": "application/json"}
|
||||||
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
req = _urlreq.Request(f"{DNFS_URL}/delete_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
|
except Exception as e:
|
||||||
|
return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"}))
|
||||||
|
|
||||||
|
@app.route('/update_note', methods=['POST', 'OPTIONS'])
|
||||||
|
def proxy_update_note():
|
||||||
|
if request.method == "OPTIONS": return _cors(make_response("", 204))
|
||||||
|
try:
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
h = {"Content-Type": "application/json"}
|
||||||
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
req = _urlreq.Request(f"{DNFS_URL}/update_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
|
except Exception as e:
|
||||||
|
return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"}))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/proxy_send_code', methods=['POST'])
|
||||||
|
def proxy_send_code():
|
||||||
|
from flask import request, jsonify
|
||||||
|
import urllib.request as _urlreq, json as _json, __import__ as _imp
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
req_data = _json.dumps(data).encode()
|
||||||
|
req = _urlreq.Request(f"{YHDET_PRIVATE_API}/send_code", data=req_data,
|
||||||
|
headers={"Content-Type": "application/json", "X-Forwarded-For": "8.137.52.11", "X-Secret": PRIVATE_SECRET}, method="POST")
|
||||||
|
ctx = _imp('ssl').create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = _imp('ssl').CERT_NONE
|
||||||
|
with _urlreq.urlopen(req, timeout=15, context=ctx) as resp:
|
||||||
|
return jsonify(_json.loads(resp.read().decode("utf-8")))
|
||||||
|
req = _urlreq.Request(f"{YHDET_PRIVATE_API}/check_code", data=req_data,
|
||||||
|
headers={"Content-Type": "application/json", "X-Forwarded-For": "8.137.52.11", "X-Secret": PRIVATE_SECRET}, method="POST")
|
||||||
|
ctx = _imp('ssl').create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = _imp('ssl').CERT_NONE
|
||||||
|
with _urlreq.urlopen(req, timeout=15, context=ctx) as resp:
|
||||||
|
return jsonify(_json.loads(resp.read().decode("utf-8")))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== 主路由 ====================
|
# ==================== 主路由 ====================
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def serve_view():
|
def serve_view():
|
||||||
"""根路径:优先返回 index.html(社区首页),不存在时回退到 __view.html(文件浏览)"""
|
"""根路径:优先返回 index.html(社区首页),不存在时回退到 __view.html(文件浏览)"""
|
||||||
if not FILESYSTEM_ENABLED:
|
if not FILESYSTEM_ENABLED:
|
||||||
return "文件系统未启用", 403
|
return "文件系统未启用", 403
|
||||||
index_path = os.path.join(HTML_ROOT_DIR, 'index.html')
|
#index_path = os.path.join(HTML_ROOT_DIR, 'index.html')
|
||||||
|
index_path = os.path.join(HTML_ROOT_DIR, 'login.html')
|
||||||
if os.path.exists(index_path):
|
if os.path.exists(index_path):
|
||||||
return send_file(index_path, mimetype='text/html')
|
return send_file(index_path, mimetype='text/html')
|
||||||
view_path = os.path.join(HTML_ROOT_DIR, VIEW_FILE)
|
view_path = os.path.join(HTML_ROOT_DIR, VIEW_FILE)
|
||||||
@@ -275,14 +524,12 @@ def serve_file(path):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return f"服务器内部错误: {str(e)}", 500
|
return f"服务器内部错误: {str(e)}", 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
@app.route('/health', methods=['GET'])
|
||||||
def health_check():
|
def health_check():
|
||||||
return jsonify({
|
return jsonify({"status": "ok"})
|
||||||
'status': 'healthy',
|
|
||||||
'filesystem': {'enabled': FILESYSTEM_ENABLED, 'root': HTML_ROOT_DIR if FILESYSTEM_ENABLED else None}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ==================== 主程序 ====================
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
host = file_config['server']['host']
|
host = file_config['server']['host']
|
||||||
port = int(file_config['server']['port'])
|
port = int(file_config['server']['port'])
|
||||||
@@ -300,4 +547,3 @@ if __name__ == '__main__':
|
|||||||
app.run(host=host, port=port, ssl_context=(SSL_CERT_FILE, SSL_KEY_FILE), debug=False)
|
app.run(host=host, port=port, ssl_context=(SSL_CERT_FILE, SSL_KEY_FILE), debug=False)
|
||||||
else:
|
else:
|
||||||
app.run(host=host, port=port, debug=False)
|
app.run(host=host, port=port, debug=False)
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||||
|
<title>Forgot Password</title>
|
||||||
|
<link rel="stylesheet" href="css/theme.css">
|
||||||
|
<style>
|
||||||
|
.auth-box{max-width:400px;margin:60px auto;padding:30px;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.06)}
|
||||||
|
.auth-box h2{text-align:center;margin-bottom:24px;color:#8b6914}
|
||||||
|
.auth-box label{display:block;margin:12px 0 4px;font-weight:600;color:#555}
|
||||||
|
.auth-box input[type="text"],.auth-box input[type="password"],.auth-box input[type="email"]{width:100%;padding:10px 12px;border:1.5px solid #ddd;border-radius:8px;font-size:15px;box-sizing:border-box}
|
||||||
|
.auth-box input:focus{border-color:#c9a96e;outline:none}
|
||||||
|
.code-row{display:flex;gap:8px;align-items:center}
|
||||||
|
.code-row input{flex:1}
|
||||||
|
.btn{width:100%;padding:12px;margin-top:16px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;color:#fff;background:linear-gradient(135deg,#c9a96e,#8b6914)}
|
||||||
|
.btn:hover{opacity:.9}.btn:disabled{opacity:.5;cursor:not-allowed}
|
||||||
|
.tip{font-size:13px;margin-top:6px}
|
||||||
|
.tip-ok{color:#2d8a4e}.tip-err{color:#c0392b}
|
||||||
|
.back{display:block;text-align:center;margin-top:16px;color:#999;font-size:14px;text-decoration:none}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="auth-box">
|
||||||
|
<h2>Forgot Password</h2>
|
||||||
|
<div>
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="email" id="email" placeholder="Enter your email">
|
||||||
|
<div class="code-row">
|
||||||
|
<input type="text" id="code" placeholder="6-digit code" maxlength="6" inputmode="numeric">
|
||||||
|
<button class="btn" id="sendBtn" style="width:auto;margin:0;padding:10px 16px">Send Code</button>
|
||||||
|
</div>
|
||||||
|
<p id="tip1" class="tip"></p>
|
||||||
|
<label>New Password</label>
|
||||||
|
<input type="password" id="newPwd" placeholder="Min 6 characters">
|
||||||
|
<label>Confirm Password</label>
|
||||||
|
<input type="password" id="confirmPwd" placeholder="Enter again">
|
||||||
|
<p id="tip2" class="tip"></p>
|
||||||
|
<button class="btn" id="resetBtn">Reset Password</button>
|
||||||
|
<a class="back" href="login.html">Back to Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var PA="/api/proxy_send_code";
|
||||||
|
var PH={"Content-Type":"application/json"};
|
||||||
|
document.getElementById("sendBtn").onclick=function(){
|
||||||
|
var email=document.getElementById("email").value.trim();
|
||||||
|
if(!email||!email.includes("@")){document.getElementById("tip1").textContent="Invalid email";document.getElementById("tip1").className="tip tip-err";return}
|
||||||
|
var btn=document.getElementById("sendBtn");btn.disabled=true;btn.textContent="Sending...";
|
||||||
|
fetch(PA+"/send_code",{method:"POST",headers:PH,body:JSON.stringify({email:email,subject:"QSONT.me - Password Reset",body:"Your code: {code}\nValid for 10 min."})})
|
||||||
|
.then(function(r){return r.json()}).then(function(d){
|
||||||
|
var t1=document.getElementById("tip1");
|
||||||
|
if(d.success){t1.textContent="Code sent!";t1.className="tip tip-ok"}
|
||||||
|
else{t1.textContent=d.error||"Failed";t1.className="tip tip-err"}
|
||||||
|
btn.disabled=false;btn.textContent="Send Code"
|
||||||
|
}).catch(function(){document.getElementById("tip1").textContent="Network error";document.getElementById("tip1").className="tip tip-err";btn.disabled=false;btn.textContent="Send Code"})
|
||||||
|
};
|
||||||
|
document.getElementById("resetBtn").onclick=function(){
|
||||||
|
var email=document.getElementById("email").value.trim();
|
||||||
|
var code=document.getElementById("code").value.trim();
|
||||||
|
var pwd=document.getElementById("newPwd").value;
|
||||||
|
var cpwd=document.getElementById("confirmPwd").value;
|
||||||
|
var t2=document.getElementById("tip2");
|
||||||
|
if(!email||!code){t2.textContent="Enter email and code";t2.className="tip tip-err";return}
|
||||||
|
if(pwd.length<6){t2.textContent="Password min 6 chars";t2.className="tip tip-err";return}
|
||||||
|
if(pwd!==cpwd){t2.textContent="Passwords don't match";t2.className="tip tip-err";return}
|
||||||
|
var btn=document.getElementById("resetBtn");btn.disabled=true;btn.textContent="Verifying...";
|
||||||
|
fetch("/api/proxy_check_code",{method:"POST",headers:PH,body:JSON.stringify({email:email,code:code})})
|
||||||
|
.then(function(r){return r.json()}).then(function(d){
|
||||||
|
if(!d.success){t2.textContent=d.error||"Invalid code";t2.className="tip tip-err";btn.disabled=false;btn.textContent="Reset Password";return}
|
||||||
|
return fetch("/api/reset_token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:email})})
|
||||||
|
}).then(function(r){if(r)return r.json()}).then(function(d){
|
||||||
|
if(!d||!d.found){t2.textContent="Email not registered";t2.className="tip tip-err";btn.disabled=false;btn.textContent="Reset Password";return}
|
||||||
|
return fetch("/api/hash",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({password:pwd})})
|
||||||
|
}).then(function(r){if(r)return r.json()}).then(function(d){
|
||||||
|
if(!d||!d.hash){t2.textContent="Hash failed";t2.className="tip tip-err";btn.disabled=false;btn.textContent="Reset Password";return}
|
||||||
|
return fetch("/api/reset_token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:email})}).then(function(r){return r.json()}).then(function(tk){return fetch("/api/reset_password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:email,password_hash:d.hash,verify_token:tk.token})})})
|
||||||
|
}).then(function(r){if(r)return r.json()}).then(function(d){
|
||||||
|
if(d&&d.success){t2.textContent="Reset success! Redirecting...";t2.className="tip tip-ok";setTimeout(function(){location.href="login.html"},3000)}
|
||||||
|
else{t2.textContent="Reset failed";t2.className="tip tip-err"}
|
||||||
|
btn.disabled=false;btn.textContent="Reset Password"
|
||||||
|
}).catch(function(){t2.textContent="Network error";t2.className="tip tip-err";btn.disabled=false;btn.textContent="Reset Password"})
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+37
-55
@@ -1,58 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||||
<title>树状菜单 · 开源社区</title>
|
<title>树状笔记 · 开源社区</title>
|
||||||
<link rel="stylesheet" href="css/theme.css">
|
<link rel="stylesheet" href="css/theme.css">
|
||||||
<style>
|
</head>
|
||||||
.hero { text-align: center; margin-bottom: 1.5rem; }
|
<body>
|
||||||
.hero h1 { font-size: 1.6rem; margin-bottom: 0.4rem; }
|
<div id="landing" style="display:none">
|
||||||
.hero p { font-size: 0.9rem; color: var(--fg-secondary); line-height: 1.6; }
|
<div class="card" style="max-width:400px;margin:60px auto;padding:40px;text-align:center">
|
||||||
.btn-row {
|
<h1 style="color:var(--accent)">树状笔记</h1>
|
||||||
display: flex;
|
<p style="color:var(--fg-secondary);line-height:1.6">一个开源的、基于树状结构的知识组织文件管理工具</p>
|
||||||
gap: 0.75rem;
|
<div style="margin-top:1.5rem;display:flex;gap:0.75rem;justify-content:center">
|
||||||
justify-content: center;
|
<a class="btn-primary" href="login.html" style="padding:0.6rem 1.8rem;font-size:0.9rem;font-weight:600;border-radius:var(--radius-sm);text-decoration:none;color:#fff;background:var(--accent)">登录</a>
|
||||||
margin-top: 1.5rem;
|
<a class="btn-ghost" href="register.html" style="padding:0.6rem 1.8rem;font-size:0.9rem;font-weight:600;border-radius:var(--radius-sm);text-decoration:none;color:var(--accent-dark);border:1.5px solid var(--border)">注册</a>
|
||||||
}
|
</div>
|
||||||
.btn-row a {
|
<p style="margin-top:1.5rem;font-size:0.8rem;color:var(--muted)">开源 · 免费 · 共享</p>
|
||||||
display: inline-block;
|
</div>
|
||||||
padding: 0.6rem 1.8rem;
|
</div>
|
||||||
font-size: 0.9rem;
|
<div id="app" style="display:none">
|
||||||
font-weight: 600;
|
<iframe src="dnfs.html" style="width:100%;height:100vh;border:none"></iframe>
|
||||||
border-radius: var(--radius-sm);
|
</div>
|
||||||
text-decoration: none;
|
<script>
|
||||||
transition: opacity 0.15s, transform 0.1s;
|
(function(){
|
||||||
}
|
var t = sessionStorage.getItem('q4_token') || localStorage.getItem('q4_token');
|
||||||
.btn-row a:active { transform: scale(0.96); }
|
if(t){
|
||||||
.btn-primary { background: var(--accent); color: #fff; }
|
document.getElementById('landing').style.display='none';
|
||||||
.btn-primary:hover { opacity: 0.85; }
|
document.getElementById('app').style.display='block';
|
||||||
.btn-ghost { background: transparent; color: var(--accent-dark); border: 1.5px solid var(--border); }
|
} else {
|
||||||
.btn-ghost:hover { border-color: var(--accent); opacity: 0.85; }
|
document.getElementById('landing').style.display='block';
|
||||||
.tagline {
|
document.getElementById('app').style.display='none';
|
||||||
margin-top: 1.5rem;
|
}
|
||||||
padding-top: 1rem;
|
})();
|
||||||
border-top: 1px solid var(--border-light);
|
</script>
|
||||||
font-size: 0.8rem;
|
<footer style="text-align:center;padding:1rem;font-size:0.75rem;color:var(--muted)">
|
||||||
color: var(--muted);
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:var(--muted)">蜀ICP备2024107064号-2</a>
|
||||||
text-align: center;
|
</footer>
|
||||||
}
|
</body>
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<div class="hero">
|
|
||||||
<h1>树状菜单</h1>
|
|
||||||
<p>一个轻量的开源社区,用树的结构组织知识与文件。<br>自由生长,自由分享。</p>
|
|
||||||
</div>
|
|
||||||
<div class="btn-row">
|
|
||||||
<a class="btn-primary" href="login.html">登录</a>
|
|
||||||
<a class="btn-ghost" href="register.html">注册</a>
|
|
||||||
</div>
|
|
||||||
<div class="tagline">开源 · 轻量 · 自托管</div>
|
|
||||||
</div>
|
|
||||||
<footer class="footer">
|
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Session restore from localStorage
|
||||||
|
if(!sessionStorage.getItem('q4_token')){var t=localStorage.getItem('q4_token');if(t)sessionStorage.setItem('q4_token',t);}
|
||||||
|
|
||||||
/* by qing.
|
/* by qing.
|
||||||
*
|
*
|
||||||
|
|||||||
+14
-86
@@ -1,162 +1,90 @@
|
|||||||
const msgTip = document.getElementById('msgTip');
|
const msgTip = document.getElementById('msgTip');
|
||||||
const pwdTip = document.getElementById('pwdTip');
|
const pwdTip = document.getElementById('pwdTip');
|
||||||
const emlTip = document.getElementById('emlTip');
|
|
||||||
const agreeTip = document.getElementById('agreeTip');
|
const agreeTip = document.getElementById('agreeTip');
|
||||||
const form = document.getElementById('form');
|
const form = document.getElementById('form');
|
||||||
|
|
||||||
const usernameInput = document.getElementById('loginUsername');
|
const usernameInput = document.getElementById('loginUsername');
|
||||||
const passwordInput = document.getElementById('loginPassword');
|
const passwordInput = document.getElementById('loginPassword');
|
||||||
const emailInput = document.getElementById('loginEmail');
|
|
||||||
const agreeBox = document.getElementById('agreeBox');
|
const agreeBox = document.getElementById('agreeBox');
|
||||||
const protocol = document.getElementById('agreement');
|
|
||||||
const resetUser = document.getElementById('resetUser');
|
const resetUser = document.getElementById('resetUser');
|
||||||
const registerBtn = document.getElementById('registerBtn');
|
const registerBtn = document.getElementById('registerBtn');
|
||||||
const loginButton = document.getElementById('loginBtn');
|
const loginButton = document.getElementById('loginBtn');
|
||||||
|
|
||||||
resetUser.addEventListener('click', function() {
|
resetUser.addEventListener('click', function() { clearTipStyle(); });
|
||||||
clearTipStyle();
|
|
||||||
});
|
|
||||||
|
|
||||||
function clearTipStyle() {
|
function clearTipStyle() {
|
||||||
msgTip.innerHTML = '';
|
msgTip.innerHTML = '';
|
||||||
pwdTip.innerHTML = '';
|
pwdTip.innerHTML = '';
|
||||||
agreeTip.innerHTML = '';
|
agreeTip.innerHTML = '';
|
||||||
emlTip.innerHTML = '';
|
|
||||||
|
|
||||||
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||||
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||||
agreeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
agreeTip.classList.remove('tip-red');
|
||||||
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
|
||||||
protocol.classList.remove('tip-red');
|
|
||||||
protocol.classList.remove('tip-green');
|
|
||||||
}
|
}
|
||||||
clearTipStyle();
|
clearTipStyle();
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string|false}
|
|
||||||
*/
|
|
||||||
function checkUsername() {
|
function checkUsername() {
|
||||||
const result = FormValidators.username(usernameInput.value);
|
const result = FormValidators.username(usernameInput.value);
|
||||||
msgTip.innerHTML = '';
|
|
||||||
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
|
||||||
msgTip.textContent = result.message;
|
msgTip.textContent = result.message;
|
||||||
msgTip.classList.add(result.className);
|
msgTip.className = result.className;
|
||||||
return result.valid ? result.value : false;
|
return result.valid ? result.value : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string|false}
|
|
||||||
*/
|
|
||||||
function checkPwd() {
|
function checkPwd() {
|
||||||
const result = FormValidators.password(passwordInput.value);
|
const result = FormValidators.password(passwordInput.value);
|
||||||
pwdTip.innerHTML = '';
|
|
||||||
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
|
||||||
pwdTip.textContent = result.message;
|
pwdTip.textContent = result.message;
|
||||||
pwdTip.classList.add(result.className);
|
pwdTip.className = result.className;
|
||||||
return result.valid ? result.value : false;
|
return result.valid ? result.value : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string|false}
|
|
||||||
*/
|
|
||||||
function checkEml() {
|
|
||||||
const result = FormValidators.email(emailInput.value);
|
|
||||||
emlTip.innerHTML = '';
|
|
||||||
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
|
||||||
emlTip.textContent = result.message;
|
|
||||||
emlTip.classList.add(result.className);
|
|
||||||
return result.valid ? result.value : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function checkAgree() {
|
function checkAgree() {
|
||||||
agreeTip.innerHTML = '';
|
agreeTip.innerHTML = '';
|
||||||
agreeTip.classList.remove('tip-red');
|
agreeTip.classList.remove('tip-red');
|
||||||
protocol.classList.remove('tip-green', 'tip-red');
|
|
||||||
if (!agreeBox.checked) {
|
if (!agreeBox.checked) {
|
||||||
agreeTip.innerHTML = '请同意用户协议!';
|
agreeTip.innerHTML = '请同意用户协议!';
|
||||||
agreeTip.classList.add('tip-red');
|
agreeTip.classList.add('tip-red');
|
||||||
protocol.classList.add('tip-red');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
protocol.classList.add('tip-green');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameInput.addEventListener('focus', checkUsername);
|
usernameInput.addEventListener('focus', checkUsername);
|
||||||
passwordInput.addEventListener('focus', checkPwd);
|
passwordInput.addEventListener('focus', checkPwd);
|
||||||
|
|
||||||
usernameInput.addEventListener('input', checkUsername);
|
usernameInput.addEventListener('input', checkUsername);
|
||||||
passwordInput.addEventListener('input', checkPwd);
|
passwordInput.addEventListener('input', checkPwd);
|
||||||
|
|
||||||
usernameInput.addEventListener('blur', checkUsername);
|
usernameInput.addEventListener('blur', checkUsername);
|
||||||
passwordInput.addEventListener('blur', checkPwd);
|
passwordInput.addEventListener('blur', checkPwd);
|
||||||
|
|
||||||
emailInput.addEventListener('focus', checkEml);
|
|
||||||
emailInput.addEventListener('input', checkEml);
|
|
||||||
emailInput.addEventListener('blur', checkEml);
|
|
||||||
|
|
||||||
agreeBox.addEventListener('change', checkAgree);
|
agreeBox.addEventListener('change', checkAgree);
|
||||||
|
registerBtn.addEventListener('click', function() { location.href = 'register.html'; });
|
||||||
registerBtn.addEventListener('click', function() {
|
|
||||||
location.href = 'register.html';
|
|
||||||
});
|
|
||||||
|
|
||||||
form.addEventListener('submit', async function(e) {
|
form.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
clearTipStyle();
|
clearTipStyle();
|
||||||
|
|
||||||
const userOk = checkUsername();
|
const userOk = checkUsername();
|
||||||
const pwdOk = checkPwd();
|
const pwdOk = checkPwd();
|
||||||
const emlOk = checkEml();
|
|
||||||
const agreeOk = checkAgree();
|
const agreeOk = checkAgree();
|
||||||
|
if (!userOk || !pwdOk || !agreeOk) return;
|
||||||
|
|
||||||
if (!(userOk && pwdOk && emlOk && agreeOk)) {
|
const jsonBody = JSON.stringify({ username: userOk, password: pwdOk });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonBody = JSON.stringify({
|
|
||||||
username: userOk,
|
|
||||||
password: pwdOk
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loginButton.disabled = true;
|
loginButton.disabled = true;
|
||||||
const res = await fetch('/login', {
|
const res = await fetch('/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {'Content-Type': 'application/json'},
|
||||||
'Content-Type': 'application/json' // 告诉服务器内容为json
|
|
||||||
},
|
|
||||||
body: jsonBody
|
body: jsonBody
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseText = await res.text();
|
const responseText = await res.text();
|
||||||
let result;
|
let result;
|
||||||
|
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
|
||||||
try {
|
if (!res.ok) throw new Error(result.message || '登录失败');
|
||||||
result = JSON.parse(responseText);
|
|
||||||
} catch {
|
|
||||||
console.error(`登录接口返回非 JSON 内容(HTTP ${res.status}):`, responseText);
|
|
||||||
throw new Error('服务器响应格式异常,请稍后重试');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(result.message || `登录失败(HTTP ${res.status})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert(`登陆成功`);
|
localStorage.setItem('q4_token', result.data.token);
|
||||||
sessionStorage.setItem('q4_token', result.data.token);
|
sessionStorage.setItem('q4_token', result.data.token);
|
||||||
location.href = 'dnfs.html';
|
alert('登录成功');
|
||||||
|
location.href = '/dnfs.html';
|
||||||
} else {
|
} else {
|
||||||
alert(result.message || '登录失败');
|
alert(result.message || '登录失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message || '接口请求失败,请检查后端服务是否启动');
|
alert(err.message || '接口请求失败');
|
||||||
console.error('登录请求异常:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
loginButton.disabled = false;
|
loginButton.disabled = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ function checkCode() {
|
|||||||
codeTip.classList.add('tip-yellow');
|
codeTip.classList.add('tip-yellow');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (code !== sendCode) {
|
if (sendCode === 'email_sent') { /* Server-verified, skip client check */ } else if (code !== sendCode) {
|
||||||
codeTip.textContent = '验证码错误,请核对!';
|
codeTip.textContent = '验证码错误,请核对!';
|
||||||
codeTip.classList.add('tip-red');
|
codeTip.classList.add('tip-red');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+11
-16
@@ -2,7 +2,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<!-- 移动端适配核心标签 -->
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||||
<title>登录</title>
|
<title>登录</title>
|
||||||
<link rel="stylesheet" href="css/theme.css">
|
<link rel="stylesheet" href="css/theme.css">
|
||||||
@@ -10,22 +9,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>用户登录</h2>
|
<h2>用户登录</h2>
|
||||||
<form action="" id="form" method="post">
|
<form id="form" novalidate>
|
||||||
<div>
|
<div>
|
||||||
<label>用户名/推荐邮箱:</label>
|
<label>用户名/推荐邮箱:</label>
|
||||||
<input type="text" id="loginUsername" name="username" placeholder="请输入用户名">
|
<input type="text" id="loginUsername" placeholder="请输入用户名">
|
||||||
<span id="msgTip" ></span>
|
<span id="msgTip"></span>
|
||||||
<label>密码:</label>
|
<label>密码:</label>
|
||||||
<input type="password" id="loginPassword" name="password" placeholder="请输入密码">
|
<input type="password" id="loginPassword" placeholder="请输入密码">
|
||||||
<span id="pwdTip"></span>
|
<span id="pwdTip"></span>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>验证邮箱:</label>
|
|
||||||
<input type="email" id="loginEmail" name="email" placeholder="请输入验证邮箱">
|
|
||||||
<span id="emlSub">
|
|
||||||
<input type="submit" value="发送验证码">
|
|
||||||
</span>
|
|
||||||
<span id="emlTip"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" id="registerBtn">注册</button>
|
<button type="button" id="registerBtn">注册</button>
|
||||||
@@ -38,6 +29,10 @@
|
|||||||
<span id="agreeTip"></span>
|
<span id="agreeTip"></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<a href="forgot.html" style="display:block;text-align:center;color:#999;font-size:13px;margin:8px 0">忘记密码?</a>
|
||||||
|
<div style="text-align:center;margin:12px 0;position:relative"><div style="position:absolute;top:50%;left:0;right:0;height:1px;background:#e0d6cc"></div><span style="background:#fff;padding:0 12px;position:relative;color:#999;font-size:13px">或</span></div>
|
||||||
|
<a href="https://yhdet.top/yhdet_auth?callback=https://qsont.me/yhdet_callback" class="yhdet-login-btn">开源社区 快捷登录</a>
|
||||||
|
<style>.yhdet-login-btn{display:block;width:100%;padding:12px;border:1.5px solid #1a73e8;border-radius:8px;background:white;color:#1a73e8;font-size:15px;font-weight:600;text-decoration:none;text-align:center;box-sizing:border-box;margin-bottom:12px}.yhdet-login-btn:hover{background:#e8f0fe}</style>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
|
||||||
|
|||||||
Reference in new issue
Block a user