- 后台添加登录验证(Session + JWT双重验证) - JSON存储改为SQLite数据库,解决并发问题 - API密钥移至config.py,支持环境变量覆盖 - SECRET_KEY改为随机生成 - 新增管理员登录页面 - 修复README.md乱码 - 更新.gitignore忽略敏感配置
330 lines
9.8 KiB
Python
330 lines
9.8 KiB
Python
"""
|
||
技术论坛 - 后台管理系统 (重构版 + 登录验证)
|
||
"""
|
||
|
||
from flask import Flask, render_template, jsonify, request, redirect, url_for, session, make_response
|
||
from flask_cors import CORS
|
||
import jwt
|
||
import datetime
|
||
import os
|
||
from functools import wraps
|
||
from pathlib import Path
|
||
|
||
# 导入配置和模型
|
||
import sys
|
||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||
from config import SECRET_KEY, ADMIN_USERNAME, ADMIN_PASSWORD, DATABASE_PATH, ADMIN_PORT
|
||
from models import Database, UserModel, PostModel, ReplyModel, TopicModel
|
||
|
||
app = Flask(__name__)
|
||
CORS(app)
|
||
app.secret_key = SECRET_KEY
|
||
|
||
# 初始化数据库
|
||
db = Database(DATABASE_PATH)
|
||
user_model = UserModel(db)
|
||
post_model = PostModel(db)
|
||
reply_model = ReplyModel(db)
|
||
topic_model = TopicModel(db)
|
||
|
||
# ============ 登录验证装饰器 ============
|
||
|
||
def admin_required(f):
|
||
@wraps(f)
|
||
def decorated_function(*args, **kwargs):
|
||
# 检查 session
|
||
if not session.get('admin_logged_in'):
|
||
# 检查 Authorization header
|
||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||
if token:
|
||
try:
|
||
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
|
||
if data.get('admin'):
|
||
return f(*args, **kwargs)
|
||
except:
|
||
pass
|
||
|
||
# API请求返回401,页面请求跳转登录
|
||
if request.path.startswith('/api/'):
|
||
return jsonify({'error': '请先登录', 'code': 401}), 401
|
||
return redirect('/login')
|
||
return f(*args, **kwargs)
|
||
return decorated_function
|
||
|
||
# ============ 登录相关 ============
|
||
|
||
@app.route('/login')
|
||
def login_page():
|
||
return render_template('login.html')
|
||
|
||
@app.route('/api/login', methods=['POST'])
|
||
def api_login():
|
||
data = request.json
|
||
|
||
username = data.get('username', '').strip()
|
||
password = data.get('password', '')
|
||
|
||
if not username or not password:
|
||
return jsonify({'error': '请输入用户名和密码'}), 400
|
||
|
||
if username != ADMIN_USERNAME or password != ADMIN_PASSWORD:
|
||
return jsonify({'error': '用户名或密码错误'}), 400
|
||
|
||
# 设置session
|
||
session['admin_logged_in'] = True
|
||
session['admin_username'] = username
|
||
|
||
# 生成token(可选,用于API调用)
|
||
token = jwt.encode({
|
||
'admin': True,
|
||
'username': username,
|
||
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
|
||
}, SECRET_KEY, algorithm='HS256')
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'token': token,
|
||
'message': '登录成功'
|
||
})
|
||
|
||
@app.route('/api/logout', methods=['POST'])
|
||
def api_logout():
|
||
session.pop('admin_logged_in', None)
|
||
session.pop('admin_username', None)
|
||
return jsonify({'success': True, 'message': '已退出登录'})
|
||
|
||
@app.route('/api/check-auth')
|
||
def api_check_auth():
|
||
if session.get('admin_logged_in'):
|
||
return jsonify({
|
||
'logged_in': True,
|
||
'username': session.get('admin_username')
|
||
})
|
||
return jsonify({'logged_in': False})
|
||
|
||
# ============ 页面路由 ============
|
||
|
||
@app.route('/')
|
||
@admin_required
|
||
def index():
|
||
return render_template('index.html')
|
||
|
||
@app.route('/users')
|
||
@admin_required
|
||
def users_page():
|
||
return render_template('users.html')
|
||
|
||
@app.route('/posts')
|
||
@admin_required
|
||
def posts_page():
|
||
return render_template('posts.html')
|
||
|
||
@app.route('/topics')
|
||
@admin_required
|
||
def topics_page():
|
||
return render_template('topics.html')
|
||
|
||
# ============ API路由 ============
|
||
|
||
@app.route('/api/stats')
|
||
@admin_required
|
||
def api_stats():
|
||
users = user_model.get_all()
|
||
posts, posts_total = post_model.get_all()
|
||
topics = topic_model.get_all()
|
||
|
||
# 统计回复数
|
||
total_replies = 0
|
||
for post in posts:
|
||
total_replies += len(reply_model.get_by_post(post['id']))
|
||
|
||
today = datetime.datetime.now().strftime('%Y-%m-%d')
|
||
today_posts = sum(1 for p in posts if p.get('created_at', '').startswith(today))
|
||
today_users = sum(1 for u in users if u.get('created_at', '').startswith(today))
|
||
|
||
# 帖子类型统计
|
||
discussion_count = sum(1 for p in posts if p.get('type') == 'discussion')
|
||
share_count = sum(1 for p in posts if p.get('type') == 'share')
|
||
|
||
return jsonify({
|
||
'users_count': len(users),
|
||
'posts_count': posts_total,
|
||
'topics_count': len(topics),
|
||
'messages_count': total_replies,
|
||
'today_posts': today_posts,
|
||
'today_users': today_users,
|
||
'discussion_count': discussion_count,
|
||
'share_count': share_count,
|
||
})
|
||
|
||
@app.route('/api/users')
|
||
@admin_required
|
||
def api_users():
|
||
users = user_model.get_all()
|
||
|
||
user_list = []
|
||
for user in users:
|
||
user_list.append({
|
||
'id': user['id'],
|
||
'username': user.get('username', ''),
|
||
'email': user.get('email', ''),
|
||
'phone': user.get('phone', ''),
|
||
'posts_count': user_model.get_posts_count(user['id']),
|
||
'replies_count': user_model.get_replies_count(user['id']),
|
||
'created_at': user.get('created_at', ''),
|
||
})
|
||
|
||
return jsonify(user_list)
|
||
|
||
@app.route('/api/users/<user_id>', methods=['DELETE'])
|
||
@admin_required
|
||
def api_delete_user(user_id):
|
||
user_model.delete(user_id)
|
||
return jsonify({'success': True})
|
||
|
||
@app.route('/api/posts')
|
||
@admin_required
|
||
def api_posts():
|
||
post_type = request.args.get('type')
|
||
|
||
posts, total = post_model.get_all(post_type)
|
||
|
||
post_list = []
|
||
for post in posts:
|
||
author = user_model.get_by_id(post['author_id']) or {}
|
||
post_list.append({
|
||
'id': post['id'],
|
||
'title': post['title'],
|
||
'type': post['type'],
|
||
'author': author.get('username', '未知'),
|
||
'author_id': post['author_id'],
|
||
'likes': len(post['likes']),
|
||
'replies': len(reply_model.get_by_post(post['id'])),
|
||
'views': post['views'],
|
||
'is_pinned': post['is_pinned'],
|
||
'created_at': post['created_at'],
|
||
})
|
||
|
||
return jsonify(post_list)
|
||
|
||
@app.route('/api/posts/<post_id>')
|
||
@admin_required
|
||
def api_post_detail(post_id):
|
||
post = post_model.get_by_id(post_id)
|
||
if not post:
|
||
return jsonify({'error': '帖子不存在'}), 404
|
||
|
||
author = user_model.get_by_id(post['author_id']) or {}
|
||
|
||
# 获取回复
|
||
replies = reply_model.get_by_post(post_id)
|
||
reply_list = []
|
||
for reply in replies:
|
||
reply_author = user_model.get_by_id(reply['author_id']) or {}
|
||
reply_list.append({
|
||
'id': reply['id'],
|
||
'content': reply['content'][:100] + '...' if len(reply['content']) > 100 else reply['content'],
|
||
'author': reply_author.get('username', '未知'),
|
||
'likes': len(reply['likes']),
|
||
'created_at': reply['created_at'],
|
||
})
|
||
|
||
return jsonify({
|
||
'id': post_id,
|
||
'title': post['title'],
|
||
'content': post['content'],
|
||
'type': post['type'],
|
||
'author': author.get('username', '未知'),
|
||
'tags': post['tags'],
|
||
'likes': len(post['likes']),
|
||
'replies': reply_list,
|
||
'views': post['views'],
|
||
'is_pinned': post['is_pinned'],
|
||
'created_at': post['created_at'],
|
||
})
|
||
|
||
@app.route('/api/posts/<post_id>', methods=['DELETE'])
|
||
@admin_required
|
||
def api_delete_post(post_id):
|
||
post_model.delete(post_id)
|
||
return jsonify({'success': True})
|
||
|
||
@app.route('/api/posts/<post_id>/pin', methods=['POST'])
|
||
@admin_required
|
||
def api_pin_post(post_id):
|
||
new_pin = post_model.toggle_pin(post_id)
|
||
return jsonify({
|
||
'success': True,
|
||
'is_pinned': new_pin
|
||
})
|
||
|
||
@app.route('/api/topics')
|
||
@admin_required
|
||
def api_topics():
|
||
topics = topic_model.get_all()
|
||
|
||
topic_list = []
|
||
for topic in topics:
|
||
author = user_model.get_by_id(topic['author_id']) or {}
|
||
topic_list.append({
|
||
'id': topic['id'],
|
||
'name': topic['name'],
|
||
'icon': topic.get('icon', '🔧'),
|
||
'author': author.get('username', '未知'),
|
||
'sub_topics_count': len(topic_model.get_sub_topics(topic['id'])),
|
||
'questions_count': len(topic_model.get_questions(topic['id'])),
|
||
'followers_count': len(topic['followers']),
|
||
'created_at': topic['created_at'],
|
||
})
|
||
|
||
return jsonify(topic_list)
|
||
|
||
@app.route('/api/topics/<topic_id>')
|
||
@admin_required
|
||
def api_topic_detail(topic_id):
|
||
topic = topic_model.get_by_id(topic_id)
|
||
if not topic:
|
||
return jsonify({'error': '主题不存在'}), 404
|
||
|
||
author = user_model.get_by_id(topic['author_id']) or {}
|
||
|
||
return jsonify({
|
||
'id': topic_id,
|
||
'name': topic['name'],
|
||
'description': topic.get('description', ''),
|
||
'icon': topic.get('icon', '🔧'),
|
||
'author': author.get('username', '未知'),
|
||
'sub_topics': topic_model.get_sub_topics(topic_id),
|
||
'questions': topic_model.get_questions(topic_id),
|
||
'followers_count': len(topic['followers']),
|
||
'created_at': topic['created_at'],
|
||
})
|
||
|
||
@app.route('/api/topics/<topic_id>', methods=['DELETE'])
|
||
@admin_required
|
||
def api_delete_topic(topic_id):
|
||
topic_model.delete(topic_id)
|
||
return jsonify({'success': True})
|
||
|
||
@app.route('/api/tags')
|
||
@admin_required
|
||
def api_tags():
|
||
tags = post_model.get_tags_stats()
|
||
return jsonify([{'name': t[0], 'count': t[1]} for t in tags[:20]])
|
||
|
||
# ============ 健康检查(无需登录) ============
|
||
|
||
@app.route('/api/health')
|
||
def api_health():
|
||
return jsonify({'status': 'ok', 'service': 'tech-forum-admin', 'port': ADMIN_PORT})
|
||
|
||
if __name__ == '__main__':
|
||
print("=" * 50)
|
||
print("技术论坛 - 后台管理系统")
|
||
print("=" * 50)
|
||
print(f"访问地址: http://localhost:{ADMIN_PORT}")
|
||
print(f"默认账号: {ADMIN_USERNAME}")
|
||
print(f"默认密码: {ADMIN_PASSWORD}")
|
||
print("=" * 50)
|
||
|
||
app.run(host='0.0.0.0', port=ADMIN_PORT, debug=True) |