|
@@ -0,0 +1,276 @@
|
|
|
|
|
+from flask import request, g
|
|
|
|
|
+from flask_restx import Resource, Namespace, fields
|
|
|
|
|
+from app.service.user_service import UserService
|
|
|
|
|
+from app.utils.jwt_utils import JWTUtils, token_required
|
|
|
|
|
+from app.utils.logger import Logger, log_request_info, ErrorHandler
|
|
|
|
|
+from app import db
|
|
|
|
|
+
|
|
|
|
|
+# 创建认证API命名空间
|
|
|
|
|
+api = Namespace('auth', description='用户认证相关操作')
|
|
|
|
|
+
|
|
|
|
|
+# 定义API模型
|
|
|
|
|
+login_model = api.model('Login', {
|
|
|
|
|
+ 'username': fields.String(required=True, description='用户名'),
|
|
|
|
|
+ 'password': fields.String(required=True, description='密码')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+register_model = api.model('Register', {
|
|
|
|
|
+ 'username': fields.String(required=True, description='用户名'),
|
|
|
|
|
+ 'email': fields.String(required=True, description='邮箱'),
|
|
|
|
|
+ 'password': fields.String(required=True, description='密码')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+change_password_model = api.model('ChangePassword', {
|
|
|
|
|
+ 'old_password': fields.String(required=True, description='当前密码'),
|
|
|
|
|
+ 'new_password': fields.String(required=True, description='新密码')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+user_model = api.model('User', {
|
|
|
|
|
+ 'id': fields.Integer(readOnly=True, description='用户ID'),
|
|
|
|
|
+ 'username': fields.String(description='用户名'),
|
|
|
|
|
+ 'email': fields.String(description='邮箱'),
|
|
|
|
|
+ 'is_active': fields.Boolean(description='是否激活'),
|
|
|
|
|
+ 'created_at': fields.String(description='创建时间'),
|
|
|
|
|
+ 'updated_at': fields.String(description='更新时间')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+token_response_model = api.model('TokenResponse', {
|
|
|
|
|
+ 'access_token': fields.String(description='访问令牌'),
|
|
|
|
|
+ 'token_type': fields.String(description='令牌类型'),
|
|
|
|
|
+ 'expires_in': fields.Integer(description='过期时间(秒)'),
|
|
|
|
|
+ 'user': fields.Nested(user_model, description='用户信息')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+@api.route('/register')
|
|
|
|
|
+class RegisterResource(Resource):
|
|
|
|
|
+ @api.expect(register_model, validate=True)
|
|
|
|
|
+ @api.marshal_with(user_model, code=201)
|
|
|
|
|
+ @api.response(400, '注册失败')
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def post(self):
|
|
|
|
|
+ """用户注册"""
|
|
|
|
|
+ data = request.get_json()
|
|
|
|
|
+ service = UserService(db.session)
|
|
|
|
|
+
|
|
|
|
|
+ # 记录注册尝试
|
|
|
|
|
+ Logger.info("用户注册尝试", {
|
|
|
|
|
+ 'username': data.get('username'),
|
|
|
|
|
+ 'email': data.get('email'),
|
|
|
|
|
+ 'ip_address': request.remote_addr,
|
|
|
|
|
+ 'user_agent': request.headers.get('User-Agent')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ new_user = service.create_user(
|
|
|
|
|
+ username=data['username'],
|
|
|
|
|
+ email=data['email'],
|
|
|
|
|
+ password=data['password']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 记录注册成功
|
|
|
|
|
+ Logger.info("用户注册成功", {
|
|
|
|
|
+ 'user_id': new_user.id,
|
|
|
|
|
+ 'username': new_user.username,
|
|
|
|
|
+ 'email': new_user.email,
|
|
|
|
|
+ 'ip_address': request.remote_addr
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return new_user.to_dict(), 201
|
|
|
|
|
+ except ValueError as e:
|
|
|
|
|
+ # 记录注册失败
|
|
|
|
|
+ Logger.warning("用户注册失败", {
|
|
|
|
|
+ 'username': data.get('username'),
|
|
|
|
|
+ 'email': data.get('email'),
|
|
|
|
|
+ 'error': str(e),
|
|
|
|
|
+ 'ip_address': request.remote_addr
|
|
|
|
|
+ })
|
|
|
|
|
+ api.abort(400, str(e))
|
|
|
|
|
+
|
|
|
|
|
+@api.route('/login')
|
|
|
|
|
+class LoginResource(Resource):
|
|
|
|
|
+ @api.expect(login_model, validate=True)
|
|
|
|
|
+ @api.marshal_with(token_response_model)
|
|
|
|
|
+ @api.response(401, '登录失败')
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def post(self):
|
|
|
|
|
+ """用户登录"""
|
|
|
|
|
+ data = request.get_json()
|
|
|
|
|
+ service = UserService(db.session)
|
|
|
|
|
+
|
|
|
|
|
+ # 记录登录尝试
|
|
|
|
|
+ Logger.info("用户登录尝试", {
|
|
|
|
|
+ 'username': data.get('username'),
|
|
|
|
|
+ 'ip_address': request.remote_addr,
|
|
|
|
|
+ 'user_agent': request.headers.get('User-Agent')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ user = service.authenticate_user(
|
|
|
|
|
+ username=data['username'],
|
|
|
|
|
+ password=data['password']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 生成永不过期的JWT token(expires_in=0)
|
|
|
|
|
+ token = JWTUtils.generate_token(user.id, user.username, expires_in=0)
|
|
|
|
|
+
|
|
|
|
|
+ # 记录登录成功
|
|
|
|
|
+ Logger.info("用户登录成功", {
|
|
|
|
|
+ 'user_id': user.id,
|
|
|
|
|
+ 'username': user.username,
|
|
|
|
|
+ 'ip_address': request.remote_addr
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'access_token': token,
|
|
|
|
|
+ 'token_type': 'Bearer',
|
|
|
|
|
+ 'expires_in': 0,
|
|
|
|
|
+ 'user': user.to_dict_public()
|
|
|
|
|
+ }
|
|
|
|
|
+ except ValueError as e:
|
|
|
|
|
+ # 记录登录失败
|
|
|
|
|
+ Logger.warning("用户登录失败", {
|
|
|
|
|
+ 'username': data.get('username'),
|
|
|
|
|
+ 'error': str(e),
|
|
|
|
|
+ 'ip_address': request.remote_addr
|
|
|
|
|
+ })
|
|
|
|
|
+ api.abort(401, str(e))
|
|
|
|
|
+
|
|
|
|
|
+@api.route('/me')
|
|
|
|
|
+class UserProfileResource(Resource):
|
|
|
|
|
+ @token_required
|
|
|
|
|
+ @api.marshal_with(user_model)
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def get(self):
|
|
|
|
|
+ """获取当前用户信息"""
|
|
|
|
|
+ Logger.info("获取用户信息", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'username': g.current_user.username
|
|
|
|
|
+ })
|
|
|
|
|
+ return g.current_user.to_dict()
|
|
|
|
|
+
|
|
|
|
|
+ @token_required
|
|
|
|
|
+ @api.expect(api.model('UpdateProfile', {
|
|
|
|
|
+ 'username': fields.String(description='用户名'),
|
|
|
|
|
+ 'email': fields.String(description='邮箱')
|
|
|
|
|
+ }))
|
|
|
|
|
+ @api.marshal_with(user_model)
|
|
|
|
|
+ @api.response(400, '更新失败')
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def put(self):
|
|
|
|
|
+ """更新当前用户信息"""
|
|
|
|
|
+ data = request.get_json()
|
|
|
|
|
+ service = UserService(db.session)
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info("用户信息更新尝试", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'username': g.current_user.username,
|
|
|
|
|
+ 'new_username': data.get('username'),
|
|
|
|
|
+ 'new_email': data.get('email')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ updated_user = service.update_user(
|
|
|
|
|
+ user_id=g.current_user.id,
|
|
|
|
|
+ username=data.get('username'),
|
|
|
|
|
+ email=data.get('email')
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info("用户信息更新成功", {
|
|
|
|
|
+ 'user_id': updated_user.id,
|
|
|
|
|
+ 'username': updated_user.username,
|
|
|
|
|
+ 'email': updated_user.email
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return updated_user.to_dict()
|
|
|
|
|
+ except ValueError as e:
|
|
|
|
|
+ Logger.warning("用户信息更新失败", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'error': str(e)
|
|
|
|
|
+ })
|
|
|
|
|
+ api.abort(400, str(e))
|
|
|
|
|
+
|
|
|
|
|
+@api.route('/change-password')
|
|
|
|
|
+class ChangePasswordResource(Resource):
|
|
|
|
|
+ @token_required
|
|
|
|
|
+ @api.expect(change_password_model, validate=True)
|
|
|
|
|
+ @api.response(200, '密码修改成功')
|
|
|
|
|
+ @api.response(400, '密码修改失败')
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def post(self):
|
|
|
|
|
+ """修改密码"""
|
|
|
|
|
+ data = request.get_json()
|
|
|
|
|
+ service = UserService(db.session)
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info("用户密码修改尝试", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'username': g.current_user.username
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ service.change_password(
|
|
|
|
|
+ user_id=g.current_user.id,
|
|
|
|
|
+ old_password=data['old_password'],
|
|
|
|
|
+ new_password=data['new_password']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info("用户密码修改成功", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'username': g.current_user.username
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return {'message': '密码修改成功'}, 200
|
|
|
|
|
+ except ValueError as e:
|
|
|
|
|
+ Logger.warning("用户密码修改失败", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'username': g.current_user.username,
|
|
|
|
|
+ 'error': str(e)
|
|
|
|
|
+ })
|
|
|
|
|
+ api.abort(400, str(e))
|
|
|
|
|
+
|
|
|
|
|
+@api.route('/logout')
|
|
|
|
|
+class LogoutResource(Resource):
|
|
|
|
|
+ @token_required
|
|
|
|
|
+ @api.response(200, '登出成功')
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def post(self):
|
|
|
|
|
+ """用户登出(客户端需要删除token)"""
|
|
|
|
|
+ Logger.info("用户登出", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'username': g.current_user.username
|
|
|
|
|
+ })
|
|
|
|
|
+ return {'message': '登出成功'}, 200
|
|
|
|
|
+
|
|
|
|
|
+@api.route('/verify-token')
|
|
|
|
|
+class VerifyTokenResource(Resource):
|
|
|
|
|
+ @token_required
|
|
|
|
|
+ @api.marshal_with(user_model)
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def get(self):
|
|
|
|
|
+ """验证token有效性"""
|
|
|
|
|
+ Logger.info("Token验证", {
|
|
|
|
|
+ 'user_id': g.current_user.id,
|
|
|
|
|
+ 'username': g.current_user.username
|
|
|
|
|
+ })
|
|
|
|
|
+ return g.current_user.to_dict()
|
|
|
|
|
+
|
|
|
|
|
+@api.route('/permanent-token')
|
|
|
|
|
+class PermanentTokenResource(Resource):
|
|
|
|
|
+ @token_required
|
|
|
|
|
+ @api.marshal_with(token_response_model)
|
|
|
|
|
+ @log_request_info
|
|
|
|
|
+ def post(self):
|
|
|
|
|
+ """使用当前有效token换取长期有效token(无过期)"""
|
|
|
|
|
+ user = g.current_user
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info("生成长期Token", {
|
|
|
|
|
+ 'user_id': user.id,
|
|
|
|
|
+ 'username': user.username
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ token = JWTUtils.generate_token(user.id, user.username, expires_in=0)
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'access_token': token,
|
|
|
|
|
+ 'token_type': 'Bearer',
|
|
|
|
|
+ 'expires_in': 0,
|
|
|
|
|
+ 'user': user.to_dict_public()
|
|
|
|
|
+ }
|