李冠威 2 months ago
commit 803417e6bd

@ -8,12 +8,17 @@ import uuid
from datetime import datetime
from sqlalchemy.exc import OperationalError
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__, static_folder='../frontend/dist')
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_key_for_goldminer')
# 设置会话cookie的设置
app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False生产环境设为True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# 云端MySQL数据库配置
DB_USER = os.environ.get('DB_USER', 'goldminer')
@ -29,7 +34,13 @@ app.config['SQLALCHEMY_ECHO'] = True # 开启SQL查询日志方便调试
logger.info(f"连接到云端数据库: {DB_HOST}:{DB_PORT}/{DB_NAME}")
CORS(app, supports_credentials=True, origins=['*']) # Enable CORS with credentials for all origins
# 确保CORS配置正确处理凭证
CORS(app,
supports_credentials=True,
origins=['*'],
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allow_headers=['Content-Type', 'Authorization', 'X-Requested-With']) # 允许所有源的凭证请求
db = SQLAlchemy(app)
socketio = SocketIO(app, cors_allowed_origins="*", engineio_logger=True)
@ -37,6 +48,9 @@ socketio = SocketIO(app, cors_allowed_origins="*", engineio_logger=True)
active_games = {} # 用户ID -> 游戏状态
sid_to_user = {} # Socket ID -> 用户ID映射
# 添加一个简单的令牌存储
user_tokens = {} # token -> user_id 映射
# 用户模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
@ -45,6 +59,7 @@ class User(db.Model):
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime, nullable=True)
high_score = db.Column(db.Integer, default=0)
is_admin = db.Column(db.Boolean, default=False)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
@ -96,6 +111,9 @@ def init_db():
# 尝试创建表结构(如果不存在)
db.create_all()
logger.info("数据库连接成功,并且所有表都已创建(如果不存在)。")
# 检查是否需要创建默认管理员账户
create_default_admin()
except OperationalError as e:
logger.error("无法连接到云端MySQL数据库。请检查您的数据库配置和网络连接。")
logger.error(f"错误详情: {e}")
@ -107,6 +125,31 @@ def init_db():
except Exception as e:
logger.error(f"初始化数据库时发生未知错误: {e}")
# 创建默认管理员账户
def create_default_admin():
# 检查是否已有管理员账户
admin_exists = User.query.filter_by(is_admin=True).first()
if admin_exists:
logger.info("已存在管理员账户,无需创建默认管理员。")
return
# 检查是否已有admin用户名的账户
admin_user = User.query.filter_by(username="admin").first()
if admin_user:
# 将现有的admin用户升级为管理员
admin_user.is_admin = True
db.session.commit()
logger.info("已将现有admin用户升级为管理员。")
else:
# 创建新的默认管理员账户
default_admin = User(username="admin", is_admin=True)
default_admin.set_password("admin")
db.session.add(default_admin)
db.session.commit()
logger.info("已创建默认管理员账户用户名admin密码admin。在生产环境中请更改此密码")
# 注册API
@app.route('/api/register', methods=['POST'])
def register():
@ -133,9 +176,11 @@ def register():
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
logger.info(f"收到登录请求: {data.get('username') if data else 'No data'}")
# 检查必要的字段
if not data or not data.get('username') or not data.get('password'):
logger.warning("登录请求缺少用户名或密码")
return jsonify({'error': '用户名和密码是必填项'}), 400
# 查找用户
@ -143,31 +188,56 @@ def login():
# 检查密码
if user and user.check_password(data['password']):
# 生成会话ID
# 生成会话ID和令牌
session_id = str(uuid.uuid4())
token = str(uuid.uuid4())
# 存储到会话和令牌映射
session['user_id'] = user.id
session['session_id'] = session_id
user_tokens[token] = user.id
# 更新最后登录时间
user.last_login = datetime.utcnow()
db.session.commit()
logger.info(f"用户 {user.username} (ID: {user.id}) 登录成功设置会话ID: {session_id} 和令牌: {token[:8]}...")
logger.info(f"当前会话内容: {dict(session)}")
return jsonify({
'message': '登录成功',
'user': {
'id': user.id,
'username': user.username,
'high_score': user.high_score
'high_score': user.high_score,
'is_admin': user.is_admin
},
'session_id': session_id
'session_id': session_id,
'token': token
})
logger.warning(f"用户名或密码不正确: {data.get('username')}")
return jsonify({'error': '用户名或密码不正确'}), 401
# 登出API
@app.route('/api/logout', methods=['POST'])
def logout():
# 获取并清除令牌
if 'Authorization' in request.headers:
token = request.headers.get('Authorization').replace('Bearer ', '')
if token in user_tokens:
logger.info(f"清除令牌: {token[:8]}...")
del user_tokens[token]
# 获取用户ID用于日志记录
user_id = session.get('user_id')
if user_id:
logger.info(f"用户 ID: {user_id} 登出")
# 清除会话
session.clear()
logger.info("会话已清除")
return jsonify({'message': '已成功登出'})
# 获取当前用户信息
@ -186,7 +256,8 @@ def get_user():
'user': {
'id': user.id,
'username': user.username,
'high_score': user.high_score
'high_score': user.high_score,
'is_admin': user.is_admin
}
})
@ -573,6 +644,277 @@ def send_active_games_update():
# 广播活跃游戏状态
socketio.emit('active_games_update', {'active_games': active_games})
# 管理员权限验证装饰器
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': '未登录'}), 401
user = User.query.get(user_id)
if not user or not user.is_admin:
return jsonify({'error': '需要管理员权限'}), 403
return f(*args, **kwargs)
return decorated_function
# 获取所有用户信息 (管理员功能)
@app.route('/api/admin/users', methods=['GET'])
@admin_required
def get_all_users():
users = User.query.all()
users_data = []
for user in users:
users_data.append({
'id': user.id,
'username': user.username,
'high_score': user.high_score,
'is_admin': user.is_admin,
'created_at': user.created_at.isoformat() if user.created_at else None,
'last_login': user.last_login.isoformat() if user.last_login else None
})
return jsonify({'users': users_data})
# 编辑用户信息 (管理员功能)
@app.route('/api/admin/users/<int:user_id>', methods=['PUT'])
@admin_required
def update_user(user_id):
user = User.query.get(user_id)
if not user:
return jsonify({'error': '用户不存在'}), 404
data = request.get_json()
# 只允许更新特定字段
if 'username' in data and data['username'] != user.username:
# 检查用户名是否已存在
if User.query.filter_by(username=data['username']).first():
return jsonify({'error': '用户名已存在'}), 400
user.username = data['username']
if 'high_score' in data:
user.high_score = data['high_score']
if 'is_admin' in data:
# 确保至少有一个管理员账户
if not data['is_admin'] and user.is_admin:
admin_count = User.query.filter_by(is_admin=True).count()
if admin_count <= 1:
return jsonify({'error': '系统必须至少有一个管理员账户'}), 400
user.is_admin = data['is_admin']
if 'password' in data and data['password']:
user.set_password(data['password'])
db.session.commit()
# 如果修改了排行榜相关信息,发送更新事件
if 'high_score' in data:
send_leaderboard_update()
return jsonify({
'message': '用户信息已更新',
'user': {
'id': user.id,
'username': user.username,
'high_score': user.high_score,
'is_admin': user.is_admin,
'created_at': user.created_at.isoformat() if user.created_at else None,
'last_login': user.last_login.isoformat() if user.last_login else None
}
})
# 删除用户 (管理员功能)
@app.route('/api/admin/users/<int:user_id>', methods=['DELETE'])
@admin_required
def delete_user(user_id):
# 获取当前管理员
admin_id = session.get('user_id')
admin = User.query.get(admin_id)
# 防止删除自己
if user_id == admin_id:
return jsonify({'error': '不能删除自己的账户'}), 400
user = User.query.get(user_id)
if not user:
return jsonify({'error': '用户不存在'}), 404
# 删除用户的所有游戏记录
GameHistory.query.filter_by(user_id=user_id).delete()
# 删除用户的所有聊天消息
ChatMessage.query.filter_by(user_id=user_id).delete()
# 删除用户创建的聊天室
ChatRoom.query.filter_by(creator_id=user_id).delete()
# 删除用户
db.session.delete(user)
db.session.commit()
# 更新排行榜
send_leaderboard_update()
return jsonify({'message': '用户已成功删除'})
# 清理排行榜记录 (管理员功能)
@app.route('/api/admin/leaderboard/reset', methods=['POST'])
@admin_required
def reset_leaderboard():
data = request.get_json()
# 如果指定了具体用户,只重置该用户的分数
if data and 'user_id' in data:
user = User.query.get(data['user_id'])
if not user:
return jsonify({'error': '用户不存在'}), 404
user.high_score = 0
GameHistory.query.filter_by(user_id=data['user_id']).delete()
db.session.commit()
send_leaderboard_update()
return jsonify({'message': f'已重置用户 {user.username} 的分数'})
# 重置所有用户的分数
users = User.query.all()
for user in users:
user.high_score = 0
# 删除所有游戏历史记录
GameHistory.query.delete()
db.session.commit()
send_leaderboard_update()
return jsonify({'message': '所有排行榜记录已重置'})
# 修改游戏历史记录 (管理员功能)
@app.route('/api/admin/game_history', methods=['GET'])
@admin_required
def get_all_game_history():
# 获取所有游戏历史记录最多返回100条
history = GameHistory.query.order_by(GameHistory.created_at.desc()).limit(100).all()
history_data = []
for record in history:
user = User.query.get(record.user_id)
history_data.append({
'id': record.id,
'username': user.username if user else 'Unknown',
'user_id': record.user_id,
'score': record.score,
'level_reached': record.level_reached,
'duration': record.duration,
'gold_earned': record.gold_earned,
'created_at': record.created_at.isoformat()
})
return jsonify({'history': history_data})
# 删除指定的游戏记录 (管理员功能)
@app.route('/api/admin/game_history/<int:history_id>', methods=['DELETE'])
@admin_required
def delete_game_history(history_id):
record = GameHistory.query.get(history_id)
if not record:
return jsonify({'error': '记录不存在'}), 404
db.session.delete(record)
db.session.commit()
return jsonify({'message': '记录已成功删除'})
# 设置管理员 (用于初始化第一个管理员账户)
@app.route('/api/admin/setup', methods=['POST'])
def setup_admin():
# 检查是否已有管理员账户
admin_exists = User.query.filter_by(is_admin=True).first()
if admin_exists:
return jsonify({'error': '管理员账户已存在,不能再创建'}), 400
data = request.get_json()
# 检查必要的字段
if not data or not data.get('username') or not data.get('password') or not data.get('setup_key'):
return jsonify({'error': '用户名、密码和安装密钥是必填项'}), 400
# 验证安装密钥 (这应该是在环境变量中配置的一个安全密钥)
setup_key = os.environ.get('ADMIN_SETUP_KEY', 'goldminer_admin_setup_key')
if data['setup_key'] != setup_key:
return jsonify({'error': '安装密钥不正确'}), 403
# 检查用户名是否已存在
existing_user = User.query.filter_by(username=data['username']).first()
if existing_user:
# 如果用户存在,将其升级为管理员
existing_user.is_admin = True
db.session.commit()
return jsonify({'message': '用户已成功升级为管理员', 'username': existing_user.username}), 200
else:
# 创建新管理员用户
admin = User(username=data['username'], is_admin=True)
admin.set_password(data['password'])
db.session.add(admin)
db.session.commit()
return jsonify({'message': '管理员账户创建成功', 'username': admin.username}), 201
# 测试管理员API
@app.route('/api/test/admin_status', methods=['GET'])
def test_admin_status():
# 尝试从会话获取用户ID
user_id = session.get('user_id')
# 如果会话中没有用户ID尝试从令牌获取
if not user_id and 'Authorization' in request.headers:
token = request.headers.get('Authorization').replace('Bearer ', '')
user_id = user_tokens.get(token)
logger.info(f"从令牌获取用户ID: {user_id}")
logger.info(f"收到管理员状态检查请求,会话内容: {dict(session)}")
logger.info(f"请求头: {request.headers}")
if not user_id:
logger.warning("会话和令牌中都没有用户ID未登录状态")
return jsonify({
'logged_in': False,
'message': '未登录',
'session_data': dict(session),
'headers': dict(request.headers)
})
user = User.query.get(user_id)
if not user:
logger.warning(f"用户ID {user_id} 不存在")
return jsonify({
'logged_in': False,
'message': '用户不存在',
'user_id': user_id
})
logger.info(f"管理员状态检查:用户 {user.username} (ID: {user.id}) 的管理员状态为 {user.is_admin}")
return jsonify({
'logged_in': True,
'username': user.username,
'is_admin': user.is_admin,
'is_admin_type': type(user.is_admin).__name__,
'user_data': {
'id': user.id,
'username': user.username,
'high_score': user.high_score,
'is_admin': user.is_admin,
'created_at': user.created_at.isoformat() if user.created_at else None,
'last_login': user.last_login.isoformat() if user.last_login else None
}
})
if __name__ == '__main__':
# 在启动应用前初始化数据库
init_db()

@ -1,9 +1,23 @@
from app import socketio, app, init_db, DB_HOST, DB_PORT, DB_NAME
from app import app, socketio, init_db
import os
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
if __name__ == '__main__':
# 在启动应用前初始化云端数据库
print(f"连接到云端数据库: {DB_HOST}:{DB_PORT}/{DB_NAME}")
# 在启动应用前初始化数据库
init_db()
print("启动后端服务器...")
print("监听所有网络接口 (0.0.0.0:5000)")
socketio.run(app, debug=True, host='0.0.0.0', port=5000, allow_unsafe_werkzeug=True)
# 从环境变量获取主机和端口,如果没有则使用默认值
host = os.environ.get('HOST', '0.0.0.0')
port = int(os.environ.get('PORT', 5000))
logger.info(f"启动服务器,监听 {host}:{port}")
logger.info(f"SECRET_KEY设置: {app.config['SECRET_KEY'][:5]}...")
logger.info(f"SESSION_COOKIE_SECURE: {app.config['SESSION_COOKIE_SECURE']}")
logger.info(f"CORS supports_credentials: True")
# 启动应用,确保支持跨域会话
socketio.run(app, host=host, port=port, allow_unsafe_werkzeug=True)

File diff suppressed because one or more lines are too long

@ -9,12 +9,14 @@
<router-link to="/game" v-if="isLoggedIn"></router-link>
<router-link to="/highscores">排行榜</router-link>
<router-link to="/chat" v-if="isLoggedIn"></router-link>
<router-link to="/admin" v-if="isLoggedIn && isAdmin" class="admin-link"></router-link>
</nav>
<div class="user-menu">
<template v-if="isLoggedIn">
<router-link to="/profile" class="profile-link">
<span class="username">{{ username }}</span>
<span class="high-score">最高分: {{ highScore }}</span>
<span class="admin-badge" v-if="isAdmin"></span>
</router-link>
<button @click="logout" class="logout-btn">退出</button>
</template>
@ -35,7 +37,8 @@ export default {
return {
isLoggedIn: false,
username: '',
highScore: 0
highScore: 0,
isAdmin: false
}
},
created() {
@ -60,19 +63,27 @@ export default {
this.isLoggedIn = true
this.username = user.username
this.highScore = user.high_score || 0
this.isAdmin = user.is_admin === true // true
} else {
this.isLoggedIn = false
this.username = ''
this.highScore = 0
this.isAdmin = false
}
},
async logout() {
try {
await axios.post('/api/logout', {}, { withCredentials: true })
//
localStorage.removeItem('user')
localStorage.removeItem('auth_token')
delete axios.defaults.headers.common['Authorization']
this.isLoggedIn = false
this.username = ''
this.highScore = 0
this.isAdmin = false
//
if (this.$route.path !== '/login') {
@ -127,6 +138,20 @@ export default {
background-color: rgba(139, 69, 19, 0.1);
}
.admin-link {
color: #ff5722 !important;
}
.admin-badge {
display: inline-block;
background-color: #ff5722;
color: white;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
margin-left: 8px;
}
.user-menu {
display: flex;
align-items: center;

@ -2,6 +2,9 @@
<div class="login-container">
<div class="auth-form">
<h2>登录</h2>
<div class="admin-tip">
提示可使用管理员账户用户名: admin, 密码: admin
</div>
<div class="form-group">
<label for="username">用户名</label>
<input
@ -59,17 +62,56 @@ export default {
this.error = ''
try {
console.log('正在发送登录请求...')
const response = await axios.post('/api/login', {
username: this.username,
password: this.password
}, { withCredentials: true })
//
localStorage.setItem('user', JSON.stringify(response.data.user))
}, {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
}
})
console.log('登录成功,响应数据:', response.data)
//
const userData = response.data.user
const token = response.data.token
console.log('登录成功,用户数据:', userData)
//
localStorage.setItem('user', JSON.stringify(userData))
localStorage.setItem('auth_token', token)
//
this.$router.push('/highscores')
// axiosAuthorization
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
//
try {
const sessionCheck = await axios.get('/api/test/admin_status', {
withCredentials: true,
headers: {
'Authorization': `Bearer ${token}`
}
})
console.log('会话检查结果:', sessionCheck.data)
} catch (err) {
console.error('会话检查失败:', err)
}
//
if (userData.is_admin === true) {
console.log('识别为管理员用户,跳转到管理页面')
//
this.$router.push('/admin')
} else {
console.log('识别为普通用户,跳转到排行榜页面')
//
this.$router.push('/highscores')
}
} catch (error) {
console.error('登录失败:', error)
if (error.response && error.response.data) {
this.error = error.response.data.error || '登录失败,请重试'
} else {
@ -173,4 +215,14 @@ button:disabled {
.register-link a:hover {
text-decoration: underline;
}
.admin-tip {
background-color: #fff3cd;
color: #856404;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 20px;
font-size: 14px;
text-align: center;
}
</style>

@ -9,6 +9,45 @@ axios.defaults.baseURL = process.env.NODE_ENV === 'production'
? ''
: `http://${currentHost}:5000`
// 确保axios请求发送凭证(cookies)
axios.defaults.withCredentials = true
// 添加请求拦截器自动添加Authorization头
axios.interceptors.request.use(config => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, error => {
return Promise.reject(error)
})
// 检查并尝试恢复用户会话
const checkAndRestoreSession = async () => {
const userData = localStorage.getItem('user')
const token = localStorage.getItem('auth_token')
if (userData && token) {
console.log('检测到本地存储的用户会话,尝试验证...')
try {
const response = await axios.get('/api/test/admin_status')
if (!response.data.logged_in) {
console.log('会话已失效,清除本地存储')
localStorage.removeItem('user')
localStorage.removeItem('auth_token')
} else {
console.log('会话有效,用户已登录')
}
} catch (error) {
console.error('验证会话失败:', error)
}
}
}
// 启动应用时检查会话
checkAndRestoreSession()
const app = createApp(App)
app.use(router)
app.mount('#app')

@ -6,6 +6,8 @@ import GameComponent from './components/Game.vue'
import UserProfile from './components/UserProfile.vue'
import Leaderboard from './components/Leaderboard.vue'
import ChatRoom from './components/ChatRoom.vue'
import Admin from './components/Admin.vue'
import AdminSetup from './components/AdminSetup.vue'
const routes = [
{
@ -45,6 +47,17 @@ const routes = [
name: 'ChatRoom',
component: ChatRoom,
meta: { requiresAuth: true }
},
{
path: '/admin',
name: 'Admin',
component: Admin,
meta: { requiresAuth: true, requiresAdmin: true }
},
{
path: '/admin/setup',
name: 'AdminSetup',
component: AdminSetup
}
]
@ -56,11 +69,29 @@ const router = createRouter({
// 全局路由守卫,检查登录状态
router.beforeEach((to, from, next) => {
const isLoggedIn = !!localStorage.getItem('user')
const user = JSON.parse(localStorage.getItem('user') || '{}')
const isAdmin = user && user.is_admin === true // 明确比较为true
console.log('路由守卫检查:', {
to: to.path,
isLoggedIn,
isAdmin,
requiresAuth: to.matched.some(record => record.meta.requiresAuth),
requiresAdmin: to.matched.some(record => record.meta.requiresAdmin)
})
// 如果需要登录但用户未登录,重定向到登录页面
if (to.matched.some(record => record.meta.requiresAuth) && !isLoggedIn) {
console.log('未登录,重定向到登录页面')
next({ name: 'Login' })
} else {
}
// 如果需要管理员权限但用户不是管理员,重定向到首页
else if (to.matched.some(record => record.meta.requiresAdmin) && !isAdmin) {
console.log('非管理员访问管理页面,重定向到首页')
next({ name: 'Home' })
}
else {
console.log('允许访问:', to.path)
next()
}
})

Loading…
Cancel
Save