Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/DaFuWeng.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added __pycache__/draw.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/game.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/player.cpython-312.pyc
Binary file not shown.
11 changes: 11 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from backend import create_app, db
from backend.models import User, Log, Resource, LogEntry

app = create_app()

@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Log': Log, 'Resource': Resource, 'LogEntry': LogEntry}

if __name__ == '__main__':
app.run(debug=True)
79 changes: 79 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_mail import Mail
from flask_jwt_extended import JWTManager
from .config import Config
from logstash_async.handler import AsynchronousLogstashHandler
from logstash_async.formatter import FlaskLogstashFormatter
import logging
from logging.handlers import RotatingFileHandler

# 创建扩展实例
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'info'
mail = Mail()
jwt = JWTManager()

def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)

# 确保上传文件夹存在
if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER'])

# 初始化扩展
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
jwt.init_app(app)

# 配置日志
configure_logging(app)

# 注册蓝图
from backend.main import bp as main_bp
app.register_blueprint(main_bp)

from backend.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')

from backend.logs import bp as logs_bp
app.register_blueprint(logs_bp, url_prefix='/logs')

from backend.resources import bp as resources_bp
app.register_blueprint(resources_bp, url_prefix='/resources')

return app

def configure_logging(app):
# 设置日志级别
app.logger.setLevel(logging.INFO)

# 创建文件日志处理器
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/dafuweng.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)

# 添加Logstash日志处理器
if app.config['LOGSTASH_HOST'] and app.config['LOGSTASH_PORT']:
logstash_handler = AsynchronousLogstashHandler(
host=app.config['LOGSTASH_HOST'],
port=app.config['LOGSTASH_PORT'],
database_path=None,
)
logstash_formatter = FlaskLogstashFormatter()
logstash_handler.setFormatter(logstash_formatter)
logstash_handler.setLevel(logging.INFO)
app.logger.addHandler(logstash_handler)

app.logger.info('DaFuWeng application startup')
98 changes: 98 additions & 0 deletions backend/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_user, logout_user, current_user, login_required
from werkzeug.urls import url_parse
from flask_mail import Message
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
from backend import db, mail
from backend.models import User
from backend.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm
import secrets
import string

# 创建认证蓝图
bp = Blueprint('auth', __name__)

def generate_secure_token(length=32):
"""生成安全令牌"""
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for i in range(length))

@bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
flash('无效的邮箱或密码', 'danger')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title='登录', form=form)

@bp.route('/logout')
def logout():
logout_user()
return redirect(url_for('main.index'))

@bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('恭喜!您已成功注册', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='注册', form=form)

def send_reset_email(user):
"""发送密码重置邮件"""
token = user.get_reset_token()
msg = Message('密码重置请求',
sender='[email protected]',
recipients=[user.email])
msg.body = f'''要重置您的密码,请访问以下链接:

{url_for('auth.reset_password', token=token, _external=True)}

如果您没有请求此重置,请忽略此邮件,您的密码将保持不变。
'''
mail.send(msg)

@bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
send_reset_email(user)
flash('检查您的邮箱以获取密码重置说明', 'info')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password_request.html',
title='重置密码', form=form)

@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.index'))
user = User.verify_reset_token(token)
if not user:
flash('无效或过期的令牌', 'warning')
return redirect(url_for('auth.reset_password_request'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('您的密码已成功重置', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)
20 changes: 20 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), '../app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
UPLOAD_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../uploads')
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'obj', 'fbx', 'mtl'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['[email protected]']
LOGSTASH_HOST = os.environ.get('LOGSTASH_HOST') or 'localhost'
LOGSTASH_PORT = int(os.environ.get('LOGSTASH_PORT') or 5000)
51 changes: 51 additions & 0 deletions backend/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField, SelectField
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length
from backend.models import User

class LoginForm(FlaskForm):
email = StringField('邮箱', validators=[DataRequired(), Email()])
password = PasswordField('密码', validators=[DataRequired()])
remember_me = BooleanField('记住我')
submit = SubmitField('登录')

class RegistrationForm(FlaskForm):
username = StringField('用户名', validators=[DataRequired(), Length(min=2, max=64)])
email = StringField('邮箱', validators=[DataRequired(), Email()])
password = PasswordField('密码', validators=[DataRequired(), Length(min=6)])
password2 = PasswordField(
'确认密码', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('注册')

def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('请使用其他用户名')

def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('请使用其他邮箱地址')

class ResetPasswordRequestForm(FlaskForm):
email = StringField('邮箱', validators=[DataRequired(), Email()])
submit = SubmitField('请求重置密码')

class ResetPasswordForm(FlaskForm):
password = PasswordField('新密码', validators=[DataRequired(), Length(min=6)])
password2 = PasswordField(
'确认新密码', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('重置密码')

class LogForm(FlaskForm):
title = StringField('标题', validators=[DataRequired(), Length(max=120)])
content = TextAreaField('内容', validators=[DataRequired()])
category = StringField('分类', validators=[Length(max=64)])
tags = StringField('标签', validators=[Length(max=128)])
submit = SubmitField('保存')

class ResourceForm(FlaskForm):
file = StringField('文件', validators=[DataRequired()])
file_type = SelectField('类型', choices=[('skin', '皮肤'), ('model', '人物模型')], validators=[DataRequired()])
description = StringField('描述', validators=[Length(max=256)])
submit = SubmitField('上传')
Loading