网友修x改、我修复之后

This commit is contained in:
qing committed 2026-07-26 12:06:56 +08:00
1 parent 060b7fc7ad
commit 896cfc32ca
7 files changed
+702 -463

No files matched your search

+549 -303
View File
@@ -1,303 +1,549 @@
# file_server.py - 极简版文件托管服务器 # file_server.py - 极简版文件托管服务器
from flask import Flask, send_from_directory, jsonify, request, Response, send_file from flask import Flask, send_from_directory, jsonify, request, Response, send_file
from flask_cors import CORS from flask_cors import CORS
import os import os
import argparse import argparse
import datetime import datetime
import logging import logging
from logging.handlers import RotatingFileHandler 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 后相对路径失效的问题 if getattr(sys, 'frozen', False):
if getattr(sys, 'frozen', False): BASE_DIR = os.path.dirname(os.path.abspath(sys.executable))
BASE_DIR = os.path.dirname(os.path.abspath(sys.executable)) else:
else: BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 配置日志
# 配置日志 logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
file_handler = RotatingFileHandler(os.path.join(BASE_DIR, 'app.log'), maxBytes=10485760, backupCount=10)
file_handler = RotatingFileHandler(os.path.join(BASE_DIR, 'app.log'), maxBytes=10485760, backupCount=10) file_handler.setFormatter(logging.Formatter(
file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s'
'%(asctime)s %(levelname)s: %(message)s' ))
)) file_handler.setLevel(logging.INFO)
file_handler.setLevel(logging.INFO) logger.addHandler(file_handler)
logger.addHandler(file_handler)
parser = argparse.ArgumentParser(description='File Server')
parser = argparse.ArgumentParser(description='File Server') parser.add_argument('--config', type=str, default='config.ini', help='Configuration file path')
parser.add_argument('--config', type=str, default='config.ini', help='Configuration file path') args = parser.parse_args()
args = parser.parse_args()
app = Flask(__name__)
app = Flask(__name__) CORS(app, origins=["https://qsont.me", "https://yhdet.top", "http://localhost:9200"])
apply_security_headers(app)
CORS(app) # ==================== 配置文件加载 ====================
CONFIG_FILE = args.config
# ==================== 配置文件加载 ==================== if not os.path.isabs(CONFIG_FILE):
CONFIG_FILE = args.config CONFIG_FILE = os.path.join(BASE_DIR, CONFIG_FILE)
if not os.path.isabs(CONFIG_FILE):
CONFIG_FILE = os.path.join(BASE_DIR, CONFIG_FILE) file_config = {
'server': {'host': '0.0.0.0', 'port': '5000'},
file_config = { 'directory': {'root': './public'},
'server': {'host': '0.0.0.0', 'port': '5000'}, 'ssl': {'enabled': 'false', 'cert_file': 'cert.pem', 'key_file': 'key.pem'},
'directory': {'root': './public'}, }
'ssl': {'enabled': 'false', 'cert_file': 'cert.pem', 'key_file': 'key.pem'},
} if os.path.exists(CONFIG_FILE):
config_parser = configparser.ConfigParser()
if os.path.exists(CONFIG_FILE): config_parser.read(CONFIG_FILE, encoding='utf-8')
config_parser = configparser.ConfigParser() for section in ['server', 'directory', 'ssl']:
config_parser.read(CONFIG_FILE, encoding='utf-8') if section in config_parser:
for section in ['server', 'directory', 'ssl']: for key, value in config_parser[section].items():
if section in config_parser: if key in file_config[section]:
for key, value in config_parser[section].items(): file_config[section][key] = value
if key in file_config[section]:
file_config[section][key] = value HTML_ROOT_DIR = file_config['directory']['root']
if HTML_ROOT_DIR and not os.path.isabs(HTML_ROOT_DIR):
HTML_ROOT_DIR = file_config['directory']['root'] HTML_ROOT_DIR = os.path.join(BASE_DIR, HTML_ROOT_DIR)
if HTML_ROOT_DIR and not os.path.isabs(HTML_ROOT_DIR): FILESYSTEM_ENABLED = bool(HTML_ROOT_DIR and HTML_ROOT_DIR.strip())
HTML_ROOT_DIR = os.path.join(BASE_DIR, HTML_ROOT_DIR)
FILESYSTEM_ENABLED = bool(HTML_ROOT_DIR and HTML_ROOT_DIR.strip()) if FILESYSTEM_ENABLED:
try:
if FILESYSTEM_ENABLED: Path(HTML_ROOT_DIR).mkdir(parents=True, exist_ok=True)
try: logger.info(f"Created directory: {HTML_ROOT_DIR}")
Path(HTML_ROOT_DIR).mkdir(parents=True, exist_ok=True) except Exception as e:
logger.info(f"Created directory: {HTML_ROOT_DIR}") logger.error(f"Failed to create directory: {str(e)}")
except Exception as e: FILESYSTEM_ENABLED = False
logger.error(f"Failed to create directory: {str(e)}")
FILESYSTEM_ENABLED = False SSL_ENABLED = file_config['ssl'].get('enabled', 'false').lower() == 'true'
SSL_CERT_FILE = file_config['ssl'].get('cert_file', 'cert.pem')
SSL_ENABLED = file_config['ssl'].get('enabled', 'false').lower() == 'true' SSL_KEY_FILE = file_config['ssl'].get('key_file', 'key.pem')
SSL_CERT_FILE = file_config['ssl'].get('cert_file', 'cert.pem') if not os.path.isabs(SSL_CERT_FILE):
SSL_KEY_FILE = file_config['ssl'].get('key_file', 'key.pem') SSL_CERT_FILE = os.path.join(BASE_DIR, SSL_CERT_FILE)
if not os.path.isabs(SSL_CERT_FILE): if not os.path.isabs(SSL_KEY_FILE):
SSL_CERT_FILE = os.path.join(BASE_DIR, SSL_CERT_FILE) SSL_KEY_FILE = os.path.join(BASE_DIR, SSL_KEY_FILE)
if not os.path.isabs(SSL_KEY_FILE):
SSL_KEY_FILE = os.path.join(BASE_DIR, SSL_KEY_FILE) # 视图文件名:根路径与目录访问时固定返回此静态文件,由前端 JS 调用 /api/filelist 拉取数据
VIEW_FILE = '__view.html'
# 视图文件名:根路径与目录访问时固定返回此静态文件,由前端 JS 调用 /api/filelist 拉取数据
VIEW_FILE = '__view.html' # ==================== 视频文件支持 ====================
VIDEO_MIME_TYPES = {
# ==================== 视频文件支持 ==================== '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg',
VIDEO_MIME_TYPES = { '.avi': 'video/x-msvideo', '.mov': 'video/quicktime', '.mkv': 'video/x-matroska'
'.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', }
'.avi': 'video/x-msvideo', '.mov': 'video/quicktime', '.mkv': 'video/x-matroska'
} AUDIO_MIME_TYPES = {
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg',
AUDIO_MIME_TYPES = { '.flac': 'audio/flac', '.aac': 'audio/aac', '.m4a': 'audio/mp4'
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', }
'.flac': 'audio/flac', '.aac': 'audio/aac', '.m4a': 'audio/mp4'
} def is_video_file(filename):
return os.path.splitext(filename)[1].lower() in VIDEO_MIME_TYPES
def is_video_file(filename):
return os.path.splitext(filename)[1].lower() in VIDEO_MIME_TYPES def is_audio_file(filename):
return os.path.splitext(filename)[1].lower() in AUDIO_MIME_TYPES
def is_audio_file(filename):
return os.path.splitext(filename)[1].lower() in AUDIO_MIME_TYPES def get_media_mime_type(filename):
ext = os.path.splitext(filename)[1].lower()
def get_media_mime_type(filename): return VIDEO_MIME_TYPES.get(ext) or AUDIO_MIME_TYPES.get(ext) or 'application/octet-stream'
ext = os.path.splitext(filename)[1].lower()
return VIDEO_MIME_TYPES.get(ext) or AUDIO_MIME_TYPES.get(ext) or 'application/octet-stream' def safe_path_join(base_dir, user_path):
if not user_path or user_path.strip() == '' or user_path.strip() == '/':
def safe_path_join(base_dir, user_path): return base_dir
if not user_path or user_path.strip() == '' or user_path.strip() == '/': if user_path.startswith('/'):
return base_dir user_path = user_path[1:]
if user_path.startswith('/'): full_path = os.path.normpath(os.path.join(base_dir, user_path))
user_path = user_path[1:] base_dir_real = os.path.realpath(base_dir)
full_path = os.path.normpath(os.path.join(base_dir, user_path)) full_path_real = os.path.realpath(full_path)
base_dir_real = os.path.realpath(base_dir) if not full_path_real.startswith(base_dir_real):
full_path_real = os.path.realpath(full_path) return None
if not full_path_real.startswith(base_dir_real): return full_path_real
return None
return full_path_real def format_file_size(size):
for unit in ['B', 'KB', 'MB', 'GB']:
def format_file_size(size): if size < 1024.0:
for unit in ['B', 'KB', 'MB', 'GB']: return f"{size:.1f}{unit}"
if size < 1024.0: size /= 1024.0
return f"{size:.1f}{unit}" return f"{size:.1f}TB"
size /= 1024.0
return f"{size:.1f}TB" # ==================== API 接口 ====================
@app.route('/api/filelist', methods=['GET'])
# ==================== API 接口 ==================== def get_file_list():
@app.route('/api/filelist', methods=['GET']) try:
def get_file_list(): if not FILESYSTEM_ENABLED:
try: return jsonify({'error': '文件系统未启用'}), 403
if not FILESYSTEM_ENABLED:
return jsonify({'error': '文件系统未启用'}), 403 requested_path = request.args.get('path', '')
full_path = safe_path_join(HTML_ROOT_DIR, requested_path)
requested_path = request.args.get('path', '')
full_path = safe_path_join(HTML_ROOT_DIR, requested_path) if full_path is None:
return jsonify({'error': '无效的路径'}), 403
if full_path is None:
return jsonify({'error': '无效的路径'}), 403 if not os.path.exists(full_path) or not os.path.isdir(full_path):
return jsonify({'error': '目录不存在'}), 404
if not os.path.exists(full_path) or not os.path.isdir(full_path):
return jsonify({'error': '目录不存在'}), 404 items = []
for item in os.listdir(full_path):
items = [] # 过滤系统项目:以双下划线开头的文件和文件夹(如 __pycache__、__view.html、__icons 等)
for item in os.listdir(full_path): if item.startswith('__'):
# 过滤系统项目:以双下划线开头的文件和文件夹(如 __pycache__、__view.html、__icons 等) continue
if item.startswith('__'):
continue item_path = os.path.join(full_path, item)
item_stat = os.stat(item_path)
item_path = os.path.join(full_path, item) is_dir = os.path.isdir(item_path)
item_stat = os.stat(item_path)
is_dir = os.path.isdir(item_path) item_rel_path = os.path.join(requested_path, item) if requested_path else item
item_rel_path = os.path.join(requested_path, item) if requested_path else item file_type = 'dir' if is_dir else ('video' if is_video_file(item) else ('audio' if is_audio_file(item) else 'file'))
file_type = 'dir' if is_dir else ('video' if is_video_file(item) else ('audio' if is_audio_file(item) else 'file')) items.append({
'name': item,
items.append({ 'type': file_type,
'name': item, 'path': item_rel_path,
'type': file_type, 'size': item_stat.st_size if not is_dir else 0,
'path': item_rel_path, 'size_formatted': format_file_size(item_stat.st_size) if not is_dir else '',
'size': item_stat.st_size if not is_dir else 0, 'modified': datetime.datetime.fromtimestamp(item_stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
'size_formatted': format_file_size(item_stat.st_size) if not is_dir else '', })
'modified': datetime.datetime.fromtimestamp(item_stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
}) items.sort(key=lambda x: (x['type'] != 'dir', x['name'].lower()))
items.sort(key=lambda x: (x['type'] != 'dir', x['name'].lower())) return jsonify({
'success': True,
return jsonify({ 'path': requested_path or '/',
'success': True, 'parent_path': os.path.dirname(requested_path) if requested_path and os.path.dirname(requested_path) != '/' else '',
'path': requested_path or '/', 'items': items
'parent_path': os.path.dirname(requested_path) if requested_path and os.path.dirname(requested_path) != '/' else '', })
'items': items except Exception as e:
}) logger.error(f"Get file list error: {str(e)}")
except Exception as e: return jsonify({'error': str(e)}), 500
logger.error(f"Get file list error: {str(e)}")
return jsonify({'error': str(e)}), 500 @app.route('/video/<path:file_path>', methods=['GET'])
def stream_video(file_path):
@app.route('/video/<path:file_path>', methods=['GET']) try:
def stream_video(file_path): if not FILESYSTEM_ENABLED:
try: return jsonify({'error': '文件系统未启用'}), 403
if not FILESYSTEM_ENABLED:
return jsonify({'error': '文件系统未启用'}), 403 full_path = safe_path_join(HTML_ROOT_DIR, file_path)
if full_path is None or not os.path.exists(full_path) or not os.path.isfile(full_path):
full_path = safe_path_join(HTML_ROOT_DIR, file_path) return jsonify({'error': '文件不存在'}), 404
if full_path is None or not os.path.exists(full_path) or not os.path.isfile(full_path):
return jsonify({'error': '文件不存在'}), 404 if not is_video_file(full_path) and not is_audio_file(full_path):
return jsonify({'error': '不支持的文件类型'}), 400
if not is_video_file(full_path) and not is_audio_file(full_path):
return jsonify({'error': '不支持的文件类型'}), 400 file_size = os.path.getsize(full_path)
mime_type = get_media_mime_type(full_path)
file_size = os.path.getsize(full_path)
mime_type = get_media_mime_type(full_path) range_header = request.headers.get('Range', None)
range_header = request.headers.get('Range', None) if range_header:
byte_range = range_header.replace('bytes=', '').split('-')
if range_header: start = int(byte_range[0]) if byte_range[0] else 0
byte_range = range_header.replace('bytes=', '').split('-') end = int(byte_range[1]) if len(byte_range) > 1 and byte_range[1] else file_size - 1
start = int(byte_range[0]) if byte_range[0] else 0
end = int(byte_range[1]) if len(byte_range) > 1 and byte_range[1] else file_size - 1 if start >= file_size:
return jsonify({'error': '请求范围超出文件大小'}), 416
if start >= file_size:
return jsonify({'error': '请求范围超出文件大小'}), 416 length = end - start + 1
length = end - start + 1 with open(full_path, 'rb') as f:
f.seek(start)
with open(full_path, 'rb') as f: data = f.read(length)
f.seek(start)
data = f.read(length) response = Response(data, 206, mimetype=mime_type, direct_passthrough=True)
response.headers.add('Content-Range', f'bytes {start}-{end}/{file_size}')
response = Response(data, 206, mimetype=mime_type, direct_passthrough=True) response.headers.add('Accept-Ranges', 'bytes')
response.headers.add('Content-Range', f'bytes {start}-{end}/{file_size}') response.headers.add('Content-Length', str(length))
response.headers.add('Accept-Ranges', 'bytes') response.headers.add('Cache-Control', 'no-cache')
response.headers.add('Content-Length', str(length)) return response
response.headers.add('Cache-Control', 'no-cache') else:
return response return send_file(full_path, mimetype=mime_type, as_attachment=False)
else: except Exception as e:
return send_file(full_path, mimetype=mime_type, as_attachment=False) logger.error(f"Video streaming error: {str(e)}")
except Exception as e: return jsonify({'error': str(e)}), 500
logger.error(f"Video streaming error: {str(e)}")
return jsonify({'error': str(e)}), 500 GATEWAY_DB = '/opt/micro_servers/gateway/sqlite3.db'
# ==================== 主路由 ====================
@app.route('/') @app.route('/api/hash', methods=['POST'])
def serve_view(): def api_hash():
"""根路径:优先返回 index.html(社区首页),不存在时回退到 __view.html(文件浏览)""" from flask import request, jsonify
if not FILESYSTEM_ENABLED: import hashlib
return "文件系统未启用", 403 data = request.get_json(force=True)
index_path = os.path.join(HTML_ROOT_DIR, 'index.html') h = hashlib.sha256(data.get('password','').encode()).hexdigest()
if os.path.exists(index_path): return jsonify({"hash": h})
return send_file(index_path, mimetype='text/html')
view_path = os.path.join(HTML_ROOT_DIR, VIEW_FILE)
if not os.path.exists(view_path): @app.route('/api/reset_password', methods=['POST'])
return f"视图文件 {VIEW_FILE} 不存在", 500 def api_reset_password():
return send_file(view_path, mimetype='text/html') from flask import request, jsonify
import sqlite3 as _sql
@app.route('/<path:path>') data = request.get_json(force=True)
def serve_file(path): email = data.get('email', '').strip()
try: new_hash = data.get('password_hash', '').strip()
if not FILESYSTEM_ENABLED: if not email or not new_hash:
return "文件系统未启用", 403 return jsonify({"success": False, "error": "missing params"}), 400
conn = _sql.connect(GATEWAY_DB)
full_path = safe_path_join(HTML_ROOT_DIR, path) conn.execute("UPDATE Users SET password_hash=? WHERE email=?", (new_hash, email))
if full_path is None: conn.commit()
return "无效的路径", 403 conn.close()
return jsonify({"success": True})
if not os.path.exists(full_path):
return "路径不存在", 404
@app.route('/api/check_user', methods=['POST'])
# 文件处理 def api_check_user():
if os.path.isfile(full_path): from flask import request, jsonify
if request.args.get('download') == 'true': import sqlite3 as _sql
return send_file(full_path, as_attachment=True, download_name=os.path.basename(full_path)) data = request.get_json(force=True)
email = data.get('email', '').strip()
if is_video_file(full_path) or is_audio_file(full_path): if not email:
return send_file(full_path, mimetype=get_media_mime_type(full_path)) return jsonify({"success": False, "error": "missing email"}), 400
conn = _sql.connect(GATEWAY_DB)
# 普通文件 rows = conn.execute("SELECT email FROM Users WHERE email=?", (email,)).fetchall()
with open(full_path, 'rb') as f: conn.close()
content = f.read() return jsonify({"success": True, "found": len(rows) > 0})
ext = os.path.splitext(full_path)[1].lower()
mime_map = {
'.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.txt': 'text/plain', YHDET_VERIFY_URL = "https://yhdet.top/api/verify_auth_token"
'.pdf': 'application/pdf', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' YHDET_SECRET = "yhdet_qsont_oauth_2026_secret_key"
} GATEWAY_DB_YHDET = '/opt/micro_servers/gateway/sqlite3.db'
return Response(content, mimetype=mime_map.get(ext, 'application/octet-stream'))
# 目录处理:返回 __view.html,由前端 JS 根据 URL 路径调用 /api/filelist 拉取文件列表 def _yhdet_verify_token(token):
view_path = os.path.join(HTML_ROOT_DIR, VIEW_FILE) import traceback
if os.path.exists(view_path): import urllib.request, json
return send_file(view_path, mimetype='text/html') try:
return "目录无法浏览", 404 data = json.dumps({"token": token}).encode()
req = urllib.request.Request(YHDET_VERIFY_URL, data=data,
except Exception as e: headers={"Content-Type": "application/json"}, method="POST")
logger.error(f"Error: {str(e)}") with urllib.request.urlopen(req, timeout=10) as resp:
import traceback return json.loads(resp.read().decode("utf-8"))
traceback.print_exc() except Exception as e:
return f"服务器内部错误: {str(e)}", 500 traceback.print_exc()
raise
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({ @app.route('/yhdet_callback', methods=['GET', 'POST'])
'status': 'healthy', def yhdet_callback():
'filesystem': {'enabled': FILESYSTEM_ENABLED, 'root': HTML_ROOT_DIR if FILESYSTEM_ENABLED else None} 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:
if __name__ == '__main__': return make_response("No token", 400)
host = file_config['server']['host'] try:
port = int(file_config['server']['port']) result = _yhdet_verify_token(token)
if not result.get("success"):
print("\n" + "="*50) return make_response("Verify failed: " + str(result), 401)
print("📁 文件托管服务器 (极简版)") username = result.get("username", "")
print("="*50) user_id = result.get("user_id", 0)
print(f"地址: http://{host}:{port}") except Exception as e:
print(f"目录: {os.path.abspath(HTML_ROOT_DIR) if FILESYSTEM_ENABLED else '未启用'}") import traceback
print("="*50) traceback.print_exc()
print("\n快捷键: / 或 Ctrl+F 搜索, ESC 清空搜索") return make_response("Verify error: " + str(e), 500)
print("\n") email = f"yhdet_{user_id}@yhdet.oauth"
password = f"oauth_{user_id}"
if SSL_ENABLED and os.path.exists(SSL_CERT_FILE) and os.path.exists(SSL_KEY_FILE): conn = sqlite3.connect(GATEWAY_DB_YHDET)
app.run(host=host, port=port, ssl_context=(SSL_CERT_FILE, SSL_KEY_FILE), debug=False) rows = conn.execute("SELECT email FROM Users WHERE email=?", (email,)).fetchall()
else: if not rows:
app.run(host=host, port=port, debug=False) 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('/')
def serve_view():
"""根路径:优先返回 index.html(社区首页),不存在时回退到 __view.html(文件浏览)"""
if not FILESYSTEM_ENABLED:
return "文件系统未启用", 403
#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):
return send_file(index_path, mimetype='text/html')
view_path = os.path.join(HTML_ROOT_DIR, VIEW_FILE)
if not os.path.exists(view_path):
return f"视图文件 {VIEW_FILE} 不存在", 500
return send_file(view_path, mimetype='text/html')
@app.route('/<path:path>')
def serve_file(path):
try:
if not FILESYSTEM_ENABLED:
return "文件系统未启用", 403
full_path = safe_path_join(HTML_ROOT_DIR, path)
if full_path is None:
return "无效的路径", 403
if not os.path.exists(full_path):
return "路径不存在", 404
# 文件处理
if os.path.isfile(full_path):
if request.args.get('download') == 'true':
return send_file(full_path, as_attachment=True, download_name=os.path.basename(full_path))
if is_video_file(full_path) or is_audio_file(full_path):
return send_file(full_path, mimetype=get_media_mime_type(full_path))
# 普通文件
with open(full_path, 'rb') as f:
content = f.read()
ext = os.path.splitext(full_path)[1].lower()
mime_map = {
'.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.txt': 'text/plain',
'.pdf': 'application/pdf', '.svg': 'image/svg+xml', '.ico': 'image/x-icon'
}
return Response(content, mimetype=mime_map.get(ext, 'application/octet-stream'))
# 目录处理:返回 __view.html,由前端 JS 根据 URL 路径调用 /api/filelist 拉取文件列表
view_path = os.path.join(HTML_ROOT_DIR, VIEW_FILE)
if os.path.exists(view_path):
return send_file(view_path, mimetype='text/html')
return "目录无法浏览", 404
except Exception as e:
logger.error(f"Error: {str(e)}")
import traceback
traceback.print_exc()
return f"服务器内部错误: {str(e)}", 500
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({"status": "ok"})
if __name__ == '__main__':
host = file_config['server']['host']
port = int(file_config['server']['port'])
print("\n" + "="*50)
print("📁 文件托管服务器 (极简版)")
print("="*50)
print(f"地址: http://{host}:{port}")
print(f"目录: {os.path.abspath(HTML_ROOT_DIR) if FILESYSTEM_ENABLED else '未启用'}")
print("="*50)
print("\n快捷键: / 或 Ctrl+F 搜索, ESC 清空搜索")
print("\n")
if SSL_ENABLED and os.path.exists(SSL_CERT_FILE) and os.path.exists(SSL_KEY_FILE):
app.run(host=host, port=port, ssl_context=(SSL_CERT_FILE, SSL_KEY_FILE), debug=False)
else:
app.run(host=host, port=port, debug=False)
+86
View File
@@ -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>
+38 -56
View File
@@ -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> </html>
</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>
+2
View File
@@ -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
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
+12 -17
View File
@@ -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>
@@ -45,4 +40,4 @@
<script src="js/validation.js"></script> <script src="js/validation.js"></script>
<script src="js/login.js"></script> <script src="js/login.js"></script>
</body> </body>
</html> </html>