Files
tech-forum/admin/app.py
hubian cb4b7d5363 feat: v1.1.0 安全重构
- 后台添加登录验证(Session + JWT双重验证)
- JSON存储改为SQLite数据库,解决并发问题
- API密钥移至config.py,支持环境变量覆盖
- SECRET_KEY改为随机生成
- 新增管理员登录页面
- 修复README.md乱码
- 更新.gitignore忽略敏感配置
2026-04-12 16:56:35 +08:00

330 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
技术论坛 - 后台管理系统 (重构版 + 登录验证)
"""
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)