Browse Source

first commit

root 1 month ago
commit
64745d5c7c
70 changed files with 5805 additions and 0 deletions
  1. 23
    0
      .env
  2. 4
    0
      .vscode/settings.json
  3. 1
    0
      README.md
  4. BIN
      __pycache__/main.cpython-311.pyc
  5. 0
    0
      app/__init__.py
  6. BIN
      app/__pycache__/__init__.cpython-311.pyc
  7. BIN
      app/__pycache__/config.cpython-311.pyc
  8. BIN
      app/__pycache__/database.cpython-311.pyc
  9. BIN
      app/__pycache__/logging_config.cpython-311.pyc
  10. BIN
      app/__pycache__/main.cpython-311.pyc
  11. 0
    0
      app/api/__init__.py
  12. BIN
      app/api/__pycache__/__init__.cpython-311.pyc
  13. 0
    0
      app/api/v1/__init__.py
  14. BIN
      app/api/v1/__pycache__/__init__.cpython-311.pyc
  15. BIN
      app/api/v1/__pycache__/admin.cpython-311.pyc
  16. BIN
      app/api/v1/__pycache__/auth.cpython-311.pyc
  17. BIN
      app/api/v1/__pycache__/users.cpython-311.pyc
  18. BIN
      app/api/v1/__pycache__/verify.cpython-311.pyc
  19. 163
    0
      app/api/v1/admin.py
  20. 330
    0
      app/api/v1/auth.py
  21. 263
    0
      app/api/v1/users.py
  22. 0
    0
      app/api/v1/verify.py
  23. 44
    0
      app/config.py
  24. 38
    0
      app/core/__init__.py
  25. BIN
      app/core/__pycache__/__init__.cpython-311.pyc
  26. BIN
      app/core/__pycache__/security.cpython-311.pyc
  27. 91
    0
      app/core/auth.py
  28. 159
    0
      app/core/email.py
  29. 0
    0
      app/core/permissions.py
  30. 394
    0
      app/core/security.py
  31. 64
    0
      app/database.py
  32. 0
    0
      app/dependencies/__init__.py
  33. BIN
      app/dependencies/__pycache__/__init__.cpython-311.pyc
  34. BIN
      app/dependencies/__pycache__/auth.cpython-311.pyc
  35. BIN
      app/dependencies/__pycache__/database.cpython-311.pyc
  36. 110
    0
      app/dependencies/auth.py
  37. 15
    0
      app/dependencies/database.py
  38. 89
    0
      app/logging_config.py
  39. 0
    0
      app/logs/__init__.py
  40. 2892
    0
      app/logs/app.log
  41. 83
    0
      app/main.py
  42. 0
    0
      app/models/__init__.py
  43. BIN
      app/models/__pycache__/__init__.cpython-311.pyc
  44. BIN
      app/models/__pycache__/token.cpython-311.pyc
  45. BIN
      app/models/__pycache__/user.cpython-311.pyc
  46. 0
    0
      app/models/role.py
  47. 31
    0
      app/models/token.py
  48. 39
    0
      app/models/user.py
  49. 42
    0
      app/schemas/__init__.py
  50. BIN
      app/schemas/__pycache__/__init__.cpython-311.pyc
  51. BIN
      app/schemas/__pycache__/auth.cpython-311.pyc
  52. BIN
      app/schemas/__pycache__/token.cpython-311.pyc
  53. BIN
      app/schemas/__pycache__/user.cpython-311.pyc
  54. 41
    0
      app/schemas/auth.py
  55. 48
    0
      app/schemas/token.py
  56. 68
    0
      app/schemas/user.py
  57. 0
    0
      app/services/__init__.py
  58. BIN
      app/services/__pycache__/__init__.cpython-311.pyc
  59. BIN
      app/services/__pycache__/auth_service.cpython-311.pyc
  60. BIN
      app/services/__pycache__/user_service.cpython-311.pyc
  61. 498
    0
      app/services/auth_service.py
  62. 0
    0
      app/services/email_service.py
  63. 169
    0
      app/services/user_service.py
  64. 0
    0
      app/utils/__init__.py
  65. 0
    0
      app/utils/jwt.py
  66. 0
    0
      app/utils/validators.py
  67. BIN
      caiyouhui.db
  68. 13
    0
      init_db.py
  69. 7
    0
      requirements.txt
  70. 86
    0
      templates/email/verify_email.html

+ 23
- 0
.env View File

@@ -0,0 +1,23 @@
1
+# CaiYouHui_backend/.env
2
+# 项目配置
3
+PROJECT_NAME=CaiYouHui 采油会
4
+VERSION=1.0.0
5
+API_V1_PREFIX=/api/v1
6
+
7
+# 安全配置
8
+SECRET_KEY=your-super-secret-key-change-in-production-123456
9
+ALGORITHM=HS256
10
+ACCESS_TOKEN_EXPIRE_MINUTES=1440  # 24小时
11
+
12
+# 数据库配置
13
+DATABASE_URL=sqlite:///./caiyouhui.db
14
+
15
+# CORS 配置
16
+BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:8080
17
+
18
+# 调试模式
19
+DEBUG=True
20
+
21
+# 文件上传
22
+UPLOAD_DIR=./uploads
23
+MAX_UPLOAD_SIZE=10485760  # 10MB

+ 4
- 0
.vscode/settings.json View File

@@ -0,0 +1,4 @@
1
+{
2
+    "python-envs.defaultEnvManager": "ms-python.python:system",
3
+    "python-envs.pythonProjects": []
4
+}

+ 1
- 0
README.md View File

@@ -0,0 +1 @@
1
+这是CaiYouHui的fastapi后端实现

BIN
__pycache__/main.cpython-311.pyc View File


+ 0
- 0
app/__init__.py View File


BIN
app/__pycache__/__init__.cpython-311.pyc View File


BIN
app/__pycache__/config.cpython-311.pyc View File


BIN
app/__pycache__/database.cpython-311.pyc View File


BIN
app/__pycache__/logging_config.cpython-311.pyc View File


BIN
app/__pycache__/main.cpython-311.pyc View File


+ 0
- 0
app/api/__init__.py View File


BIN
app/api/__pycache__/__init__.cpython-311.pyc View File


+ 0
- 0
app/api/v1/__init__.py View File


BIN
app/api/v1/__pycache__/__init__.cpython-311.pyc View File


BIN
app/api/v1/__pycache__/admin.cpython-311.pyc View File


BIN
app/api/v1/__pycache__/auth.cpython-311.pyc View File


BIN
app/api/v1/__pycache__/users.cpython-311.pyc View File


BIN
app/api/v1/__pycache__/verify.cpython-311.pyc View File


+ 163
- 0
app/api/v1/admin.py View File

@@ -0,0 +1,163 @@
1
+# app/api/admin.py
2
+from fastapi import APIRouter, Depends, HTTPException
3
+from fastapi.responses import JSONResponse
4
+from sqlalchemy.orm import Session
5
+from sqlalchemy import text
6
+import json
7
+
8
+from ..dependencies.auth import get_current_user
9
+from ..models.user import User
10
+from ..database import get_db
11
+
12
+router = APIRouter(prefix="/admin", tags=["admin"])
13
+
14
+@router.get("/database/tables")
15
+async def get_database_tables(
16
+    db: Session = Depends(get_db),
17
+    current_user: User = Depends(get_current_user)
18
+):
19
+    """获取所有表(需要管理员权限)"""
20
+    if not current_user.is_superuser:
21
+        raise HTTPException(status_code=403, detail="需要管理员权限")
22
+    
23
+    try:
24
+        # 使用 SQLAlchemy 执行原生 SQL
25
+        result = db.execute(text("""
26
+            SELECT name, type, sql 
27
+            FROM sqlite_master 
28
+            WHERE type='table' 
29
+            AND name NOT LIKE 'sqlite_%'
30
+            ORDER BY name;
31
+        """))
32
+        
33
+        tables = []
34
+        for row in result:
35
+            tables.append({
36
+                "name": row[0],
37
+                "type": row[1],
38
+                "sql": row[2]
39
+            })
40
+        
41
+        return JSONResponse(content={"tables": tables})
42
+    
43
+    except Exception as e:
44
+        raise HTTPException(status_code=500, detail=str(e))
45
+
46
+@router.get("/database/table/{table_name}")
47
+async def get_table_data(
48
+    table_name: str,
49
+    limit: int = 100,
50
+    db: Session = Depends(get_db),
51
+    current_user: User = Depends(get_current_user)
52
+):
53
+    """获取表数据(需要管理员权限)"""
54
+    if not current_user.is_superuser:
55
+        raise HTTPException(status_code=403, detail="需要管理员权限")
56
+    
57
+    try:
58
+        # 先验证表存在且可访问
59
+        result = db.execute(text(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}';"))
60
+        if not result.fetchone():
61
+            raise HTTPException(status_code=404, detail="表不存在")
62
+        
63
+        # 获取表结构
64
+        columns_result = db.execute(text(f"PRAGMA table_info({table_name});"))
65
+        columns = []
66
+        for row in columns_result:
67
+            columns.append({
68
+                "cid": row[0],
69
+                "name": row[1],
70
+                "type": row[2],
71
+                "notnull": bool(row[3]),
72
+                "default_value": row[4],
73
+                "pk": bool(row[5])
74
+            })
75
+        
76
+        # 获取数据
77
+        data_result = db.execute(text(f"SELECT * FROM {table_name} LIMIT {limit};"))
78
+        
79
+        # 获取列名
80
+        column_names = [desc[0] for desc in data_result.cursor.description]
81
+        
82
+        # 获取数据行
83
+        rows = []
84
+        for row in data_result:
85
+            row_dict = {}
86
+            for i, value in enumerate(row):
87
+                # 处理特殊类型(如 datetime)
88
+                if hasattr(value, 'isoformat'):
89
+                    row_dict[column_names[i]] = value.isoformat()
90
+                else:
91
+                    row_dict[column_names[i]] = value
92
+            rows.append(row_dict)
93
+        
94
+        # 统计总数
95
+        count_result = db.execute(text(f"SELECT COUNT(*) FROM {table_name};"))
96
+        total_count = count_result.scalar()
97
+        
98
+        return JSONResponse(content={
99
+            "table": table_name,
100
+            "columns": columns,
101
+            "data": rows,
102
+            "total_count": total_count,
103
+            "showing": len(rows)
104
+        })
105
+    
106
+    except HTTPException:
107
+        raise
108
+    except Exception as e:
109
+        raise HTTPException(status_code=500, detail=str(e))
110
+
111
+@router.post("/database/query")
112
+async def execute_custom_query(
113
+    query: str,
114
+    db: Session = Depends(get_db),
115
+    current_user: User = Depends(get_current_user)
116
+):
117
+    """执行自定义查询(需要管理员权限,生产环境请谨慎)"""
118
+    if not current_user.is_superuser:
119
+        raise HTTPException(status_code=403, detail="需要管理员权限")
120
+    
121
+    # 安全限制:不允许修改操作
122
+    query_lower = query.strip().lower()
123
+    dangerous_keywords = ["drop", "delete", "update", "insert", "alter", "truncate"]
124
+    
125
+    if any(keyword in query_lower for keyword in dangerous_keywords):
126
+        raise HTTPException(status_code=400, detail="只允许SELECT查询")
127
+    
128
+    try:
129
+        result = db.execute(text(query))
130
+        
131
+        # 如果是查询语句
132
+        if query_lower.startswith("select"):
133
+            # 获取列名
134
+            column_names = [desc[0] for desc in result.cursor.description]
135
+            
136
+            # 获取数据
137
+            rows = []
138
+            for row in result:
139
+                row_dict = {}
140
+                for i, value in enumerate(row):
141
+                    if hasattr(value, 'isoformat'):
142
+                        row_dict[column_names[i]] = value.isoformat()
143
+                    else:
144
+                        row_dict[column_names[i]] = value
145
+                rows.append(row_dict)
146
+            
147
+            return JSONResponse(content={
148
+                "query": query,
149
+                "columns": column_names,
150
+                "data": rows,
151
+                "row_count": len(rows)
152
+            })
153
+        else:
154
+            # 非查询语句
155
+            db.commit()
156
+            return JSONResponse(content={
157
+                "query": query,
158
+                "affected_rows": result.rowcount,
159
+                "message": "执行成功"
160
+            })
161
+    
162
+    except Exception as e:
163
+        raise HTTPException(status_code=500, detail=str(e))

+ 330
- 0
app/api/v1/auth.py View File

@@ -0,0 +1,330 @@
1
+# app/api/v1/auth.py
2
+from fastapi import APIRouter, HTTPException, status, Request, Depends
3
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
4
+from sqlalchemy.orm import Session
5
+from typing import Optional
6
+
7
+from app.schemas.user import UserCreate, UserLogin
8
+from app.schemas.token import TokenResponse, RefreshTokenRequest
9
+from app.core.security import (
10
+    create_access_token,
11
+    JWTManager,
12
+    verify_access_token
13
+)
14
+from app.config import settings
15
+from app.database import get_db
16
+from app.models.user import User
17
+from app.services.user_service import UserService
18
+
19
+router = APIRouter(prefix="/auth", tags=["认证"])
20
+security = HTTPBearer()
21
+
22
+# 初始化 JWT 管理器
23
+jwt_manager = JWTManager(
24
+    secret_key=settings.SECRET_KEY,
25
+    algorithm=settings.ALGORITHM
26
+)
27
+
28
+import logging
29
+logger = logging.getLogger(__name__)
30
+
31
+@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
32
+async def register(
33
+    user_data: UserCreate,
34
+    request: Request,
35
+    db: Session = Depends(get_db)
36
+):
37
+    """用户注册"""
38
+    user_service = UserService(db)
39
+    
40
+    try:
41
+        # 创建用户
42
+        user = await user_service.create_user(user_data)
43
+        
44
+        # 创建访问令牌
45
+        access_token = create_access_token(
46
+            data={
47
+                "sub": user.username,
48
+                "user_id": user.id,
49
+                "email": user.email,
50
+                "type": "access"
51
+            },
52
+            secret_key=settings.SECRET_KEY,
53
+            expires_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
54
+        )
55
+        
56
+        # 创建刷新令牌
57
+        refresh_token = jwt_manager.create_refresh_token(
58
+            {
59
+                "sub": user.username,
60
+                "user_id": user.id,
61
+                "type": "refresh"
62
+            },
63
+            expires_days=7
64
+        )
65
+        
66
+        # 构建用户响应
67
+        user_response = {
68
+            "id": user.id,
69
+            "username": user.username,
70
+            "email": user.email,
71
+            "full_name": user.full_name,
72
+            "is_active": user.is_active,
73
+            "is_verified": user.is_verified,
74
+            "is_superuser": user.is_superuser,
75
+            "created_at": user.created_at.isoformat() if user.created_at else None
76
+        }
77
+        
78
+        return TokenResponse(
79
+            access_token=access_token,
80
+            token_type="bearer",
81
+            expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
82
+            refresh_token=refresh_token,
83
+            user=user_response
84
+        )
85
+        
86
+    except ValueError as e:
87
+        raise HTTPException(
88
+            status_code=status.HTTP_400_BAD_REQUEST,
89
+            detail=str(e)
90
+        )
91
+    except Exception as e:
92
+        raise HTTPException(
93
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
94
+            detail=f"注册失败: {str(e)}"
95
+        )
96
+
97
+@router.post("/login", response_model=TokenResponse)
98
+async def login(
99
+    login_data: UserLogin,
100
+    request: Request,
101
+    db: Session = Depends(get_db)
102
+):
103
+    """用户登录"""
104
+    user_service = UserService(db)
105
+
106
+    logger.info("✅ 用户登录")
107
+    
108
+    # 验证用户
109
+    user = await user_service.authenticate_user(
110
+        login_data.username,
111
+        login_data.password
112
+    )
113
+    
114
+    if not user:
115
+        raise HTTPException(
116
+            status_code=status.HTTP_401_UNAUTHORIZED,
117
+            detail="用户名或密码错误"
118
+        )
119
+    
120
+    if not user.is_active:
121
+        raise HTTPException(
122
+            status_code=status.HTTP_403_FORBIDDEN,
123
+            detail="用户账户已被禁用"
124
+        )
125
+    
126
+    if user.is_locked:
127
+        raise HTTPException(
128
+            status_code=status.HTTP_423_LOCKED,
129
+            detail="账户已被锁定,请联系管理员"
130
+        )
131
+    
132
+    # 创建访问令牌
133
+    access_token = jwt_manager.create_access_token(
134
+        {
135
+            "sub": user.username,
136
+            "user_id": user.id,
137
+            "email": user.email,
138
+            "type": "access"
139
+        },
140
+        expires_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
141
+    )
142
+    
143
+    # 创建刷新令牌
144
+    refresh_token = jwt_manager.create_refresh_token(
145
+        {
146
+            "sub": user.username,
147
+            "user_id": user.id,
148
+            "type": "refresh"
149
+        },
150
+        expires_days=7
151
+    )
152
+    
153
+    # 构建用户响应
154
+    user_response = {
155
+        "id": user.id,
156
+        "username": user.username,
157
+        "email": user.email,
158
+        "full_name": user.full_name,
159
+        "is_active": user.is_active,
160
+        "is_verified": user.is_verified,
161
+        "is_superuser": user.is_superuser,
162
+        "created_at": user.created_at.isoformat() if user.created_at else None,
163
+        "last_login": user.last_login.isoformat() if user.last_login else None
164
+    }
165
+    
166
+    return TokenResponse(
167
+        access_token=access_token,
168
+        token_type="bearer",
169
+        expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
170
+        refresh_token=refresh_token,
171
+        user=user_response
172
+    )
173
+
174
+@router.get("/me")
175
+async def get_current_user(
176
+    credentials: HTTPAuthorizationCredentials = Depends(security),
177
+    db: Session = Depends(get_db)
178
+):
179
+    """获取当前用户信息"""
180
+    token = credentials.credentials
181
+
182
+    logger.info("✅ 获取当前用户信息")
183
+    
184
+    # 验证令牌
185
+    payload = verify_access_token(token, settings.SECRET_KEY)
186
+    if not payload:
187
+        raise HTTPException(
188
+            status_code=status.HTTP_401_UNAUTHORIZED,
189
+            detail="无效的令牌"
190
+        )
191
+    
192
+    username = payload.get("sub")
193
+    user_service = UserService(db)
194
+    user = await user_service.get_user_by_username(username)
195
+    
196
+    if not user:
197
+        raise HTTPException(
198
+            status_code=status.HTTP_404_NOT_FOUND,
199
+            detail="用户不存在"
200
+        )
201
+    
202
+    if not user.is_active:
203
+        raise HTTPException(
204
+            status_code=status.HTTP_403_FORBIDDEN,
205
+            detail="用户账户已被禁用"
206
+        )
207
+    
208
+    return {
209
+        "id": user.id,
210
+        "username": user.username,
211
+        "email": user.email,
212
+        "full_name": user.full_name,
213
+        "is_active": user.is_active,
214
+        "is_verified": user.is_verified,
215
+        "is_superuser": user.is_superuser,
216
+        "created_at": user.created_at.isoformat() if user.created_at else None,
217
+        "last_login": user.last_login.isoformat() if user.last_login else None,
218
+        "avatar": user.avatar
219
+    }
220
+
221
+@router.post("/refresh")
222
+async def refresh_token(
223
+    refresh_data: RefreshTokenRequest,
224
+    db: Session = Depends(get_db)
225
+):
226
+    """刷新访问令牌"""
227
+    refresh_token = refresh_data.refresh_token
228
+    
229
+    if not refresh_token:
230
+        raise HTTPException(
231
+            status_code=status.HTTP_400_BAD_REQUEST,
232
+            detail="缺少刷新令牌"
233
+        )
234
+    
235
+    # 验证刷新令牌
236
+    payload = jwt_manager.verify_token(refresh_token)
237
+    
238
+    if not payload or payload.get("type") != "refresh":
239
+        raise HTTPException(
240
+            status_code=status.HTTP_401_UNAUTHORIZED,
241
+            detail="无效的刷新令牌"
242
+        )
243
+    
244
+    username = payload.get("sub")
245
+    user_service = UserService(db)
246
+    user = await user_service.get_user_by_username(username)
247
+    
248
+    if not user:
249
+        raise HTTPException(
250
+            status_code=status.HTTP_404_NOT_FOUND,
251
+            detail="用户不存在"
252
+        )
253
+    
254
+    if not user.is_active:
255
+        raise HTTPException(
256
+            status_code=status.HTTP_403_FORBIDDEN,
257
+            detail="用户账户已被禁用"
258
+        )
259
+    
260
+    # 创建新的访问令牌
261
+    access_token = jwt_manager.create_access_token(
262
+        {
263
+            "sub": user.username,
264
+            "user_id": user.id,
265
+            "email": user.email,
266
+            "type": "access"
267
+        },
268
+        expires_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
269
+    )
270
+    
271
+    return {
272
+        "access_token": access_token,
273
+        "token_type": "bearer",
274
+        "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
275
+    }
276
+
277
+@router.post("/logout")
278
+async def logout(
279
+    refresh_data: RefreshTokenRequest,
280
+    db: Session = Depends(get_db),
281
+    credentials: HTTPAuthorizationCredentials = Depends(security)
282
+):
283
+    """用户登出"""
284
+    # 这里可以记录令牌到黑名单,或者简单返回成功
285
+    # 在实际应用中,可能需要将令牌存储到Redis黑名单
286
+    return {"message": "登出成功"}
287
+
288
+@router.get("/test-db")
289
+async def test_database(db: Session = Depends(get_db)):
290
+    """测试数据库连接和用户统计"""
291
+    user_service = UserService(db)
292
+    
293
+    user_count = await user_service.count_users()
294
+    all_users = await user_service.list_users(limit=10)
295
+    
296
+    users_info = []
297
+    for user in all_users:
298
+        users_info.append({
299
+            "id": user.id,
300
+            "username": user.username,
301
+            "email": user.email,
302
+            "is_active": user.is_active
303
+        })
304
+    
305
+    return {
306
+        "database": "SQLite",
307
+        "user_count": user_count,
308
+        "users": users_info,
309
+        "message": "数据库连接正常"
310
+    }
311
+
312
+@router.get("/test")
313
+async def test_auth():
314
+    """测试认证 API"""
315
+    return {
316
+        "message": "认证 API 正常运行(使用 SQLite 数据库)",
317
+        "endpoints": {
318
+            "POST /register": "用户注册",
319
+            "POST /login": "用户登录",
320
+            "GET /me": "获取当前用户",
321
+            "POST /refresh": "刷新令牌",
322
+            "POST /logout": "用户登出",
323
+            "GET /test-db": "测试数据库",
324
+            "GET /test": "测试接口"
325
+        },
326
+        "test_credentials": {
327
+            "username": "admin",
328
+            "password": "Admin123!"
329
+        }
330
+    }

+ 263
- 0
app/api/v1/users.py View File

@@ -0,0 +1,263 @@
1
+# app/api/v1/users.py
2
+from fastapi import APIRouter, Depends, HTTPException, status, Query
3
+from sqlalchemy.orm import Session
4
+from typing import List, Optional
5
+from datetime import datetime
6
+
7
+from app.schemas.user import UserProfile, UserUpdate, PasswordChange
8
+from app.database import get_db
9
+from app.services.user_service import UserService
10
+from app.core.security import password_validator
11
+
12
+# 依赖:获取当前用户
13
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
14
+from app.core.security import verify_access_token
15
+from app.config import settings
16
+
17
+router = APIRouter(prefix="/users", tags=["用户管理"])
18
+security = HTTPBearer()
19
+
20
+def get_current_user(
21
+    credentials: HTTPAuthorizationCredentials = Depends(security),
22
+    db: Session = Depends(get_db)
23
+):
24
+    """获取当前用户依赖"""
25
+    token = credentials.credentials
26
+    
27
+    # 验证令牌
28
+    payload = verify_access_token(token, settings.SECRET_KEY)
29
+    if not payload:
30
+        raise HTTPException(
31
+            status_code=status.HTTP_401_UNAUTHORIZED,
32
+            detail="无效的令牌"
33
+        )
34
+    
35
+    username = payload.get("sub")
36
+    user_service = UserService(db)
37
+    user = user_service.get_user_by_username(username)
38
+    
39
+    if not user:
40
+        raise HTTPException(
41
+            status_code=status.HTTP_404_NOT_FOUND,
42
+            detail="用户不存在"
43
+        )
44
+    
45
+    if not user.is_active:
46
+        raise HTTPException(
47
+            status_code=status.HTTP_403_FORBIDDEN,
48
+            detail="用户账户已被禁用"
49
+        )
50
+    
51
+    return user
52
+
53
+@router.get("/me", response_model=UserProfile)
54
+async def get_my_profile(
55
+    current_user = Depends(get_current_user),
56
+    db: Session = Depends(get_db)
57
+):
58
+    """获取我的资料"""
59
+    user_service = UserService(db)
60
+    user = await user_service.get_user_by_id(current_user.id)
61
+    
62
+    if not user:
63
+        raise HTTPException(
64
+            status_code=status.HTTP_404_NOT_FOUND,
65
+            detail="用户不存在"
66
+        )
67
+    
68
+    return UserProfile(
69
+        id=user.id,
70
+        username=user.username,
71
+        email=user.email,
72
+        full_name=user.full_name,
73
+        is_active=user.is_active,
74
+        is_verified=user.is_verified,
75
+        created_at=user.created_at,
76
+        last_login=user.last_login,
77
+        avatar=user.avatar
78
+    )
79
+
80
+@router.put("/me", response_model=UserProfile)
81
+async def update_my_profile(
82
+    user_data: UserUpdate,
83
+    current_user = Depends(get_current_user),
84
+    db: Session = Depends(get_db)
85
+):
86
+    """更新我的资料"""
87
+    user_service = UserService(db)
88
+    
89
+    update_dict = user_data.dict(exclude_unset=True)
90
+    
91
+    # 如果更新邮箱,需要验证新邮箱是否已被使用
92
+    if user_data.email and user_data.email != current_user.email:
93
+        existing_user = await user_service.get_user_by_email(user_data.email)
94
+        if existing_user and existing_user.id != current_user.id:
95
+            raise HTTPException(
96
+                status_code=status.HTTP_400_BAD_REQUEST,
97
+                detail="邮箱已被使用"
98
+            )
99
+    
100
+    updated_user = await user_service.update_user(current_user.id, update_dict)
101
+    
102
+    if not updated_user:
103
+        raise HTTPException(
104
+            status_code=status.HTTP_404_NOT_FOUND,
105
+            detail="用户不存在"
106
+        )
107
+    
108
+    return UserProfile(
109
+        id=updated_user.id,
110
+        username=updated_user.username,
111
+        email=updated_user.email,
112
+        full_name=updated_user.full_name,
113
+        is_active=updated_user.is_active,
114
+        is_verified=updated_user.is_verified,
115
+        created_at=updated_user.created_at,
116
+        last_login=updated_user.last_login,
117
+        avatar=updated_user.avatar
118
+    )
119
+
120
+@router.post("/me/change-password")
121
+async def change_password(
122
+    password_data: PasswordChange,
123
+    current_user = Depends(get_current_user),
124
+    db: Session = Depends(get_db)
125
+):
126
+    """修改密码"""
127
+    user_service = UserService(db)
128
+    
129
+    try:
130
+        success = await user_service.change_password(
131
+            current_user.id,
132
+            password_data.current_password,
133
+            password_data.new_password
134
+        )
135
+        
136
+        if not success:
137
+            raise HTTPException(
138
+                status_code=status.HTTP_400_BAD_REQUEST,
139
+                detail="当前密码错误"
140
+            )
141
+        
142
+        return {"message": "密码修改成功"}
143
+        
144
+    except ValueError as e:
145
+        raise HTTPException(
146
+            status_code=status.HTTP_400_BAD_REQUEST,
147
+            detail=str(e)
148
+        )
149
+
150
+@router.get("/", response_model=List[UserProfile])
151
+async def list_users(
152
+    skip: int = Query(0, ge=0),
153
+    limit: int = Query(100, ge=1, le=100),
154
+    active_only: bool = Query(True),
155
+    current_user = Depends(get_current_user),
156
+    db: Session = Depends(get_db)
157
+):
158
+    """获取用户列表(需要管理员权限)"""
159
+    if not current_user.is_superuser:
160
+        raise HTTPException(
161
+            status_code=status.HTTP_403_FORBIDDEN,
162
+            detail="需要管理员权限"
163
+        )
164
+    
165
+    user_service = UserService(db)
166
+    users = await user_service.list_users(skip, limit, active_only)
167
+    
168
+    return [
169
+        UserProfile(
170
+            id=user.id,
171
+            username=user.username,
172
+            email=user.email,
173
+            full_name=user.full_name,
174
+            is_active=user.is_active,
175
+            is_verified=user.is_verified,
176
+            created_at=user.created_at,
177
+            last_login=user.last_login,
178
+            avatar=user.avatar
179
+        )
180
+        for user in users
181
+    ]
182
+
183
+@router.get("/{user_id}", response_model=UserProfile)
184
+async def get_user(
185
+    user_id: int,
186
+    current_user = Depends(get_current_user),
187
+    db: Session = Depends(get_db)
188
+):
189
+    """获取单个用户信息(需要管理员权限查看其他用户)"""
190
+    user_service = UserService(db)
191
+    
192
+    # 普通用户只能查看自己的信息
193
+    if not current_user.is_superuser and user_id != current_user.id:
194
+        raise HTTPException(
195
+            status_code=status.HTTP_403_FORBIDDEN,
196
+            detail="只能查看自己的用户信息"
197
+        )
198
+    
199
+    user = await user_service.get_user_by_id(user_id)
200
+    
201
+    if not user:
202
+        raise HTTPException(
203
+            status_code=status.HTTP_404_NOT_FOUND,
204
+            detail="用户不存在"
205
+        )
206
+    
207
+    return UserProfile(
208
+        id=user.id,
209
+        username=user.username,
210
+        email=user.email,
211
+        full_name=user.full_name,
212
+        is_active=user.is_active,
213
+        is_verified=user.is_verified,
214
+        created_at=user.created_at,
215
+        last_login=user.last_login,
216
+        avatar=user.avatar
217
+    )
218
+
219
+@router.delete("/{user_id}")
220
+async def delete_user(
221
+    user_id: int,
222
+    current_user = Depends(get_current_user),
223
+    db: Session = Depends(get_db)
224
+):
225
+    """删除用户(需要管理员权限)"""
226
+    if not current_user.is_superuser:
227
+        raise HTTPException(
228
+            status_code=status.HTTP_403_FORBIDDEN,
229
+            detail="需要管理员权限"
230
+        )
231
+    
232
+    # 不能删除自己
233
+    if user_id == current_user.id:
234
+        raise HTTPException(
235
+            status_code=status.HTTP_400_BAD_REQUEST,
236
+            detail="不能删除自己的账户"
237
+        )
238
+    
239
+    user_service = UserService(db)
240
+    success = await user_service.delete_user(user_id)
241
+    
242
+    if not success:
243
+        raise HTTPException(
244
+            status_code=status.HTTP_404_NOT_FOUND,
245
+            detail="用户不存在"
246
+        )
247
+    
248
+    return {"message": "用户已禁用"}
249
+
250
+@router.get("/test")
251
+async def test_users():
252
+    """测试用户管理 API"""
253
+    return {
254
+        "message": "用户管理 API 正常运行(使用 SQLite 数据库)",
255
+        "endpoints": {
256
+            "GET /me": "获取我的资料",
257
+            "PUT /me": "更新我的资料",
258
+            "POST /me/change-password": "修改密码",
259
+            "GET /": "获取用户列表(管理员)",
260
+            "GET /{user_id}": "获取单个用户",
261
+            "DELETE /{user_id}": "删除用户(管理员)"
262
+        }
263
+    }

+ 0
- 0
app/api/v1/verify.py View File


+ 44
- 0
app/config.py View File

@@ -0,0 +1,44 @@
1
+# app/config.py
2
+import os
3
+from typing import List
4
+import secrets
5
+from dotenv import load_dotenv
6
+
7
+# 加载环境变量
8
+load_dotenv()
9
+
10
+class Settings:
11
+    # 项目配置
12
+    PROJECT_NAME: str = os.getenv("PROJECT_NAME", "CaiYouHui 采油会")
13
+    VERSION: str = os.getenv("VERSION", "1.0.0")
14
+    API_V1_PREFIX: str = os.getenv("API_V1_PREFIX", "/api/v1")
15
+    
16
+    # 安全配置
17
+    SECRET_KEY: str = os.getenv("SECRET_KEY", secrets.token_urlsafe(32))
18
+    ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
19
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "1440"))  # 24小时
20
+    
21
+    # 数据库配置 - SQLite
22
+    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./caiyouhui.db")
23
+    
24
+    # CORS 配置
25
+    BACKEND_CORS_ORIGINS: List[str] = os.getenv(
26
+        "BACKEND_CORS_ORIGINS", 
27
+        "http://localhost:3000,http://localhost:5173"
28
+    ).split(",")
29
+    
30
+    # 调试模式
31
+    DEBUG: bool = os.getenv("DEBUG", "True").lower() == "true"
32
+    
33
+    # 文件上传
34
+    UPLOAD_DIR: str = os.getenv("UPLOAD_DIR", "./uploads")
35
+    MAX_UPLOAD_SIZE: int = int(os.getenv("MAX_UPLOAD_SIZE", "10485760"))  # 10MB
36
+    
37
+    # 邮箱验证(可选,后续添加)
38
+    SMTP_ENABLED: bool = os.getenv("SMTP_ENABLED", "False").lower() == "true"
39
+    SMTP_HOST: str = os.getenv("SMTP_HOST", "")
40
+    SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587"))
41
+    SMTP_USER: str = os.getenv("SMTP_USER", "")
42
+    SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
43
+
44
+settings = Settings()

+ 38
- 0
app/core/__init__.py View File

@@ -0,0 +1,38 @@
1
+# app/core/__init__.py
2
+from .security import (
3
+    # 便捷函数
4
+    create_access_token,
5
+    verify_access_token,
6
+    verify_password,
7
+    get_password_hash,
8
+    
9
+    # 类
10
+    PasswordHasher,
11
+    JWTManager,
12
+    PasswordValidator,
13
+    TokenUtils,
14
+    
15
+    # 实例
16
+    password_hasher,
17
+    password_validator,
18
+    token_utils,
19
+)
20
+
21
+__all__ = [
22
+    # 便捷函数
23
+    "create_access_token",
24
+    "verify_access_token",
25
+    "verify_password",
26
+    "get_password_hash",
27
+    
28
+    # 类
29
+    "PasswordHasher",
30
+    "JWTManager",
31
+    "PasswordValidator",
32
+    "TokenUtils",
33
+    
34
+    # 实例
35
+    "password_hasher",
36
+    "password_validator",
37
+    "token_utils",
38
+]

BIN
app/core/__pycache__/__init__.cpython-311.pyc View File


BIN
app/core/__pycache__/security.cpython-311.pyc View File


+ 91
- 0
app/core/auth.py View File

@@ -0,0 +1,91 @@
1
+from passlib.context import CryptContext
2
+from jose import JWTError, jwt
3
+from datetime import datetime, timedelta
4
+from typing import Optional, Dict, Any, Union
5
+import secrets
6
+import string
7
+import re
8
+from ..config import settings
9
+
10
+# 密码上下文
11
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
12
+
13
+def verify_password(plain_password: str, hashed_password: str) -> bool:
14
+    """验证密码"""
15
+    return pwd_context.verify(plain_password, hashed_password)
16
+
17
+def get_password_hash(password: str) -> str:
18
+    """生成密码哈希"""
19
+    return pwd_context.hash(password)
20
+
21
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
22
+    """创建访问令牌"""
23
+    to_encode = data.copy()
24
+    
25
+    if expires_delta:
26
+        expire = datetime.utcnow() + expires_delta
27
+    else:
28
+        expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
29
+    
30
+    to_encode.update({"exp": expire, "type": "access"})
31
+    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
32
+    return encoded_jwt
33
+
34
+def create_refresh_token(data: dict) -> str:
35
+    """创建刷新令牌"""
36
+    to_encode = data.copy()
37
+    expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
38
+    
39
+    to_encode.update({"exp": expire, "type": "refresh"})
40
+    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
41
+    return encoded_jwt
42
+
43
+def create_verification_token(email: str) -> str:
44
+    """创建验证令牌"""
45
+    to_encode = {"email": email, "type": "verify"}
46
+    expire = datetime.utcnow() + timedelta(hours=settings.VERIFICATION_TOKEN_EXPIRE_HOURS)
47
+    
48
+    to_encode.update({"exp": expire})
49
+    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
50
+    return encoded_jwt
51
+
52
+def create_reset_token(email: str) -> str:
53
+    """创建密码重置令牌"""
54
+    to_encode = {"email": email, "type": "reset"}
55
+    expire = datetime.utcnow() + timedelta(minutes=settings.RESET_TOKEN_EXPIRE_MINUTES)
56
+    
57
+    to_encode.update({"exp": expire})
58
+    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
59
+    return encoded_jwt
60
+
61
+def decode_token(token: str) -> Optional[Dict[str, Any]]:
62
+    """解码令牌"""
63
+    try:
64
+        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
65
+        return payload
66
+    except JWTError:
67
+        return None
68
+
69
+def generate_verification_code(length: int = 6) -> str:
70
+    """生成验证码"""
71
+    digits = string.digits
72
+    return ''.join(secrets.choice(digits) for _ in range(length))
73
+
74
+def validate_password_strength(password: str) -> tuple[bool, str]:
75
+    """验证密码强度"""
76
+    if len(password) < 8:
77
+        return False, "Password must be at least 8 characters long"
78
+    
79
+    if not re.search(r"[A-Z]", password):
80
+        return False, "Password must contain at least one uppercase letter"
81
+    
82
+    if not re.search(r"[a-z]", password):
83
+        return False, "Password must contain at least one lowercase letter"
84
+    
85
+    if not re.search(r"\d", password):
86
+        return False, "Password must contain at least one digit"
87
+    
88
+    if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):
89
+        return False, "Password must contain at least one special character"
90
+    
91
+    return True, "Password is strong"

+ 159
- 0
app/core/email.py View File

@@ -0,0 +1,159 @@
1
+from fastapi import BackgroundTasks
2
+from typing import Optional
3
+import smtplib
4
+from email.mime.text import MIMEText
5
+from email.mime.multipart import MIMEMultipart
6
+from jinja2 import Template
7
+import aiosmtplib
8
+from ..config import settings
9
+import os
10
+
11
+class EmailService:
12
+    def __init__(self):
13
+        self.smtp_host = settings.SMTP_HOST
14
+        self.smtp_port = settings.SMTP_PORT
15
+        self.smtp_user = settings.SMTP_USER
16
+        self.smtp_password = settings.SMTP_PASSWORD
17
+        self.from_email = settings.EMAILS_FROM_EMAIL
18
+        self.from_name = settings.EMAILS_FROM_NAME
19
+        
20
+    async def send_email_async(
21
+        self,
22
+        email_to: str,
23
+        subject: str,
24
+        html_content: str,
25
+        text_content: Optional[str] = None
26
+    ) -> bool:
27
+        """异步发送邮件"""
28
+        message = MIMEMultipart("alternative")
29
+        message["Subject"] = subject
30
+        message["From"] = f"{self.from_name} <{self.from_email}>"
31
+        message["To"] = email_to
32
+        
33
+        # 添加纯文本版本
34
+        if text_content:
35
+            part1 = MIMEText(text_content, "plain")
36
+            message.attach(part1)
37
+        
38
+        # 添加HTML版本
39
+        part2 = MIMEText(html_content, "html")
40
+        message.attach(part2)
41
+        
42
+        try:
43
+            await aiosmtplib.send(
44
+                message,
45
+                hostname=self.smtp_host,
46
+                port=self.smtp_port,
47
+                username=self.smtp_user,
48
+                password=self.smtp_password,
49
+                use_tls=True,
50
+            )
51
+            return True
52
+        except Exception as e:
53
+            print(f"Error sending email: {e}")
54
+            return False
55
+    
56
+    def send_email_sync(
57
+        self,
58
+        email_to: str,
59
+        subject: str,
60
+        html_content: str,
61
+        text_content: Optional[str] = None
62
+    ) -> bool:
63
+        """同步发送邮件"""
64
+        message = MIMEMultipart("alternative")
65
+        message["Subject"] = subject
66
+        message["From"] = f"{self.from_name} <{self.from_email}>"
67
+        message["To"] = email_to
68
+        
69
+        if text_content:
70
+            part1 = MIMEText(text_content, "plain")
71
+            message.attach(part1)
72
+        
73
+        part2 = MIMEText(html_content, "html")
74
+        message.attach(part2)
75
+        
76
+        try:
77
+            with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
78
+                server.starttls()
79
+                server.login(self.smtp_user, self.smtp_password)
80
+                server.send_message(message)
81
+            return True
82
+        except Exception as e:
83
+            print(f"Error sending email: {e}")
84
+            return False
85
+    
86
+    async def send_verification_email(
87
+        self,
88
+        email_to: str,
89
+        username: str,
90
+        verification_url: str,
91
+        verification_code: Optional[str] = None
92
+    ) -> bool:
93
+        """发送验证邮件"""
94
+        # 加载模板
95
+        template_path = os.path.join("templates", "email", "verify_email.html")
96
+        with open(template_path, "r") as f:
97
+            template_str = f.read()
98
+        
99
+        template = Template(template_str)
100
+        html_content = template.render(
101
+            username=username,
102
+            verification_url=verification_url,
103
+            verification_code=verification_code,
104
+            frontend_url=settings.FRONTEND_URL
105
+        )
106
+        
107
+        subject = "Verify Your Email Address"
108
+        text_content = f"Hello {username},\n\nPlease verify your email by clicking: {verification_url}"
109
+        
110
+        if verification_code:
111
+            text_content += f"\n\nOr use this verification code: {verification_code}"
112
+        
113
+        return await self.send_email_async(email_to, subject, html_content, text_content)
114
+    
115
+    async def send_password_reset_email(
116
+        self,
117
+        email_to: str,
118
+        username: str,
119
+        reset_url: str
120
+    ) -> bool:
121
+        """发送密码重置邮件"""
122
+        template_path = os.path.join("templates", "email", "reset_password.html")
123
+        with open(template_path, "r") as f:
124
+            template_str = f.read()
125
+        
126
+        template = Template(template_str)
127
+        html_content = template.render(
128
+            username=username,
129
+            reset_url=reset_url,
130
+            frontend_url=settings.FRONTEND_URL
131
+        )
132
+        
133
+        subject = "Password Reset Request"
134
+        text_content = f"Hello {username},\n\nClick here to reset your password: {reset_url}"
135
+        
136
+        return await self.send_email_async(email_to, subject, html_content, text_content)
137
+    
138
+    async def send_welcome_email(
139
+        self,
140
+        email_to: str,
141
+        username: str
142
+    ) -> bool:
143
+        """发送欢迎邮件"""
144
+        template_path = os.path.join("templates", "email", "welcome.html")
145
+        with open(template_path, "r") as f:
146
+            template_str = f.read()
147
+        
148
+        template = Template(template_str)
149
+        html_content = template.render(
150
+            username=username,
151
+            frontend_url=settings.FRONTEND_URL
152
+        )
153
+        
154
+        subject = "Welcome to Our Platform!"
155
+        text_content = f"Welcome {username}! We're glad to have you on board."
156
+        
157
+        return await self.send_email_async(email_to, subject, html_content, text_content)
158
+
159
+email_service = EmailService()

+ 0
- 0
app/core/permissions.py View File


+ 394
- 0
app/core/security.py View File

@@ -0,0 +1,394 @@
1
+# app/core/security.py
2
+import hashlib
3
+import secrets
4
+import base64
5
+import hmac
6
+import time
7
+import json
8
+from typing import Optional, Dict, Any, Tuple
9
+from datetime import datetime, timedelta
10
+import re
11
+
12
+# 密码哈希工具
13
+class PasswordHasher:
14
+    """密码哈希和验证工具(使用 PBKDF2)"""
15
+    
16
+    @staticmethod
17
+    def hash_password(password: str) -> str:
18
+        """哈希密码"""
19
+        # 生成随机盐(16字节)
20
+        salt = secrets.token_bytes(16)
21
+        
22
+        # 使用 PBKDF2-HMAC-SHA256
23
+        dk = hashlib.pbkdf2_hmac(
24
+            'sha256',
25
+            password.encode('utf-8'),
26
+            salt,
27
+            100000  # 迭代次数
28
+        )
29
+        
30
+        # 组合 salt + hash,然后 base64 编码
31
+        combined = salt + dk
32
+        return base64.b64encode(combined).decode('utf-8')
33
+    
34
+    @staticmethod
35
+    def verify_password(password: str, hashed_password: str) -> bool:
36
+        """验证密码"""
37
+        try:
38
+            # 解码 base64
39
+            decoded = base64.b64decode(hashed_password.encode('utf-8'))
40
+            
41
+            # 提取 salt (前16字节) 和存储的 hash
42
+            salt = decoded[:16]
43
+            stored_hash = decoded[16:]
44
+            
45
+            # 用相同的盐计算输入密码的 hash
46
+            dk = hashlib.pbkdf2_hmac(
47
+                'sha256',
48
+                password.encode('utf-8'),
49
+                salt,
50
+                100000
51
+            )
52
+            
53
+            # 使用常量时间比较防止时序攻击
54
+            return secrets.compare_digest(dk, stored_hash)
55
+            
56
+        except Exception:
57
+            return False
58
+
59
+# JWT 管理器
60
+class JWTManager:
61
+    """JWT 令牌管理工具"""
62
+    
63
+    def __init__(self, secret_key: str, algorithm: str = "HS256"):
64
+        self.secret_key = secret_key
65
+        self.algorithm = algorithm
66
+    
67
+    def create_access_token(
68
+        self, 
69
+        data: Dict[str, Any], 
70
+        expires_delta: Optional[timedelta] = None,
71
+        expires_minutes: Optional[int] = None
72
+    ) -> str:
73
+        """创建访问令牌
74
+        
75
+        Args:
76
+            data: 要编码的数据
77
+            expires_delta: 过期时间差
78
+            expires_minutes: 过期分钟数
79
+            
80
+        Returns:
81
+            str: JWT 令牌
82
+        """
83
+        return self.create_token(data, expires_delta, expires_minutes, token_type="access")
84
+    
85
+    def create_refresh_token(
86
+        self,
87
+        data: Dict[str, Any],
88
+        expires_delta: Optional[timedelta] = None,
89
+        expires_days: int = 7
90
+    ) -> str:
91
+        """创建刷新令牌
92
+        
93
+        Args:
94
+            data: 要编码的数据
95
+            expires_delta: 过期时间差
96
+            expires_days: 过期天数
97
+            
98
+        Returns:
99
+            str: JWT 刷新令牌
100
+        """
101
+        if expires_delta is None:
102
+            expires_delta = timedelta(days=expires_days)
103
+        return self.create_token(data, expires_delta, token_type="refresh")
104
+    
105
+    def create_verification_token(
106
+        self,
107
+        data: Dict[str, Any],
108
+        expires_delta: Optional[timedelta] = None,
109
+        expires_hours: int = 24
110
+    ) -> str:
111
+        """创建验证令牌(用于邮箱验证等)
112
+        
113
+        Args:
114
+            data: 要编码的数据
115
+            expires_delta: 过期时间差
116
+            expires_hours: 过期小时数
117
+            
118
+        Returns:
119
+            str: JWT 验证令牌
120
+        """
121
+        if expires_delta is None:
122
+            expires_delta = timedelta(hours=expires_hours)
123
+        return self.create_token(data, expires_delta, token_type="verify")
124
+    
125
+    def create_reset_token(
126
+        self,
127
+        data: Dict[str, Any],
128
+        expires_delta: Optional[timedelta] = None,
129
+        expires_minutes: int = 30
130
+    ) -> str:
131
+        """创建密码重置令牌
132
+        
133
+        Args:
134
+            data: 要编码的数据
135
+            expires_delta: 过期时间差
136
+            expires_minutes: 过期分钟数
137
+            
138
+        Returns:
139
+            str: JWT 重置令牌
140
+        """
141
+        if expires_delta is None:
142
+            expires_delta = timedelta(minutes=expires_minutes)
143
+        return self.create_token(data, expires_delta, token_type="reset")
144
+    
145
+    def create_token(
146
+        self, 
147
+        data: Dict[str, Any], 
148
+        expires_delta: Optional[timedelta] = None,
149
+        expires_minutes: Optional[int] = None,
150
+        token_type: str = "access"
151
+    ) -> str:
152
+        """创建 JWT 令牌(通用方法)
153
+        
154
+        Args:
155
+            data: 要编码的数据
156
+            expires_delta: 过期时间差
157
+            expires_minutes: 过期分钟数
158
+            token_type: 令牌类型
159
+            
160
+        Returns:
161
+            str: JWT 令牌
162
+        """
163
+        # 复制数据以避免修改原始数据
164
+        payload = data.copy()
165
+        
166
+        # 设置过期时间
167
+        if expires_delta:
168
+            expire = datetime.utcnow() + expires_delta
169
+        elif expires_minutes:
170
+            expire = datetime.utcnow() + timedelta(minutes=expires_minutes)
171
+        else:
172
+            expire = datetime.utcnow() + timedelta(minutes=30)  # 默认30分钟
173
+        
174
+        # 添加标准声明
175
+        payload.update({
176
+            "exp": int(expire.timestamp()),
177
+            "iat": int(datetime.utcnow().timestamp()),
178
+            "iss": "caiyouhui-api",
179
+            "type": token_type
180
+        })
181
+        
182
+        # 编码 header 和 payload
183
+        header = json.dumps({"alg": self.algorithm, "typ": "JWT"})
184
+        payload_str = json.dumps(payload)
185
+        
186
+        header_b64 = base64.urlsafe_b64encode(header.encode()).decode().rstrip('=')
187
+        payload_b64 = base64.urlsafe_b64encode(payload_str.encode()).decode().rstrip('=')
188
+        
189
+        # 创建签名
190
+        message = f"{header_b64}.{payload_b64}"
191
+        signature = hmac.new(
192
+            self.secret_key.encode(),
193
+            message.encode(),
194
+            hashlib.sha256
195
+        ).digest()
196
+        signature_b64 = base64.urlsafe_b64encode(signature).decode().rstrip('=')
197
+        
198
+        return f"{header_b64}.{payload_b64}.{signature_b64}"
199
+    
200
+    def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
201
+        """验证 JWT 令牌
202
+        
203
+        Args:
204
+            token: JWT 令牌
205
+            
206
+        Returns:
207
+            Optional[Dict]: 解码后的数据,如果令牌无效则返回 None
208
+        """
209
+        try:
210
+            parts = token.split('.')
211
+            if len(parts) != 3:
212
+                return None
213
+            
214
+            header_b64, payload_b64, signature_b64 = parts
215
+            
216
+            # 验证签名
217
+            message = f"{header_b64}.{payload_b64}"
218
+            expected_signature = hmac.new(
219
+                self.secret_key.encode(),
220
+                message.encode(),
221
+                hashlib.sha256
222
+            ).digest()
223
+            expected_signature_b64 = base64.urlsafe_b64encode(expected_signature).decode().rstrip('=')
224
+            
225
+            # 使用常量时间比较
226
+            if not secrets.compare_digest(signature_b64, expected_signature_b64):
227
+                return None
228
+            
229
+            # 解码 payload
230
+            payload_json = base64.urlsafe_b64decode(payload_b64 + '=' * (4 - len(payload_b64) % 4)).decode()
231
+            payload = json.loads(payload_json)
232
+            
233
+            # 检查过期时间
234
+            if 'exp' in payload and payload['exp'] < int(time.time()):
235
+                return None
236
+            
237
+            return payload
238
+            
239
+        except Exception:
240
+            return None
241
+    
242
+    def decode_token(self, token: str) -> Optional[Dict[str, Any]]:
243
+        """解码令牌(不验证签名,用于调试)
244
+        
245
+        Args:
246
+            token: JWT 令牌
247
+            
248
+        Returns:
249
+            Optional[Dict]: 解码后的数据
250
+        """
251
+        try:
252
+            parts = token.split('.')
253
+            if len(parts) != 3:
254
+                return None
255
+            
256
+            _, payload_b64, _ = parts
257
+            
258
+            # 解码 payload
259
+            payload_json = base64.urlsafe_b64decode(payload_b64 + '=' * (4 - len(payload_b64) % 4)).decode()
260
+            return json.loads(payload_json)
261
+            
262
+        except Exception:
263
+            return None
264
+
265
+# 密码验证工具
266
+class PasswordValidator:
267
+    """密码强度验证工具"""
268
+    
269
+    @staticmethod
270
+    def validate_password_strength(password: str) -> Tuple[bool, str]:
271
+        """验证密码强度"""
272
+        # 检查最小长度
273
+        if len(password) < 8:
274
+            return False, "密码必须至少8个字符"
275
+        
276
+        # 检查最大长度
277
+        if len(password) > 100:
278
+            return False, "密码不能超过100个字符"
279
+        
280
+        # 检查是否包含大写字母
281
+        if not re.search(r'[A-Z]', password):
282
+            return False, "密码必须包含至少一个大写字母"
283
+        
284
+        # 检查是否包含小写字母
285
+        if not re.search(r'[a-z]', password):
286
+            return False, "密码必须包含至少一个小写字母"
287
+        
288
+        # 检查是否包含数字
289
+        if not re.search(r'\d', password):
290
+            return False, "密码必须包含至少一个数字"
291
+        
292
+        # 检查是否包含特殊字符
293
+        if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
294
+            return False, "密码必须包含至少一个特殊字符"
295
+        
296
+        return True, "密码强度足够"
297
+
298
+# 令牌工具
299
+class TokenUtils:
300
+    """令牌相关工具"""
301
+    
302
+    @staticmethod
303
+    def generate_verification_code(length: int = 6) -> str:
304
+        """生成数字验证码"""
305
+        digits = '0123456789'
306
+        return ''.join(secrets.choice(digits) for _ in range(length))
307
+    
308
+    @staticmethod
309
+    def generate_reset_token(length: int = 32) -> str:
310
+        """生成密码重置令牌"""
311
+        return secrets.token_urlsafe(length)
312
+    
313
+    @staticmethod
314
+    def generate_api_key(length: int = 32) -> str:
315
+        """生成 API 密钥"""
316
+        import string
317
+        alphabet = string.ascii_letters + string.digits
318
+        return ''.join(secrets.choice(alphabet) for _ in range(length))
319
+
320
+# 导出的便捷函数
321
+def create_access_token(
322
+    data: Dict[str, Any],
323
+    secret_key: str,
324
+    expires_delta: Optional[timedelta] = None,
325
+    expires_minutes: int = 30,
326
+    algorithm: str = "HS256"
327
+) -> str:
328
+    """创建访问令牌(便捷函数)
329
+    
330
+    Args:
331
+        data: 要编码的数据
332
+        secret_key: 密钥
333
+        expires_delta: 过期时间差
334
+        expires_minutes: 过期分钟数
335
+        algorithm: 算法
336
+        
337
+    Returns:
338
+        str: JWT 令牌
339
+    """
340
+    jwt_manager = JWTManager(secret_key, algorithm)
341
+    return jwt_manager.create_access_token(data, expires_delta, expires_minutes)
342
+
343
+def verify_access_token(token: str, secret_key: str, algorithm: str = "HS256") -> Optional[Dict[str, Any]]:
344
+    """验证访问令牌(便捷函数)
345
+    
346
+    Args:
347
+        token: JWT 令牌
348
+        secret_key: 密钥
349
+        algorithm: 算法
350
+        
351
+    Returns:
352
+        Optional[Dict]: 解码后的数据
353
+    """
354
+    jwt_manager = JWTManager(secret_key, algorithm)
355
+    return jwt_manager.verify_token(token)
356
+
357
+def get_password_hash(password: str) -> str:
358
+    """哈希密码(便捷函数)
359
+    
360
+    Args:
361
+        password: 明文密码
362
+        
363
+    Returns:
364
+        str: 哈希后的密码
365
+    """
366
+    return PasswordHasher.hash_password(password)
367
+
368
+# 创建全局实例
369
+password_hasher = PasswordHasher()
370
+password_validator = PasswordValidator()
371
+token_utils = TokenUtils()
372
+
373
+# 导出函数(保持向后兼容)
374
+verify_password = password_hasher.verify_password
375
+
376
+# 导出的函数和类
377
+__all__ = [
378
+    # 便捷函数
379
+    "create_access_token",
380
+    "verify_access_token",
381
+    "verify_password",
382
+    "get_password_hash",
383
+    
384
+    # 类
385
+    "PasswordHasher",
386
+    "JWTManager",
387
+    "PasswordValidator",
388
+    "TokenUtils",
389
+    
390
+    # 实例
391
+    "password_hasher",
392
+    "password_validator",
393
+    "token_utils",
394
+]

+ 64
- 0
app/database.py View File

@@ -0,0 +1,64 @@
1
+# app/database.py
2
+from sqlalchemy import create_engine
3
+from sqlalchemy.orm import sessionmaker, Session
4
+from sqlalchemy.ext.declarative import declarative_base
5
+from contextlib import contextmanager
6
+from typing import Generator
7
+import os
8
+
9
+from .config import settings
10
+
11
+# 创建数据库引擎
12
+engine = create_engine(
13
+    settings.DATABASE_URL,
14
+    connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {},
15
+    echo=settings.DEBUG
16
+)
17
+
18
+# 创建会话工厂
19
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
20
+
21
+# 声明基类
22
+Base = declarative_base()
23
+
24
+def get_db() -> Generator[Session, None, None]:
25
+    """数据库会话依赖注入"""
26
+    db = SessionLocal()
27
+    try:
28
+        yield db
29
+    finally:
30
+        db.close()
31
+
32
+def init_db():
33
+    """初始化数据库表"""
34
+    from .models.user import User
35
+    Base.metadata.create_all(bind=engine)
36
+    print("✅ 数据库表创建完成")
37
+    
38
+    # 创建默认管理员用户
39
+    db = SessionLocal()
40
+    try:
41
+        # 检查是否已存在管理员
42
+        admin = db.query(User).filter(User.username == "admin").first()
43
+        if not admin:
44
+            from .core.security import password_hasher
45
+            admin_user = User(
46
+                username="admin",
47
+                email="admin@caiyouhui.com",
48
+                hashed_password=password_hasher.hash_password("Admin123!"),
49
+                full_name="系统管理员",
50
+                is_active=True,
51
+                is_verified=True,
52
+                is_superuser=True
53
+            )
54
+            db.add(admin_user)
55
+            db.commit()
56
+            print("✅ 默认管理员用户已创建")
57
+    except Exception as e:
58
+        print(f"⚠️  创建管理员用户时出错: {e}")
59
+        db.rollback()
60
+    finally:
61
+        db.close()
62
+
63
+# 导出
64
+__all__ = ["Base", "engine", "SessionLocal", "get_db", "init_db"]

+ 0
- 0
app/dependencies/__init__.py View File


BIN
app/dependencies/__pycache__/__init__.cpython-311.pyc View File


BIN
app/dependencies/__pycache__/auth.cpython-311.pyc View File


BIN
app/dependencies/__pycache__/database.cpython-311.pyc View File


+ 110
- 0
app/dependencies/auth.py View File

@@ -0,0 +1,110 @@
1
+from fastapi import Depends, HTTPException, status
2
+from fastapi.security import OAuth2PasswordBearer
3
+from sqlalchemy.orm import Session
4
+from jose import JWTError, jwt
5
+from typing import Optional
6
+
7
+from ..database import get_db
8
+from ..models.user import User
9
+from ..schemas.token import TokenData
10
+from ..config import settings
11
+
12
+oauth2_scheme = OAuth2PasswordBearer(
13
+    tokenUrl=f"{settings.API_V1_PREFIX}/auth/login",
14
+    auto_error=False
15
+)
16
+
17
+async def get_current_user(
18
+    token: Optional[str] = Depends(oauth2_scheme),
19
+    db: Session = Depends(get_db)
20
+) -> Optional[User]:
21
+    """获取当前用户"""
22
+    if not token:
23
+        print("dfsfdsfdfdsfd")
24
+        return None
25
+    
26
+    credentials_exception = HTTPException(
27
+        status_code=status.HTTP_401_UNAUTHORIZED,
28
+        detail="Could not validate credentials",
29
+        headers={"WWW-Authenticate": "Bearer"},
30
+    )
31
+    
32
+    try:
33
+        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
34
+        username: str = payload.get("sub")
35
+        token_type: str = payload.get("type")
36
+        
37
+        if username is None or token_type != "access":
38
+            raise credentials_exception
39
+        
40
+        token_data = TokenData(username=username)
41
+    except JWTError:
42
+        raise credentials_exception
43
+    
44
+    user = db.query(User).filter(
45
+        User.username == token_data.username,
46
+        User.is_active == True
47
+    ).first()
48
+    
49
+    if user is None:
50
+        raise credentials_exception
51
+    
52
+    return user
53
+
54
+async def get_current_active_user(
55
+    current_user: User = Depends(get_current_user)
56
+) -> User:
57
+    """获取当前活跃用户"""
58
+    if not current_user:
59
+        raise HTTPException(
60
+            status_code=status.HTTP_401_UNAUTHORIZED,
61
+            detail="Not authenticated"
62
+        )
63
+    
64
+    if not current_user.is_active:
65
+        raise HTTPException(
66
+            status_code=status.HTTP_400_BAD_REQUEST,
67
+            detail="Inactive user"
68
+        )
69
+    
70
+    return current_user
71
+
72
+async def get_current_superuser(
73
+    current_user: User = Depends(get_current_user)
74
+) -> User:
75
+    """获取超级用户"""
76
+    if not current_user or not current_user.is_superuser:
77
+        raise HTTPException(
78
+            status_code=status.HTTP_403_FORBIDDEN,
79
+            detail="Not enough permissions"
80
+        )
81
+    
82
+    return current_user
83
+
84
+def require_auth(current_user: Optional[User] = Depends(get_current_user)) -> User:
85
+    """要求认证的依赖"""
86
+    if not current_user:
87
+        raise HTTPException(
88
+            status_code=status.HTTP_401_UNAUTHORIZED,
89
+            detail="Not authenticated"
90
+        )
91
+    return current_user
92
+
93
+# 权限检查装饰器
94
+def require_permission(permission: str):
95
+    """权限检查装饰器"""
96
+    def permission_dependency(
97
+        current_user: User = Depends(get_current_active_user)
98
+    ) -> User:
99
+        # 这里实现具体的权限检查逻辑
100
+        # 可以从数据库或缓存中获取用户权限
101
+        if not current_user.is_superuser:
102
+            # 检查用户是否有特定权限
103
+            user_permissions = []  # 从数据库获取
104
+            if permission not in user_permissions:
105
+                raise HTTPException(
106
+                    status_code=status.HTTP_403_FORBIDDEN,
107
+                    detail="Insufficient permissions"
108
+                )
109
+        return current_user
110
+    return permission_dependency

+ 15
- 0
app/dependencies/database.py View File

@@ -0,0 +1,15 @@
1
+# app/dependencies/database.py
2
+from sqlalchemy.orm import Session
3
+from contextlib import contextmanager
4
+from typing import Generator
5
+
6
+# ✅ 正确导入方式
7
+from app.database import SessionLocal
8
+
9
+def get_db() -> Generator[Session, None, None]:
10
+    """数据库会话依赖注入"""
11
+    db = SessionLocal()
12
+    try:
13
+        yield db
14
+    finally:
15
+        db.close()

+ 89
- 0
app/logging_config.py View File

@@ -0,0 +1,89 @@
1
+# app/logging_config.py
2
+import logging
3
+import logging.handlers  # ✅ 这里导入 handlers
4
+import os
5
+from datetime import datetime
6
+
7
+def setup_logging_with_rotation():
8
+    """配置带轮转的日志系统"""
9
+    
10
+    # 创建日志目录
11
+    log_dir = "logs"
12
+    os.makedirs(log_dir, exist_ok=True)
13
+    
14
+    # 主日志文件
15
+    main_log_file = os.path.join(log_dir, "app.log")
16
+    
17
+    # 配置根日志记录器
18
+    root_logger = logging.getLogger()
19
+    root_logger.setLevel(logging.INFO)
20
+    
21
+    # 清除现有的处理器
22
+    root_logger.handlers.clear()
23
+    
24
+    # 1. 控制台处理器
25
+    console_handler = logging.StreamHandler()
26
+    console_handler.setLevel(logging.INFO)
27
+    
28
+    # 2. 文件处理器 - 轮转,最大10MB,保留5个备份
29
+    file_handler = logging.handlers.RotatingFileHandler(  # ✅ 使用完整的路径
30
+        main_log_file,
31
+        maxBytes=10 * 1024 * 1024,  # 10MB
32
+        backupCount=5,
33
+        encoding='utf-8'
34
+    )
35
+    file_handler.setLevel(logging.DEBUG)
36
+    
37
+    # 3. 错误日志处理器 - 单独记录错误
38
+    error_log_file = os.path.join(log_dir, "error.log")
39
+    error_handler = logging.handlers.RotatingFileHandler(
40
+        error_log_file,
41
+        maxBytes=5 * 1024 * 1024,  # 5MB
42
+        backupCount=3,
43
+        encoding='utf-8'
44
+    )
45
+    error_handler.setLevel(logging.ERROR)
46
+    
47
+    # 设置日志格式
48
+    detailed_formatter = logging.Formatter(
49
+        '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s',
50
+        datefmt='%Y-%m-%d %H:%M:%S'
51
+    )
52
+    
53
+    simple_formatter = logging.Formatter(
54
+        '%(asctime)s - %(levelname)s - %(message)s',
55
+        datefmt='%H:%M:%S'
56
+    )
57
+    
58
+    console_handler.setFormatter(simple_formatter)
59
+    file_handler.setFormatter(detailed_formatter)
60
+    error_handler.setFormatter(detailed_formatter)
61
+    
62
+    # 添加过滤器到错误处理器
63
+    class ErrorFilter(logging.Filter):
64
+        def filter(self, record):
65
+            return record.levelno >= logging.ERROR
66
+    
67
+    error_handler.addFilter(ErrorFilter())
68
+    
69
+    # 添加处理器
70
+    root_logger.addHandler(console_handler)
71
+    root_logger.addHandler(file_handler)
72
+    root_logger.addHandler(error_handler)
73
+    
74
+    # 配置第三方库的日志级别
75
+    logging.getLogger("uvicorn").setLevel(logging.WARNING)
76
+    logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
77
+    logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
78
+    logging.getLogger("sqlalchemy.orm").setLevel(logging.WARNING)
79
+
80
+    # 配置我们应用的日志级别
81
+    logging.getLogger("app").setLevel(logging.DEBUG)
82
+    logging.getLogger("app.api.v1").setLevel(logging.DEBUG)
83
+    logging.getLogger("app.core.security").setLevel(logging.DEBUG)
84
+    
85
+    root_logger.info(f"✅ 轮转日志系统初始化完成")
86
+    root_logger.info(f"   主日志文件: {main_log_file}")
87
+    root_logger.info(f"   错误日志文件: {error_log_file}")
88
+    
89
+    return root_logger

+ 0
- 0
app/logs/__init__.py View File


+ 2892
- 0
app/logs/app.log
File diff suppressed because it is too large
View File


+ 83
- 0
app/main.py View File

@@ -0,0 +1,83 @@
1
+# app/main.py - 简化版本
2
+from fastapi import FastAPI
3
+from fastapi.middleware.cors import CORSMiddleware
4
+from app.logging_config import setup_logging_with_rotation
5
+import logging
6
+import os
7
+
8
+# 配置
9
+from .config import settings
10
+
11
+# 配置日志
12
+logging.basicConfig(
13
+    level=logging.INFO,
14
+    # format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
15
+    # datefmt='%Y-%m-%d %H:%M:%S',
16
+    # handlers=[
17
+    #     logging.StreamHandler(),  # 控制台
18
+    #     logging.FileHandler('app/logs/app.log', encoding='utf-8')  # 文件
19
+    # ]
20
+)
21
+# 为特定模块设置更详细的日志
22
+logging.getLogger("app.api.v1").setLevel(logging.DEBUG)
23
+logging.getLogger("app.core.security").setLevel(logging.DEBUG)
24
+
25
+logger = logging.getLogger(__name__)
26
+logger.info("✅ 日志配置完成")
27
+
28
+# logger = setup_logging_with_rotation
29
+
30
+
31
+
32
+# 创建 FastAPI 应用
33
+app = FastAPI(
34
+    title=settings.PROJECT_NAME,
35
+    version=settings.VERSION,
36
+    docs_url="/docs" if settings.DEBUG else None,
37
+    redoc_url="/redoc" if settings.DEBUG else None
38
+)
39
+
40
+# 配置CORS
41
+if settings.BACKEND_CORS_ORIGINS:
42
+    app.add_middleware(
43
+        CORSMiddleware,
44
+        allow_origins=settings.BACKEND_CORS_ORIGINS,
45
+        allow_credentials=True,
46
+        allow_methods=["*"],
47
+        allow_headers=["*"],
48
+    )
49
+
50
+# 导入并注册路由
51
+try:
52
+    # 导入所有路由模块
53
+    # from .api.v1.admin import router as admin_router
54
+    from .api.v1.auth import router as auth_router
55
+    from .api.v1.users import router as users_router
56
+    # from .api.v1.verify import router as verify_router
57
+    
58
+    # 注册路由
59
+    app.include_router(auth_router, prefix=settings.API_V1_PREFIX)
60
+    app.include_router(users_router, prefix=settings.API_V1_PREFIX)
61
+    # app.include_router(verify_router, prefix=settings.API_V1_PREFIX)
62
+    
63
+    logger.info("✅ API 路由注册成功")
64
+    
65
+except ImportError as e:
66
+    logger.warning(f"⚠️  部分路由模块未找到: {e}")
67
+
68
+# 系统级路由
69
+@app.get("/health")
70
+async def health_check():
71
+    return {"status": "healthy", "service": settings.PROJECT_NAME}
72
+
73
+@app.get("/")
74
+async def root():
75
+    return {
76
+        "message": f"Welcome to {settings.PROJECT_NAME} API",
77
+        "version": settings.VERSION,
78
+        "docs": "/docs"
79
+    }
80
+
81
+if __name__ == "__main__":
82
+    import uvicorn
83
+    uvicorn.run(app, host="0.0.0.0", port=10003, reload=settings.DEBUG)

+ 0
- 0
app/models/__init__.py View File


BIN
app/models/__pycache__/__init__.cpython-311.pyc View File


BIN
app/models/__pycache__/token.cpython-311.pyc View File


BIN
app/models/__pycache__/user.cpython-311.pyc View File


+ 0
- 0
app/models/role.py View File


+ 31
- 0
app/models/token.py View File

@@ -0,0 +1,31 @@
1
+from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
2
+from sqlalchemy.orm import relationship
3
+from sqlalchemy.sql import func
4
+from datetime import datetime
5
+from ..database import Base
6
+
7
+class Token(Base):
8
+    __tablename__ = "tokens"
9
+    
10
+    id = Column(Integer, primary_key=True, index=True)
11
+    
12
+    # Token信息
13
+    token = Column(String(500), nullable=False, index=True)
14
+    token_type = Column(String(50), nullable=False)  # access, refresh, verify, reset
15
+    expires_at = Column(DateTime, nullable=False)
16
+    is_revoked = Column(Boolean, default=False)
17
+    
18
+    # 用户关联
19
+    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
20
+    user = relationship("User", back_populates="tokens")
21
+    
22
+    # 额外信息
23
+    ip_address = Column(String(45), nullable=True)
24
+    user_agent = Column(String(500), nullable=True)
25
+    created_at = Column(DateTime(timezone=True), server_default=func.now())
26
+    
27
+    def is_expired(self):
28
+        return datetime.utcnow() > self.expires_at
29
+    
30
+    def __repr__(self):
31
+        return f"<Token {self.token_type} for user {self.user_id}>"

+ 39
- 0
app/models/user.py View File

@@ -0,0 +1,39 @@
1
+# app/models/user.py
2
+from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
3
+from sqlalchemy.sql import func
4
+from ..database import Base
5
+
6
+class User(Base):
7
+    __tablename__ = "users"
8
+    
9
+    id = Column(Integer, primary_key=True, index=True)
10
+    username = Column(String(50), unique=True, index=True, nullable=False)
11
+    email = Column(String(100), unique=True, index=True, nullable=False)
12
+    hashed_password = Column(String(255), nullable=False)
13
+    
14
+    # 用户状态
15
+    is_active = Column(Boolean, default=True)
16
+    is_verified = Column(Boolean, default=False)
17
+    is_superuser = Column(Boolean, default=False)
18
+    is_locked = Column(Boolean, default=False)
19
+    
20
+    # 个人信息
21
+    full_name = Column(String(100), nullable=True)
22
+    phone = Column(String(20), nullable=True)
23
+    avatar = Column(Text, nullable=True)
24
+    
25
+    # 时间戳
26
+    created_at = Column(DateTime(timezone=True), server_default=func.now())
27
+    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
28
+    last_login = Column(DateTime, nullable=True)
29
+    last_password_change = Column(DateTime, nullable=True)
30
+    
31
+    # 安全相关
32
+    failed_login_attempts = Column(Integer, default=0)
33
+    verification_token = Column(String(255), nullable=True)
34
+    verification_token_expires = Column(DateTime, nullable=True)
35
+    reset_token = Column(String(255), nullable=True)
36
+    reset_token_expires = Column(DateTime, nullable=True)
37
+    
38
+    def __repr__(self):
39
+        return f"<User {self.username}>"

+ 42
- 0
app/schemas/__init__.py View File

@@ -0,0 +1,42 @@
1
+# app/schemas/__init__.py
2
+# 导出所有 schemas
3
+
4
+# Token 相关
5
+from .token import (
6
+    Token,
7
+    TokenResponse,
8
+    TokenData,
9
+    TokenPayload,
10
+    RefreshTokenRequest,
11
+    TokenCreate
12
+)
13
+
14
+# User 相关
15
+from .user import (
16
+    UserBase,
17
+    UserCreate,
18
+    UserLogin,
19
+    UserUpdate,
20
+    UserResponse,
21
+    UserProfile,
22
+    PasswordChange
23
+)
24
+
25
+__all__ = [
26
+    # Token
27
+    "Token",
28
+    "TokenResponse",
29
+    "TokenData",
30
+    "TokenPayload",
31
+    "RefreshTokenRequest",
32
+    "TokenCreate",
33
+    
34
+    # User
35
+    "UserBase",
36
+    "UserCreate",
37
+    "UserLogin",
38
+    "UserUpdate",
39
+    "UserResponse",
40
+    "UserProfile",
41
+    "PasswordChange",
42
+]

BIN
app/schemas/__pycache__/__init__.cpython-311.pyc View File


BIN
app/schemas/__pycache__/auth.cpython-311.pyc View File


BIN
app/schemas/__pycache__/token.cpython-311.pyc View File


BIN
app/schemas/__pycache__/user.cpython-311.pyc View File


+ 41
- 0
app/schemas/auth.py View File

@@ -0,0 +1,41 @@
1
+from pydantic import BaseModel, Field
2
+from typing import Optional
3
+from datetime import datetime
4
+from .user import UserResponse
5
+
6
+class TokenBase(BaseModel):
7
+    access_token: str
8
+    refresh_token: Optional[str] = None
9
+    token_type: str = "bearer"
10
+    expires_in: int
11
+
12
+class TokenResponse(TokenBase):
13
+    user: UserResponse
14
+
15
+class LoginRequest(BaseModel):
16
+    username: str = Field(..., min_length=1)
17
+    password: str = Field(..., min_length=1)
18
+
19
+class EmailVerifyRequest(BaseModel):
20
+    email: str
21
+
22
+class ResetPasswordRequest(BaseModel):
23
+    token: str
24
+    new_password: str = Field(..., min_length=8, max_length=100)
25
+    confirm_password: str
26
+    
27
+    class Config:
28
+        schema_extra = {
29
+            "example": {
30
+                "username": "john_doe",
31
+                "email": "user@example.com"
32
+            }
33
+        }
34
+
35
+class RefreshTokenRequest(BaseModel):
36
+    refresh_token: str
37
+
38
+class OAuth2Request(BaseModel):
39
+    provider: str  # google, github, facebook
40
+    code: str
41
+    redirect_uri: str

+ 48
- 0
app/schemas/token.py View File

@@ -0,0 +1,48 @@
1
+# app/schemas/token.py
2
+from pydantic import BaseModel, Field
3
+from typing import Optional
4
+from datetime import datetime
5
+
6
+class Token(BaseModel):
7
+    """令牌响应模型"""
8
+    access_token: str
9
+    token_type: str = "bearer"
10
+    expires_in: Optional[int] = None
11
+    refresh_token: Optional[str] = None
12
+
13
+class TokenResponse(Token):
14
+    """带用户信息的令牌响应"""
15
+    user: dict
16
+
17
+class TokenData(BaseModel):
18
+    """令牌数据模型"""
19
+    username: Optional[str] = None
20
+    user_id: Optional[int] = None
21
+    email: Optional[str] = None
22
+    exp: Optional[int] = None
23
+    iat: Optional[int] = None
24
+
25
+class TokenPayload(BaseModel):
26
+    """令牌载荷"""
27
+    sub: Optional[str] = None
28
+    exp: Optional[datetime] = None
29
+
30
+class RefreshTokenRequest(BaseModel):
31
+    """刷新令牌请求"""
32
+    refresh_token: str
33
+
34
+class TokenCreate(BaseModel):
35
+    """创建令牌请求"""
36
+    user_id: int
37
+    username: str
38
+    email: str
39
+
40
+# 导出所有模型
41
+__all__ = [
42
+    "Token",
43
+    "TokenResponse",
44
+    "TokenData",
45
+    "TokenPayload",
46
+    "RefreshTokenRequest",
47
+    "TokenCreate"
48
+]

+ 68
- 0
app/schemas/user.py View File

@@ -0,0 +1,68 @@
1
+# app/schemas/user.py
2
+from pydantic import BaseModel, EmailStr, Field, validator
3
+from typing import Optional
4
+from datetime import datetime
5
+
6
+class UserBase(BaseModel):
7
+    """用户基础模型"""
8
+    username: str = Field(..., min_length=3, max_length=50, example="john_doe")
9
+    email: EmailStr = Field(..., example="user@example.com")
10
+    full_name: Optional[str] = Field(None, example="John Doe")
11
+
12
+class UserCreate(UserBase):
13
+    """创建用户模型"""
14
+    password: str = Field(..., min_length=6, example="password123")
15
+    password_confirm: str = Field(..., example="password123")
16
+    
17
+    @validator('password_confirm')
18
+    def passwords_match(cls, v, values, **kwargs):
19
+        if 'password' in values and v != values['password']:
20
+            raise ValueError('密码不匹配')
21
+        return v
22
+
23
+class UserLogin(BaseModel):
24
+    """用户登录模型"""
25
+    username: str = Field(..., example="john_doe")
26
+    password: str = Field(..., example="password123")
27
+
28
+class UserUpdate(BaseModel):
29
+    """更新用户模型"""
30
+    full_name: Optional[str] = None
31
+    email: Optional[EmailStr] = None
32
+
33
+class UserResponse(UserBase):
34
+    """用户响应模型"""
35
+    id: int
36
+    is_active: bool = True
37
+    is_verified: bool = False
38
+    created_at: datetime
39
+    
40
+    class Config:
41
+        from_attributes = True
42
+
43
+class UserProfile(UserResponse):
44
+    """用户详情模型"""
45
+    last_login: Optional[datetime] = None
46
+    avatar: Optional[str] = None
47
+
48
+class PasswordChange(BaseModel):
49
+    """修改密码模型"""
50
+    current_password: str
51
+    new_password: str = Field(..., min_length=6)
52
+    confirm_password: str
53
+    
54
+    @validator('confirm_password')
55
+    def passwords_match(cls, v, values, **kwargs):
56
+        if 'new_password' in values and v != values['new_password']:
57
+            raise ValueError('新密码不匹配')
58
+        return v
59
+
60
+__all__ = [
61
+    "UserBase",
62
+    "UserCreate",
63
+    "UserLogin",
64
+    "UserUpdate",
65
+    "UserResponse",
66
+    "UserProfile",
67
+    "PasswordChange"
68
+]

+ 0
- 0
app/services/__init__.py View File


BIN
app/services/__pycache__/__init__.cpython-311.pyc View File


BIN
app/services/__pycache__/auth_service.cpython-311.pyc View File


BIN
app/services/__pycache__/user_service.cpython-311.pyc View File


+ 498
- 0
app/services/auth_service.py View File

@@ -0,0 +1,498 @@
1
+from typing import Optional, Dict, Any, Tuple
2
+from datetime import datetime, timedelta
3
+from sqlalchemy.orm import Session
4
+from fastapi import HTTPException, status, BackgroundTasks
5
+import secrets
6
+import string
7
+
8
+from ..models.user import User
9
+from ..models.token import Token
10
+from ..schemas.auth import LoginRequest, TokenResponse
11
+from ..core.security import (
12
+    verify_password,
13
+    create_access_token,
14
+    create_refresh_token,
15
+    create_verification_token,
16
+    create_reset_token,
17
+    decode_token,
18
+    generate_verification_code
19
+)
20
+from ..core.email import email_service
21
+from ..config import settings
22
+
23
+class AuthService:
24
+    def __init__(self, db: Session):
25
+        self.db = db
26
+    
27
+    async def register_user(
28
+        self,
29
+        user_data: Dict[str, Any],
30
+        background_tasks: BackgroundTasks,
31
+        ip_address: Optional[str] = None,
32
+        user_agent: Optional[str] = None
33
+    ) -> User:
34
+        """注册新用户"""
35
+        # 检查用户是否存在
36
+        existing_user = self.db.query(User).filter(
37
+            (User.username == user_data["username"]) | 
38
+            (User.email == user_data["email"])
39
+        ).first()
40
+        
41
+        if existing_user:
42
+            if existing_user.username == user_data["username"]:
43
+                raise HTTPException(
44
+                    status_code=status.HTTP_400_BAD_REQUEST,
45
+                    detail="Username already registered"
46
+                )
47
+            else:
48
+                raise HTTPException(
49
+                    status_code=status.HTTP_400_BAD_REQUEST,
50
+                    detail="Email already registered"
51
+                )
52
+        
53
+        # 创建用户
54
+        from ..core.security import get_password_hash
55
+        hashed_password = get_password_hash(user_data["password"])
56
+        
57
+        user = User(
58
+            username=user_data["username"],
59
+            email=user_data["email"],
60
+            hashed_password=hashed_password,
61
+            first_name=user_data.get("first_name"),
62
+            last_name=user_data.get("last_name"),
63
+            is_active=False,  # 需要邮箱验证
64
+            is_verified=False
65
+        )
66
+        
67
+        # 生成验证码
68
+        verification_code = generate_verification_code()
69
+        user.verification_code = verification_code
70
+        user.verification_code_expires = datetime.utcnow() + timedelta(hours=24)
71
+        
72
+        self.db.add(user)
73
+        self.db.commit()
74
+        self.db.refresh(user)
75
+        
76
+        # 发送验证邮件(后台任务)
77
+        verification_token = create_verification_token(user.email)
78
+        verification_url = f"{settings.FRONTEND_URL}/verify-email?token={verification_token}"
79
+        
80
+        background_tasks.add_task(
81
+            email_service.send_verification_email,
82
+            user.email,
83
+            user.username,
84
+            verification_url,
85
+            verification_code
86
+        )
87
+        
88
+        return user
89
+    
90
+    async def login(
91
+        self,
92
+        login_data: LoginRequest,
93
+        ip_address: Optional[str] = None,
94
+        user_agent: Optional[str] = None
95
+    ) -> Tuple[TokenResponse, User]:
96
+        """用户登录"""
97
+        user = self.db.query(User).filter(
98
+            (User.username == login_data.username) | 
99
+            (User.email == login_data.username)
100
+        ).first()
101
+        
102
+        if not user:
103
+            raise HTTPException(
104
+                status_code=status.HTTP_401_UNAUTHORIZED,
105
+                detail="Incorrect username or password"
106
+            )
107
+        
108
+        # 检查账户是否被锁定
109
+        if user.is_locked and user.locked_until and user.locked_until > datetime.utcnow():
110
+            raise HTTPException(
111
+                status_code=status.HTTP_423_LOCKED,
112
+                detail=f"Account is locked until {user.locked_until}"
113
+            )
114
+        
115
+        # 验证密码
116
+        if not verify_password(login_data.password, user.hashed_password):
117
+            # 记录失败尝试
118
+            user.failed_login_attempts += 1
119
+            
120
+            # 如果失败次数超过5次,锁定账户
121
+            if user.failed_login_attempts >= 5:
122
+                user.is_locked = True
123
+                user.locked_until = datetime.utcnow() + timedelta(minutes=30)
124
+            
125
+            self.db.commit()
126
+            
127
+            raise HTTPException(
128
+                status_code=status.HTTP_401_UNAUTHORIZED,
129
+                detail="Incorrect username or password"
130
+            )
131
+        
132
+        # 检查邮箱是否已验证
133
+        if not user.is_verified:
134
+            raise HTTPException(
135
+                status_code=status.HTTP_403_FORBIDDEN,
136
+                detail="Email not verified"
137
+            )
138
+        
139
+        # 检查账户是否激活
140
+        if not user.is_active:
141
+            raise HTTPException(
142
+                status_code=status.HTTP_403_FORBIDDEN,
143
+                detail="Account is not active"
144
+            )
145
+        
146
+        # 重置失败尝试次数
147
+        user.failed_login_attempts = 0
148
+        user.last_login = datetime.utcnow()
149
+        user.is_locked = False
150
+        user.locked_until = None
151
+        self.db.commit()
152
+        
153
+        # 创建令牌
154
+        access_token = create_access_token({"sub": user.username, "user_id": user.id})
155
+        refresh_token = create_refresh_token({"sub": user.username, "user_id": user.id})
156
+        
157
+        # 保存刷新令牌到数据库
158
+        refresh_token_entry = Token(
159
+            token=refresh_token,
160
+            token_type="refresh",
161
+            expires_at=datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),
162
+            user_id=user.id,
163
+            ip_address=ip_address,
164
+            user_agent=user_agent
165
+        )
166
+        
167
+        self.db.add(refresh_token_entry)
168
+        self.db.commit()
169
+        
170
+        # 构建响应
171
+        token_response = TokenResponse(
172
+            access_token=access_token,
173
+            refresh_token=refresh_token,
174
+            expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
175
+            user=user
176
+        )
177
+        
178
+        return token_response, user
179
+    
180
+    async def verify_email(
181
+        self,
182
+        token: str,
183
+        code: Optional[str] = None
184
+    ) -> bool:
185
+        """验证邮箱"""
186
+        payload = decode_token(token)
187
+        
188
+        if not payload or payload.get("type") != "verify":
189
+            raise HTTPException(
190
+                status_code=status.HTTP_400_BAD_REQUEST,
191
+                detail="Invalid verification token"
192
+            )
193
+        
194
+        email = payload.get("email")
195
+        if not email:
196
+            raise HTTPException(
197
+                status_code=status.HTTP_400_BAD_REQUEST,
198
+                detail="Invalid token payload"
199
+            )
200
+        
201
+        user = self.db.query(User).filter(User.email == email).first()
202
+        
203
+        if not user:
204
+            raise HTTPException(
205
+                status_code=status.HTTP_404_NOT_FOUND,
206
+                detail="User not found"
207
+            )
208
+        
209
+        if user.is_verified:
210
+            raise HTTPException(
211
+                status_code=status.HTTP_400_BAD_REQUEST,
212
+                detail="Email already verified"
213
+            )
214
+        
215
+        # 验证码验证
216
+        if code:
217
+            if (not user.verification_code or 
218
+                user.verification_code != code or
219
+                not user.verification_code_expires or
220
+                user.verification_code_expires < datetime.utcnow()):
221
+                raise HTTPException(
222
+                    status_code=status.HTTP_400_BAD_REQUEST,
223
+                    detail="Invalid or expired verification code"
224
+                )
225
+        
226
+        # 更新用户状态
227
+        user.is_verified = True
228
+        user.is_active = True
229
+        user.verification_code = None
230
+        user.verification_code_expires = None
231
+        self.db.commit()
232
+        
233
+        # 发送欢迎邮件
234
+        await email_service.send_welcome_email(user.email, user.username)
235
+        
236
+        return True
237
+    
238
+    async def resend_verification_email(
239
+        self,
240
+        email: str,
241
+        background_tasks: BackgroundTasks
242
+    ) -> bool:
243
+        """重新发送验证邮件"""
244
+        user = self.db.query(User).filter(User.email == email).first()
245
+        
246
+        if not user:
247
+            # 出于安全考虑,即使用户不存在也返回成功
248
+            return True
249
+        
250
+        if user.is_verified:
251
+            raise HTTPException(
252
+                status_code=status.HTTP_400_BAD_REQUEST,
253
+                detail="Email already verified"
254
+            )
255
+        
256
+        # 生成新的验证码
257
+        verification_code = generate_verification_code()
258
+        user.verification_code = verification_code
259
+        user.verification_code_expires = datetime.utcnow() + timedelta(hours=24)
260
+        
261
+        self.db.commit()
262
+        
263
+        # 发送验证邮件
264
+        verification_token = create_verification_token(user.email)
265
+        verification_url = f"{settings.FRONTEND_URL}/verify-email?token={verification_token}"
266
+        
267
+        background_tasks.add_task(
268
+            email_service.send_verification_email,
269
+            user.email,
270
+            user.username,
271
+            verification_url,
272
+            verification_code
273
+        )
274
+        
275
+        return True
276
+    
277
+    async def request_password_reset(
278
+        self,
279
+        email: str,
280
+        background_tasks: BackgroundTasks
281
+    ) -> bool:
282
+        """请求密码重置"""
283
+        user = self.db.query(User).filter(User.email == email).first()
284
+        
285
+        if not user:
286
+            # 出于安全考虑,即使用户不存在也返回成功
287
+            return True
288
+        
289
+        if not user.is_active:
290
+            raise HTTPException(
291
+                status_code=status.HTTP_400_BAD_REQUEST,
292
+                detail="Account is not active"
293
+            )
294
+        
295
+        # 生成重置令牌
296
+        reset_token = create_reset_token(user.email)
297
+        reset_url = f"{settings.FRONTEND_URL}/reset-password?token={reset_token}"
298
+        
299
+        # 保存重置令牌到数据库
300
+        reset_token_entry = Token(
301
+            token=reset_token,
302
+            token_type="reset",
303
+            expires_at=datetime.utcnow() + timedelta(minutes=settings.RESET_TOKEN_EXPIRE_MINUTES),
304
+            user_id=user.id
305
+        )
306
+        
307
+        self.db.add(reset_token_entry)
308
+        self.db.commit()
309
+        
310
+        # 发送重置邮件
311
+        background_tasks.add_task(
312
+            email_service.send_password_reset_email,
313
+            user.email,
314
+            user.username,
315
+            reset_url
316
+        )
317
+        
318
+        return True
319
+    
320
+    async def reset_password(
321
+        self,
322
+        token: str,
323
+        new_password: str
324
+    ) -> bool:
325
+        """重置密码"""
326
+        # 验证令牌
327
+        payload = decode_token(token)
328
+        
329
+        if not payload or payload.get("type") != "reset":
330
+            raise HTTPException(
331
+                status_code=status.HTTP_400_BAD_REQUEST,
332
+                detail="Invalid reset token"
333
+            )
334
+        
335
+        email = payload.get("email")
336
+        if not email:
337
+            raise HTTPException(
338
+                status_code=status.HTTP_400_BAD_REQUEST,
339
+                detail="Invalid token payload"
340
+            )
341
+        
342
+        # 检查令牌是否在数据库中且未过期
343
+        token_entry = self.db.query(Token).filter(
344
+            Token.token == token,
345
+            Token.token_type == "reset",
346
+            Token.is_revoked == False,
347
+            Token.expires_at > datetime.utcnow()
348
+        ).first()
349
+        
350
+        if not token_entry:
351
+            raise HTTPException(
352
+                status_code=status.HTTP_400_BAD_REQUEST,
353
+                detail="Invalid or expired reset token"
354
+            )
355
+        
356
+        user = self.db.query(User).filter(User.email == email).first()
357
+        
358
+        if not user:
359
+            raise HTTPException(
360
+                status_code=status.HTTP_404_NOT_FOUND,
361
+                detail="User not found"
362
+            )
363
+        
364
+        if not user.is_active:
365
+            raise HTTPException(
366
+                status_code=status.HTTP_400_BAD_REQUEST,
367
+                detail="Account is not active"
368
+            )
369
+        
370
+        # 更新密码
371
+        from ..core.security import get_password_hash
372
+        user.hashed_password = get_password_hash(new_password)
373
+        user.last_password_change = datetime.utcnow()
374
+        
375
+        # 撤销所有现有令牌
376
+        self.db.query(Token).filter(
377
+            Token.user_id == user.id,
378
+            Token.token_type.in_(["access", "refresh"])
379
+        ).update({"is_revoked": True})
380
+        
381
+        # 标记重置令牌为已使用
382
+        token_entry.is_revoked = True
383
+        
384
+        self.db.commit()
385
+        
386
+        return True
387
+    
388
+    async def refresh_token(
389
+        self,
390
+        refresh_token: str,
391
+        ip_address: Optional[str] = None,
392
+        user_agent: Optional[str] = None
393
+    ) -> TokenResponse:
394
+        """刷新访问令牌"""
395
+        # 验证刷新令牌
396
+        payload = decode_token(refresh_token)
397
+        
398
+        if not payload or payload.get("type") != "refresh":
399
+            raise HTTPException(
400
+                status_code=status.HTTP_401_UNAUTHORIZED,
401
+                detail="Invalid refresh token"
402
+            )
403
+        
404
+        # 检查令牌是否在数据库中且有效
405
+        token_entry = self.db.query(Token).filter(
406
+            Token.token == refresh_token,
407
+            Token.token_type == "refresh",
408
+            Token.is_revoked == False,
409
+            Token.expires_at > datetime.utcnow()
410
+        ).first()
411
+        
412
+        if not token_entry:
413
+            raise HTTPException(
414
+                status_code=status.HTTP_401_UNAUTHORIZED,
415
+                detail="Invalid refresh token"
416
+            )
417
+        
418
+        username = payload.get("sub")
419
+        user_id = payload.get("user_id")
420
+        
421
+        if not username or not user_id:
422
+            raise HTTPException(
423
+                status_code=status.HTTP_401_UNAUTHORIZED,
424
+                detail="Invalid token payload"
425
+            )
426
+        
427
+        user = self.db.query(User).filter(
428
+            User.id == user_id,
429
+            User.username == username,
430
+            User.is_active == True
431
+        ).first()
432
+        
433
+        if not user:
434
+            raise HTTPException(
435
+                status_code=status.HTTP_401_UNAUTHORIZED,
436
+                detail="User not found or inactive"
437
+            )
438
+        
439
+        # 创建新的访问令牌
440
+        access_token = create_access_token({"sub": user.username, "user_id": user.id})
441
+        
442
+        # 可选:创建新的刷新令牌(刷新令牌轮换)
443
+        new_refresh_token = create_refresh_token({"sub": user.username, "user_id": user.id})
444
+        
445
+        # 保存新的刷新令牌
446
+        new_refresh_token_entry = Token(
447
+            token=new_refresh_token,
448
+            token_type="refresh",
449
+            expires_at=datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),
450
+            user_id=user.id,
451
+            ip_address=ip_address,
452
+            user_agent=user_agent
453
+        )
454
+        
455
+        # 标记旧令牌为已撤销
456
+        token_entry.is_revoked = True
457
+        
458
+        self.db.add(new_refresh_token_entry)
459
+        self.db.commit()
460
+        
461
+        # 构建响应
462
+        return TokenResponse(
463
+            access_token=access_token,
464
+            refresh_token=new_refresh_token,
465
+            expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
466
+            user=user
467
+        )
468
+    
469
+    async def logout(
470
+        self,
471
+        refresh_token: str
472
+    ) -> bool:
473
+        """用户登出"""
474
+        # 撤销刷新令牌
475
+        token_entry = self.db.query(Token).filter(
476
+            Token.token == refresh_token,
477
+            Token.token_type == "refresh"
478
+        ).first()
479
+        
480
+        if token_entry:
481
+            token_entry.is_revoked = True
482
+            self.db.commit()
483
+        
484
+        return True
485
+    
486
+    async def logout_all(
487
+        self,
488
+        user_id: int
489
+    ) -> bool:
490
+        """撤销用户的所有令牌"""
491
+        self.db.query(Token).filter(
492
+            Token.user_id == user_id,
493
+            Token.token_type.in_(["access", "refresh"]),
494
+            Token.is_revoked == False
495
+        ).update({"is_revoked": True})
496
+        
497
+        self.db.commit()
498
+        return True

+ 0
- 0
app/services/email_service.py View File


+ 169
- 0
app/services/user_service.py View File

@@ -0,0 +1,169 @@
1
+# app/services/user_service.py
2
+from sqlalchemy.orm import Session
3
+from sqlalchemy import or_
4
+from typing import Optional, List
5
+from datetime import datetime
6
+
7
+from ..models.user import User
8
+from ..core.security import password_hasher, password_validator
9
+from ..schemas.user import UserCreate, UserUpdate, UserResponse
10
+
11
+class UserService:
12
+    """用户服务"""
13
+    
14
+    def __init__(self, db: Session):
15
+        self.db = db
16
+    
17
+    async def create_user(self, user_data: UserCreate) -> User:
18
+        """创建用户"""
19
+        # 验证密码强度
20
+        is_valid, error_msg = password_validator.validate_password_strength(user_data.password)
21
+        if not is_valid:
22
+            raise ValueError(error_msg)
23
+        
24
+        # 检查用户是否已存在
25
+        existing_user = self.db.query(User).filter(
26
+            or_(
27
+                User.username == user_data.username,
28
+                User.email == user_data.email
29
+            )
30
+        ).first()
31
+        
32
+        if existing_user:
33
+            if existing_user.username == user_data.username:
34
+                raise ValueError("用户名已存在")
35
+            else:
36
+                raise ValueError("邮箱已被注册")
37
+        
38
+        # 哈希密码
39
+        hashed_password = password_hasher.hash_password(user_data.password)
40
+        
41
+        # 创建用户
42
+        user = User(
43
+            username=user_data.username,
44
+            email=user_data.email,
45
+            hashed_password=hashed_password,
46
+            full_name=user_data.full_name,
47
+            is_active=True,
48
+            is_verified=False
49
+        )
50
+        
51
+        self.db.add(user)
52
+        self.db.commit()
53
+        self.db.refresh(user)
54
+        
55
+        return user
56
+    
57
+    async def get_user_by_id(self, user_id: int) -> Optional[User]:
58
+        """通过ID获取用户"""
59
+        return self.db.query(User).filter(User.id == user_id).first()
60
+    
61
+    async def get_user_by_username(self, username: str) -> Optional[User]:
62
+        """通过用户名获取用户"""
63
+        return self.db.query(User).filter(User.username == username).first()
64
+    
65
+    async def get_user_by_email(self, email: str) -> Optional[User]:
66
+        """通过邮箱获取用户"""
67
+        return self.db.query(User).filter(User.email == email).first()
68
+    
69
+    async def authenticate_user(self, username: str, password: str) -> Optional[User]:
70
+        """验证用户"""
71
+        # 通过用户名或邮箱查找用户
72
+        user = self.db.query(User).filter(
73
+            or_(
74
+                User.username == username,
75
+                User.email == username
76
+            )
77
+        ).first()
78
+        
79
+        if not user:
80
+            return None
81
+        
82
+        if not password_hasher.verify_password(password, user.hashed_password):
83
+            # 记录失败尝试
84
+            user.failed_login_attempts += 1
85
+            if user.failed_login_attempts >= 5:
86
+                user.is_locked = True
87
+            self.db.commit()
88
+            return None
89
+        
90
+        # 重置失败尝试
91
+        user.failed_login_attempts = 0
92
+        user.last_login = datetime.now()
93
+        user.is_locked = False
94
+        self.db.commit()
95
+        
96
+        return user
97
+    
98
+    async def update_user(self, user_id: int, update_data: dict) -> Optional[User]:
99
+        """更新用户信息"""
100
+        user = await self.get_user_by_id(user_id)
101
+        if not user:
102
+            return None
103
+        
104
+        for key, value in update_data.items():
105
+            if hasattr(user, key) and value is not None:
106
+                setattr(user, key, value)
107
+        
108
+        user.updated_at = datetime.now()
109
+        self.db.commit()
110
+        self.db.refresh(user)
111
+        
112
+        return user
113
+    
114
+    async def change_password(self, user_id: int, current_password: str, new_password: str) -> bool:
115
+        """修改密码"""
116
+        user = await self.get_user_by_id(user_id)
117
+        if not user:
118
+            return False
119
+        
120
+        # 验证当前密码
121
+        if not password_hasher.verify_password(current_password, user.hashed_password):
122
+            return False
123
+        
124
+        # 验证新密码强度
125
+        is_valid, error_msg = password_validator.validate_password_strength(new_password)
126
+        if not is_valid:
127
+            raise ValueError(error_msg)
128
+        
129
+        # 更新密码
130
+        user.hashed_password = password_hasher.hash_password(new_password)
131
+        user.last_password_change = datetime.now()
132
+        self.db.commit()
133
+        
134
+        return True
135
+    
136
+    async def list_users(
137
+        self, 
138
+        skip: int = 0, 
139
+        limit: int = 100,
140
+        active_only: bool = True
141
+    ) -> List[User]:
142
+        """列出用户"""
143
+        query = self.db.query(User)
144
+        
145
+        if active_only:
146
+            query = query.filter(User.is_active == True)
147
+        
148
+        return query.offset(skip).limit(limit).all()
149
+    
150
+    async def delete_user(self, user_id: int) -> bool:
151
+        """删除用户(软删除)"""
152
+        user = await self.get_user_by_id(user_id)
153
+        if not user:
154
+            return False
155
+        
156
+        user.is_active = False
157
+        user.updated_at = datetime.now()
158
+        self.db.commit()
159
+        
160
+        return True
161
+    
162
+    async def count_users(self, active_only: bool = True) -> int:
163
+        """统计用户数量"""
164
+        query = self.db.query(User)
165
+        
166
+        if active_only:
167
+            query = query.filter(User.is_active == True)
168
+        
169
+        return query.count()

+ 0
- 0
app/utils/__init__.py View File


+ 0
- 0
app/utils/jwt.py View File


+ 0
- 0
app/utils/validators.py View File


BIN
caiyouhui.db View File


+ 13
- 0
init_db.py View File

@@ -0,0 +1,13 @@
1
+# init_db.py
2
+import sys
3
+import os
4
+
5
+# 添加项目根目录到 Python 路径
6
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
7
+
8
+from app.database import init_db
9
+
10
+if __name__ == "__main__":
11
+    print("🔄 正在初始化数据库...")
12
+    init_db()
13
+    print("✅ 数据库初始化完成!")

+ 7
- 0
requirements.txt View File

@@ -0,0 +1,7 @@
1
+# fastapi==0.104.1
2
+# uvicorn[standard]==0.24.0
3
+# sqlalchemy==2.0.23
4
+# pydantic==2.5.0
5
+# python-dotenv==1.0.0
6
+# python-multipart==0.0.6
7
+# werkzeug==3.0.1  # 替代 passlib

+ 86
- 0
templates/email/verify_email.html View File

@@ -0,0 +1,86 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+    <meta charset="utf-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+    <title>Verify Your Email</title>
7
+    <style>
8
+        body {
9
+            font-family: Arial, sans-serif;
10
+            line-height: 1.6;
11
+            color: #333;
12
+            max-width: 600px;
13
+            margin: 0 auto;
14
+            padding: 20px;
15
+        }
16
+        .container {
17
+            background-color: #f9f9f9;
18
+            border-radius: 10px;
19
+            padding: 30px;
20
+            margin-top: 20px;
21
+        }
22
+        .header {
23
+            text-align: center;
24
+            margin-bottom: 30px;
25
+        }
26
+        .logo {
27
+            max-width: 150px;
28
+            margin-bottom: 20px;
29
+        }
30
+        .button {
31
+            display: inline-block;
32
+            background-color: #4CAF50;
33
+            color: white;
34
+            padding: 12px 24px;
35
+            text-decoration: none;
36
+            border-radius: 5px;
37
+            margin: 20px 0;
38
+        }
39
+        .code {
40
+            background-color: #f0f0f0;
41
+            padding: 10px;
42
+            border-radius: 5px;
43
+            font-family: monospace;
44
+            font-size: 18px;
45
+            text-align: center;
46
+            margin: 20px 0;
47
+        }
48
+        .footer {
49
+            margin-top: 30px;
50
+            text-align: center;
51
+            color: #777;
52
+            font-size: 12px;
53
+        }
54
+    </style>
55
+</head>
56
+<body>
57
+    <div class="container">
58
+        <div class="header">
59
+            <h1>Verify Your Email Address</h1>
60
+        </div>
61
+        
62
+        <p>Hello <strong>{{ username }}</strong>,</p>
63
+        
64
+        <p>Thank you for registering! Please verify your email address by clicking the button below:</p>
65
+        
66
+        <div style="text-align: center;">
67
+            <a href="{{ verification_url }}" class="button">Verify Email Address</a>
68
+        </div>
69
+        
70
+        {% if verification_code %}
71
+        <p>Or enter this verification code on the verification page:</p>
72
+        <div class="code">{{ verification_code }}</div>
73
+        {% endif %}
74
+        
75
+        <p>This link will expire in 24 hours. If you didn't create an account, you can safely ignore this email.</p>
76
+        
77
+        <p>If the button doesn't work, copy and paste this URL into your browser:</p>
78
+        <p><small>{{ verification_url }}</small></p>
79
+        
80
+        <div class="footer">
81
+            <p>© 2024 Your Company. All rights reserved.</p>
82
+            <p>This is an automated message, please do not reply to this email.</p>
83
+        </div>
84
+    </div>
85
+</body>
86
+</html>