diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..cff1567 Binary files /dev/null and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/main.py b/backend/main.py index f2b230d..5dc2a60 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- """ 导航网站后端 API (Flask版本) +包含用户认证功能 """ import os import sys +import jwt +import hashlib from pathlib import Path -from datetime import datetime +from datetime import datetime, timedelta +from functools import wraps # 添加当前目录到 path BASE_DIR = Path(__file__).resolve().parent @@ -22,6 +26,11 @@ DB_USER = os.environ.get('DB_USER', 'dbuser') DB_PASS = os.environ.get('DB_PASS', 'Tata@1234') DB_NAME = os.environ.get('DB_NAME', 'favorites_nav') +# JWT 配置 +JWT_SECRET = os.environ.get('JWT_SECRET', 'easynavi-secret-key-change-in-production') +JWT_ALGORITHM = 'HS256' +JWT_EXPIRATION_HOURS = 24 * 30 # 30天过期 + app = Flask(__name__) # 简单 CORS 中间件 @@ -29,7 +38,7 @@ app = Flask(__name__) def add_cors_headers(response): response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' - response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' return response def get_db_connection(): @@ -44,6 +53,162 @@ def get_db_connection(): cursorclass=DictCursor ) +# ==================== 密码加密 ==================== + +def hash_password(password): + """哈希密码""" + return hashlib.sha256(password.encode()).hexdigest() + +def verify_password(password, password_hash): + """验证密码""" + return hashlib.sha256(password.encode()).hexdigest() == password_hash + +# ==================== JWT 工具 ==================== + +def create_token(user_id, username): + """创建 JWT token""" + payload = { + 'user_id': user_id, + 'username': username, + 'exp': datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS), + 'iat': datetime.utcnow() + } + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + +def decode_token(token): + """解码 JWT token""" + try: + return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + +def token_required(f): + """装饰器:验证 JWT token""" + @wraps(f) + def decorated(*args, **kwargs): + token = None + + # 从 header 获取 token + auth_header = request.headers.get('Authorization') + if auth_header and auth_header.startswith('Bearer '): + token = auth_header.split(' ')[1] + + if not token: + return jsonify({'error': '未登录', 'message': '请先登录'}), 401 + + payload = decode_token(token) + if not payload: + return jsonify({'error': '登录已过期', 'message': '请重新登录'}), 401 + + # 将用户信息传递给路由 + request.user_id = payload.get('user_id') + request.username = payload.get('username') + + return f(*args, **kwargs) + return decorated + +# ==================== 用户认证 API ==================== + +@app.route('/api/auth/login', methods=['POST']) +def login(): + """用户登录""" + data = request.get_json() + username = data.get('username', '').strip() + password = data.get('password', '') + + if not username or not password: + return jsonify({'error': '参数错误', 'message': '请输入用户名和密码'}), 400 + + conn = get_db_connection() + try: + with conn.cursor() as cursor: + # 查找用户 + cursor.execute( + "SELECT id, username, password_hash FROM users WHERE username = %s", + (username,) + ) + user = cursor.fetchone() + + if not user: + return jsonify({'error': '登录失败', 'message': '用户名或密码错误'}), 401 + + # 验证密码 + if not verify_password(password, user['password_hash']): + return jsonify({'error': '登录失败', 'message': '用户名或密码错误'}), 401 + + # 创建 token + token = create_token(user['id'], user['username']) + + return jsonify({ + 'token': token, + 'user_id': user['id'], + 'username': user['username'] + }) + finally: + conn.close() + +@app.route('/api/auth/register', methods=['POST']) +def register(): + """用户注册""" + data = request.get_json() + username = data.get('username', '').strip() + password = data.get('password', '') + email = data.get('email', '').strip() + + if not username or not password: + return jsonify({'error': '参数错误', 'message': '请输入用户名和密码'}), 400 + + if len(username) < 3: + return jsonify({'error': '用户名太短', 'message': '用户名至少3个字符'}), 400 + + if len(password) < 6: + return jsonify({'error': '密码太短', 'message': '密码至少6个字符'}), 400 + + conn = get_db_connection() + try: + with conn.cursor() as cursor: + # 检查用户名是否已存在 + cursor.execute("SELECT id FROM users WHERE username = %s", (username,)) + if cursor.fetchone(): + return jsonify({'error': '注册失败', 'message': '用户名已存在'}), 409 + + # 创建用户 + password_hash = hash_password(password) + cursor.execute( + "INSERT INTO users (username, password_hash, email) VALUES (%s, %s, %s)", + (username, password_hash, email) + ) + conn.commit() + user_id = cursor.lastrowid + + # 创建 token + token = create_token(user_id, username) + + return jsonify({ + 'token': token, + 'user_id': user_id, + 'username': username + }), 201 + finally: + conn.close() + +@app.route('/api/auth/profile', methods=['GET']) +@token_required +def get_profile(): + """获取当前用户信息""" + return jsonify({ + 'user_id': request.user_id, + 'username': request.username + }) + +@app.route('/api/auth/logout', methods=['POST']) +@token_required +def logout(): + """退出登录(前端删除 token 即可)""" + return jsonify({'message': '已退出登录'}) + # ==================== 分类 API ==================== @app.route('/api/categories', methods=['GET']) @@ -63,8 +228,9 @@ def get_categories(): conn.close() @app.route('/api/categories', methods=['POST']) +@token_required def create_category(): - """创建分类""" + """创建分类(需要登录)""" data = request.get_json() conn = get_db_connection() @@ -84,8 +250,9 @@ def create_category(): conn.close() @app.route('/api/categories/', methods=['PUT']) +@token_required def update_category(category_id): - """更新分类""" + """更新分类(需要登录)""" data = request.get_json() conn = get_db_connection() @@ -104,8 +271,9 @@ def update_category(category_id): conn.close() @app.route('/api/categories/', methods=['DELETE']) +@token_required def delete_category(category_id): - """删除分类""" + """删除分类(需要登录)""" conn = get_db_connection() try: with conn.cursor() as cursor: @@ -159,8 +327,9 @@ def get_websites(): conn.close() @app.route('/api/websites', methods=['POST']) +@token_required def create_website(): - """创建网站""" + """创建网站(需要登录)""" data = request.get_json() conn = get_db_connection() @@ -193,8 +362,9 @@ def create_website(): conn.close() @app.route('/api/websites/', methods=['PUT']) +@token_required def update_website(website_id): - """更新网站""" + """更新网站(需要登录)""" data = request.get_json() conn = get_db_connection() @@ -251,8 +421,9 @@ def update_website(website_id): conn.close() @app.route('/api/websites/', methods=['DELETE']) +@token_required def delete_website(website_id): - """删除网站""" + """删除网站(需要登录)""" conn = get_db_connection() try: with conn.cursor() as cursor: @@ -264,7 +435,7 @@ def delete_website(website_id): @app.route('/api/websites//click', methods=['POST']) def click_website(website_id): - """点击网站""" + """点击网站(无需登录)""" conn = get_db_connection() try: with conn.cursor() as cursor: @@ -287,5 +458,34 @@ def health_check(): def root(): return jsonify({'message': '导航网站 API', 'docs': '/api/health'}) +# ==================== 启动时创建 users 表 ==================== + +def init_db(): + """初始化数据库,创建 users 表(如果不存在)""" + try: + conn = get_db_connection() + try: + with conn.cursor() as cursor: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(64) NOT NULL, + email VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_username (username) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + """) + conn.commit() + print("Users table initialized successfully") + finally: + conn.close() + except Exception as e: + print(f"Failed to initialize users table: {e}") + +# 应用启动时初始化 +with app.app_context(): + init_db() + if __name__ == '__main__': app.run(host='0.0.0.0', port=5003, debug=False) diff --git a/frontend/dev.log b/frontend/dev.log new file mode 100644 index 0000000..8642e46 --- /dev/null +++ b/frontend/dev.log @@ -0,0 +1,11 @@ + +> favorites-nav-frontend@1.0.0 dev +> vite --host 0.0.0.0 --port 5173 + + + VITE v5.4.21 ready in 131 ms + + ➜ Local: http://localhost:5173/ + ➜ Network: http://192.168.8.207:5173/ + ➜ Network: http://192.168.8.170:5173/ + ➜ Network: http://172.17.0.1:5173/