feat: 登录验证 + 邀请码注册 + 用量监控后台

- 新增用户认证系统(登录/登出/邀请码注册)
- 新增管理后台(邀请码管理/用户管理/使用记录/用量统计)
- 合成接口加登录验证,每次调用记录用户和稿件内容
- SQLite存储用户数据和使用日志
- 默认admin账号: simonkoson

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
lanhao
2026-07-02 18:51:23 +08:00
parent 3b6019d0f2
commit 223761e717
10 changed files with 1304 additions and 27 deletions
+150
View File
@@ -0,0 +1,150 @@
from flask import Blueprint, request, jsonify, session
from werkzeug.security import generate_password_hash, check_password_hash
from app.db import get_db
import functools
bp = Blueprint('auth', __name__, url_prefix='/api')
def login_required(f):
"""登录验证装饰器"""
@functools.wraps(f)
def wrapped(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': '未登录,请先登录'}), 401
return f(*args, **kwargs)
return wrapped
def admin_required(f):
"""管理员验证装饰器"""
@functools.wraps(f)
def wrapped(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': '未登录'}), 401
if session.get('role') != 'admin':
return jsonify({'error': '无权限,仅管理员可操作'}), 403
return f(*args, **kwargs)
return wrapped
@bp.route('/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': '用户名和密码不能为空'}), 400
conn = get_db()
user = conn.execute(
'SELECT * FROM users WHERE username = ?', (username,)
).fetchone()
conn.close()
if not user or not check_password_hash(user['password_hash'], password):
return jsonify({'error': '用户名或密码错误'}), 401
if not user['is_active']:
return jsonify({'error': '账号已被停用,请联系管理员'}), 403
session.clear()
session['user_id'] = user['id']
session['username'] = user['username']
session['display_name'] = user['display_name']
session['role'] = user['role']
session.permanent = True
return jsonify({
'success': True,
'user': {
'id': user['id'],
'username': user['username'],
'display_name': user['display_name'],
'role': user['role']
}
})
@bp.route('/logout', methods=['POST'])
def logout():
session.clear()
return jsonify({'success': True})
@bp.route('/me', methods=['GET'])
@login_required
def me():
return jsonify({
'user': {
'id': session['user_id'],
'username': session['username'],
'display_name': session['display_name'],
'role': session['role']
}
})
@bp.route('/register', methods=['POST'])
def register():
"""邀请码注册"""
data = request.get_json()
invite_code = data.get('invite_code', '').strip()
username = data.get('username', '').strip()
password = data.get('password', '')
display_name = data.get('display_name', '').strip()
if not invite_code or not username or not password:
return jsonify({'error': '邀请码、用户名、密码均不能为空'}), 400
if len(username) < 2 or len(username) > 20:
return jsonify({'error': '用户名长度2-20个字符'}), 400
if len(password) < 6:
return jsonify({'error': '密码至少6位'}), 400
conn = get_db()
try:
# 验证邀请码
invite = conn.execute(
'SELECT * FROM invite_codes WHERE code = ? AND is_used = 0',
(invite_code,)
).fetchone()
if not invite:
return jsonify({'error': '邀请码无效或已被使用'}), 400
# 检查用户名是否已存在
existing = conn.execute(
'SELECT id FROM users WHERE username = ?', (username,)
).fetchone()
if existing:
return jsonify({'error': '用户名已存在,请换一个'}), 400
# 如果用户没填显示名,用邀请码绑定的编导姓名
if not display_name:
display_name = invite['editor_name']
# 创建用户
cursor = conn.execute(
'INSERT INTO users (username, display_name, password_hash, role) VALUES (?, ?, ?, ?)',
(username, display_name, generate_password_hash(password), 'editor')
)
user_id = cursor.lastrowid
# 标记邀请码已使用
conn.execute(
'UPDATE invite_codes SET is_used = 1, used_by_user_id = ?, used_at = CURRENT_TIMESTAMP WHERE id = ?',
(user_id, invite['id'])
)
conn.commit()
return jsonify({
'success': True,
'message': f'注册成功!欢迎 {display_name}'
})
except Exception as e:
conn.rollback()
return jsonify({'error': f'注册失败: {str(e)}'}), 500
finally:
conn.close()