fastv 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. fastv/__init__.py +396 -0
  2. fastv/auth/__init__.py +24 -0
  3. fastv/auth/auth_manager.py +122 -0
  4. fastv/auth/guard.py +127 -0
  5. fastv/auth/jwt_manager.py +80 -0
  6. fastv/auth/password_hasher.py +46 -0
  7. fastv/cache/__init__.py +3 -0
  8. fastv/cache/manager.py +273 -0
  9. fastv/config/__init__.py +120 -0
  10. fastv/console/__init__.py +6 -0
  11. fastv/console/application.py +75 -0
  12. fastv/console/caller.py +71 -0
  13. fastv/console/command.py +115 -0
  14. fastv/console/commands/__init__.py +1 -0
  15. fastv/console/commands/about.py +32 -0
  16. fastv/console/commands/cache_clear.py +46 -0
  17. fastv/console/commands/list_command.py +33 -0
  18. fastv/console/commands/make_command.py +43 -0
  19. fastv/console/commands/make_controller.py +72 -0
  20. fastv/console/commands/make_migration.py +30 -0
  21. fastv/console/commands/make_model.py +69 -0
  22. fastv/console/commands/migrate.py +28 -0
  23. fastv/console/commands/migrate_refresh.py +32 -0
  24. fastv/console/commands/migrate_rollback.py +29 -0
  25. fastv/console/commands/migrate_status.py +80 -0
  26. fastv/console/commands/start_server.py +79 -0
  27. fastv/console/output.py +165 -0
  28. fastv/console/registrar.py +114 -0
  29. fastv/console/signature_parser.py +115 -0
  30. fastv/container/__init__.py +3 -0
  31. fastv/container/container.py +151 -0
  32. fastv/controllers.py +122 -0
  33. fastv/database/__init__.py +14 -0
  34. fastv/database/builder.py +497 -0
  35. fastv/database/connection.py +221 -0
  36. fastv/database/migration.py +43 -0
  37. fastv/database/model.py +447 -0
  38. fastv/events/__init__.py +3 -0
  39. fastv/events/dispatcher.py +87 -0
  40. fastv/exceptions/__init__.py +93 -0
  41. fastv/exceptions/handler.py +181 -0
  42. fastv/http/__init__.py +4 -0
  43. fastv/http/middleware/__init__.py +3 -0
  44. fastv/http/middleware/pipeline.py +53 -0
  45. fastv/http/request.py +151 -0
  46. fastv/http/response.py +67 -0
  47. fastv/log/__init__.py +3 -0
  48. fastv/log/logger.py +146 -0
  49. fastv/queue/__init__.py +31 -0
  50. fastv/queue/asynctasq_integration.py +230 -0
  51. fastv/redis/__init__.py +10 -0
  52. fastv/redis/client.py +903 -0
  53. fastv/routing/__init__.py +3 -0
  54. fastv/routing/router.py +346 -0
  55. fastv/schedule/__init__.py +5 -0
  56. fastv/schedule/commands/__init__.py +1 -0
  57. fastv/schedule/commands/schedule_serve.py +105 -0
  58. fastv/schedule/runner.py +140 -0
  59. fastv/schedule/scheduler.py +256 -0
  60. fastv/support/__init__.py +8 -0
  61. fastv/support/helpers.py +48 -0
  62. fastv/support/str.py +46 -0
  63. fastv/validation/__init__.py +3 -0
  64. fastv/validation/validator.py +147 -0
  65. fastv-1.0.0.dist-info/METADATA +93 -0
  66. fastv-1.0.0.dist-info/RECORD +69 -0
  67. fastv-1.0.0.dist-info/WHEEL +5 -0
  68. fastv-1.0.0.dist-info/entry_points.txt +2 -0
  69. fastv-1.0.0.dist-info/top_level.txt +1 -0
fastv/__init__.py ADDED
@@ -0,0 +1,396 @@
1
+ """
2
+ Application - Framework core entry point.
3
+ """
4
+
5
+ import importlib
6
+ import os
7
+ import sys
8
+ from contextlib import asynccontextmanager
9
+ from typing import AsyncIterator, Callable, Coroutine, List
10
+
11
+ from fastapi import FastAPI
12
+
13
+ from fastv.config import Config
14
+ from fastv.container.container import Container
15
+ from fastv.support.helpers import set_application
16
+
17
+
18
+ class Application(Container):
19
+ _instance = None
20
+
21
+ def __new__(cls, base_dir=None):
22
+ if cls._instance is None:
23
+ cls._instance = super().__new__(cls)
24
+ cls._instance._initialized = False
25
+ return cls._instance
26
+
27
+ def __init__(self, base_dir=None):
28
+ if getattr(self, "_initialized", False):
29
+ return
30
+ super().__init__()
31
+ self.base_dir = base_dir or os.getcwd()
32
+ self._shutdown_callbacks: List[Callable[[], Coroutine]] = []
33
+ self._startup_callbacks: List[Callable[[], Coroutine]] = []
34
+ self.app = FastAPI(title="FastAPI Application")
35
+ self.config = Config(self.base_dir)
36
+ self.router = None
37
+ self._bootstrapped = False
38
+ self._initialized = True
39
+ set_application(self)
40
+
41
+ def _on_startup(self, callback: Callable[[], Coroutine]):
42
+ """注册 startup 回调(替代 @app.on_event("startup"))"""
43
+ self._startup_callbacks.append(callback)
44
+
45
+ def _on_shutdown(self, callback: Callable[[], Coroutine]):
46
+ """注册 shutdown 回调(替代 @app.on_event("shutdown"))"""
47
+ self._shutdown_callbacks.append(callback)
48
+
49
+ @asynccontextmanager
50
+ async def _lifespan(self, app: FastAPI) -> AsyncIterator[None]:
51
+ """FastAPI lifespan 上下文管理器"""
52
+ # 启动
53
+ for cb in self._startup_callbacks:
54
+ try:
55
+ await cb()
56
+ except Exception as e:
57
+ print(f"[Lifespan] startup callback failed: {e}")
58
+ yield
59
+ # 关闭
60
+ for cb in self._shutdown_callbacks:
61
+ try:
62
+ await cb()
63
+ except Exception as e:
64
+ print(f"[Lifespan] shutdown callback failed: {e}")
65
+
66
+ def bootstrap(self):
67
+ if self._bootstrapped:
68
+ return self.app
69
+ self._load_config()
70
+ self._register_services()
71
+ self._register_exception_handler()
72
+ self._init_redis()
73
+ self._init_cache()
74
+ self._init_logger()
75
+ self._init_database()
76
+ self._init_auth()
77
+ self._init_asynctasq()
78
+ self._discover_routes()
79
+ self._discover_middleware()
80
+ self._discover_events()
81
+ self._discover_commands()
82
+ # 注册 lifespan(替代 on_event)
83
+ self.app.router.lifespan_context = self._lifespan
84
+ self._bootstrapped = True
85
+ return self.app
86
+
87
+ def _load_config(self):
88
+ config_dir = os.path.join(self.base_dir, "config")
89
+ if os.path.isdir(config_dir):
90
+ self.config.load_from_dir(config_dir)
91
+ self.instance("config", self.config)
92
+
93
+ def _register_services(self):
94
+ from fastv.events.dispatcher import Dispatcher
95
+ from fastv.http.middleware.pipeline import Pipeline
96
+ from fastv.routing.router import Router
97
+ self.singleton("router", lambda: Router())
98
+ self.singleton("pipeline", lambda: Pipeline())
99
+ self.singleton("events", lambda: Dispatcher())
100
+ self.instance("app", self.app)
101
+
102
+ def _register_exception_handler(self):
103
+ from fastv.exceptions.handler import register_exception_handler
104
+ exc_config = self.config.get("exception", {})
105
+ register_exception_handler(self.app, exc_config)
106
+
107
+ def _discover_routes(self):
108
+ route_dir = os.path.join(self.base_dir, "app")
109
+ if not os.path.isdir(route_dir):
110
+ return
111
+ self._add_app_to_path()
112
+ route_count = 0
113
+ for root, dirs, files in os.walk(route_dir):
114
+ if os.path.basename(root) == "route":
115
+ for f in files:
116
+ if f.endswith(".py") and not f.startswith("_"):
117
+ rel_path = os.path.relpath(os.path.join(root, f), self.base_dir)
118
+ module_name = rel_path.replace(os.sep, ".").replace(".py", "")
119
+ try:
120
+ module = importlib.import_module(module_name)
121
+ if hasattr(module, "register"):
122
+ module.register(self.app)
123
+ route_count += 1
124
+ from fastv.routing.router import Router as PkgRouter
125
+ for name in dir(module):
126
+ obj = getattr(module, name)
127
+ if isinstance(obj, PkgRouter):
128
+ obj.register(self.app)
129
+ route_count += 1
130
+ except ImportError as e:
131
+ print("[Route] Failed:", module_name, e)
132
+ if route_count > 0:
133
+ print("[Route] Discovered", route_count, "route file(s)")
134
+
135
+ def _discover_middleware(self):
136
+ mw_config = self.config.get("middleware")
137
+ if mw_config:
138
+ self._load_middleware_from_config(mw_config)
139
+ else:
140
+ self._auto_discover_middleware()
141
+
142
+ def _load_middleware_from_config(self, mw_config: dict):
143
+ mw_count = 0
144
+ self._add_app_to_path()
145
+
146
+ # 全局中间件
147
+ for mw_path in mw_config.get("global_middleware", []):
148
+ cls = self._import_class(mw_path)
149
+ if cls:
150
+ self.app.middleware("http")(self._mw_adapter(cls))
151
+ mw_count += 1
152
+
153
+ # 模块中间件
154
+ for module_name, mw_list in mw_config.get("modules", {}).items():
155
+ for mw_path in mw_list:
156
+ cls = self._import_class(mw_path)
157
+ if cls:
158
+ self.app.middleware("http")(self._mw_adapter(cls))
159
+ mw_count += 1
160
+
161
+ if mw_count > 0:
162
+ print(f"[Middleware] Registered {mw_count} middleware(s) from config")
163
+
164
+ def _auto_discover_middleware(self):
165
+ app_dir = os.path.join(self.base_dir, "app")
166
+ if not os.path.isdir(app_dir):
167
+ return
168
+ self._add_app_to_path()
169
+ mw_count = 0
170
+ for root, dirs, files in os.walk(app_dir):
171
+ if os.path.basename(root) == "middleware":
172
+ for f in files:
173
+ if f.endswith(".py") and not f.startswith("_"):
174
+ rel_path = os.path.relpath(os.path.join(root, f), self.base_dir)
175
+ module_name = rel_path.replace(os.sep, ".").replace(".py", "")
176
+ try:
177
+ module = importlib.import_module(module_name)
178
+ for name in dir(module):
179
+ cls = getattr(module, name)
180
+ if isinstance(cls, type) and hasattr(cls, "__call__"):
181
+ self.app.middleware("http")(self._mw_adapter(cls))
182
+ mw_count += 1
183
+ except ImportError as e:
184
+ print("[Middleware] Failed:", module_name, e)
185
+ if mw_count > 0:
186
+ print(f"[Middleware] Auto-discovered {mw_count} middleware(s)")
187
+
188
+ @staticmethod
189
+ def _import_class(dotted_path: str):
190
+ module_name, class_name = dotted_path.rsplit(".", 1)
191
+ try:
192
+ module = importlib.import_module(module_name)
193
+ return getattr(module, class_name, None)
194
+ except ImportError as e:
195
+ print(f"[Middleware] Failed to import {dotted_path}: {e}")
196
+ return None
197
+
198
+ def _discover_events(self):
199
+ event_dir = os.path.join(self.base_dir, "app", "event")
200
+ if not os.path.isdir(event_dir):
201
+ return
202
+ self._add_app_to_path()
203
+ listener_count = 0
204
+ for f in os.listdir(event_dir):
205
+ if f.endswith(".py") and not f.startswith("_"):
206
+ modname = "app.event." + f[:-3]
207
+ try:
208
+ module = importlib.import_module(modname)
209
+ dispatcher = self.make("events")
210
+ if hasattr(module, "register"):
211
+ module.register(dispatcher)
212
+ listener_count += 1
213
+ for name in dir(module):
214
+ cls = getattr(module, name)
215
+ if isinstance(cls, type) and hasattr(cls, "subscribe"):
216
+ cls().subscribe(dispatcher)
217
+ listener_count += 1
218
+ except ImportError as e:
219
+ print("[Event] Failed:", modname, e)
220
+ if listener_count > 0:
221
+ print("[Event] Registered", listener_count, "event listener(s)")
222
+
223
+ def _init_database(self):
224
+ default = self.config.get("database.default", "sqlite")
225
+ connections = self.config.get("database.connections", {})
226
+ if not connections:
227
+ return
228
+ conn_config = connections.get(default, {})
229
+ if not conn_config:
230
+ return
231
+ from fastv.database.connection import ConnectionManager, get_db
232
+ db = get_db()
233
+ url = ConnectionManager.build_url(conn_config)
234
+ echo = self.config.get("database.debug", False)
235
+
236
+ # 连接池配置(可在 config/database.php 中覆盖)
237
+ pool_kwargs = {
238
+ "pool_size": conn_config.get("pool_size", 10),
239
+ "max_overflow": conn_config.get("max_overflow", 20),
240
+ "pool_recycle": conn_config.get("pool_recycle", 3600),
241
+ "pool_pre_ping": conn_config.get("pool_pre_ping", True),
242
+ "connect_timeout": conn_config.get("connect_timeout", 10),
243
+ }
244
+ try:
245
+ db.configure(url, echo=echo, **pool_kwargs)
246
+ self.instance("db", db)
247
+ print(f"[Database] Connected ({default}) pool_size={pool_kwargs['pool_size']}")
248
+
249
+ self._on_shutdown(_make_close_db(db))
250
+ except Exception as e:
251
+ print(f"[Database] Skip ({e})")
252
+ return
253
+
254
+ def _init_auth(self):
255
+ """初始化认证服务"""
256
+ auth_config = self.config.get("auth", {})
257
+ if not auth_config:
258
+ return
259
+
260
+ from fastv.auth import AuthManager, set_auth
261
+ auth = AuthManager(auth_config)
262
+ self.instance("auth", auth)
263
+ set_auth(auth)
264
+ print("[Auth] JWT authentication initialized")
265
+
266
+ def _discover_commands(self):
267
+ """Deprecated: CLI command discovery is now handled by ArtisanApplication via fastv.py."""
268
+ pass
269
+
270
+ def _add_app_to_path(self):
271
+ app_path = os.path.join(self.base_dir, "app")
272
+ if app_path not in sys.path:
273
+ sys.path.insert(0, app_path)
274
+ if self.base_dir not in sys.path:
275
+ sys.path.insert(0, self.base_dir)
276
+
277
+
278
+ def _init_redis(self):
279
+ """初始化 Redis 连接"""
280
+ redis_config = self.config.get("redis.redis", {})
281
+ if not redis_config:
282
+ return
283
+ from fastv.redis import get_redis
284
+ rm = get_redis()
285
+ rm.configure(redis_config)
286
+ conn = rm.connection("default")
287
+
288
+ self.instance("redis", rm)
289
+
290
+ if conn.is_connected:
291
+ import asyncio
292
+ loop = asyncio.get_event_loop()
293
+ if loop.is_running():
294
+ print("[Redis] Connected")
295
+ else:
296
+ try:
297
+ pong = loop.run_until_complete(conn.ping())
298
+ if pong:
299
+ print("[Redis] Connected")
300
+ else:
301
+ print("[Redis] Connected but server unreachable")
302
+ except Exception as e:
303
+ print(f"[Redis] Connected but ping failed ({e})")
304
+
305
+ self._on_shutdown(_make_close_redis(rm))
306
+ else:
307
+ print("[Redis] Package not installed (pip install redis), using fallback")
308
+
309
+ def _init_cache(self):
310
+ """初始化缓存"""
311
+ default = self.config.get("cache.default", "file")
312
+ stores = self.config.get("cache.stores", {})
313
+ if not stores:
314
+ stores = {"array": {"driver": "array"}}
315
+ default = "array"
316
+ redis_mgr = None
317
+ if self.has("redis"):
318
+ redis_mgr = self.make("redis")
319
+ from fastv.cache import get_cache
320
+ cm = get_cache()
321
+ cm.configure(default, stores, redis_mgr)
322
+ self.instance("cache", cm)
323
+ print(f"[Cache] Driver: {default}")
324
+
325
+ def _init_logger(self):
326
+ """初始化日志"""
327
+ default = self.config.get("logging.default", "daily")
328
+ channels = self.config.get("logging.channels", {})
329
+ level = self.config.get("logging.level", "debug")
330
+ if not channels:
331
+ channels = {"daily": {"driver": "daily", "path": "storage/logs/app.log", "level": "debug", "days": 14}}
332
+ from fastv.log import get_logger
333
+ lm = get_logger()
334
+ lm.configure(default, channels, level)
335
+ self.instance("log", lm)
336
+ lm.info(f"[Logger] Channel: {default}, Level: {level}")
337
+ print(f"[Logger] Channel: {default}")
338
+
339
+ def _init_asynctasq(self):
340
+ """初始化 AsyncTasQ 任务队列"""
341
+ queue_cfg = self.config.get("queue.asynctasq", {})
342
+ if not queue_cfg:
343
+ return
344
+ redis_cfg = self.config.get("redis.redis.default", {})
345
+ db_name = self.config.get("database.default", "sqlite")
346
+ db_cfg = self.config.get(f"database.connections.{db_name}", {})
347
+ try:
348
+ from fastv.queue import register_lifecycle
349
+ register_lifecycle(self, queue_cfg, redis_cfg, db_cfg)
350
+ except ImportError:
351
+ print("[TaskQueue] asynctasq not installed, skipping")
352
+ except Exception as e:
353
+ print(f"[TaskQueue] Init failed ({e}), task dispatch will be unavailable")
354
+
355
+ async def __call__(self, scope, receive, send):
356
+ await self.app(scope, receive, send)
357
+
358
+ @staticmethod
359
+ def _mw_adapter(mw_class):
360
+ async def middleware_func(request, call_next):
361
+ instance = mw_class()
362
+ if hasattr(instance, "__call__"):
363
+ return await instance(request, call_next)
364
+ if hasattr(instance, "handle"):
365
+ return await instance.handle(request, call_next)
366
+ return await call_next(request)
367
+ return middleware_func
368
+
369
+
370
+ def _make_close_db(db):
371
+ """创建关闭数据库连接的回调"""
372
+ async def _close():
373
+ await db.close()
374
+ print("[Database] Disconnected")
375
+ return _close
376
+
377
+
378
+ def _make_close_redis(rm):
379
+ """创建关闭 Redis 连接的回调"""
380
+ async def _close():
381
+ await rm.close_all()
382
+ print("[Redis] Disconnected")
383
+ return _close
384
+
385
+
386
+ def get_app():
387
+ return Application._instance
388
+
389
+
390
+ def cli():
391
+ """CLI entry point — Artisan-style command interface."""
392
+ from fastv.console.application import ArtisanApplication
393
+ app = ArtisanApplication()
394
+ app.discover()
395
+ app.create_cli()()
396
+
fastv/auth/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ Auth 认证模块
3
+
4
+ 提供 JWT 认证功能:
5
+ - JwtManager: JWT token 生成和验证
6
+ - PasswordHasher: 密码哈希和验证
7
+ - JwtGuard: JWT 认证守卫
8
+ - AuthManager: 认证管理器
9
+ - get_auth(): 获取全局 Auth 实例
10
+ """
11
+
12
+ from .auth_manager import AuthManager, get_auth, set_auth
13
+ from .guard import JwtGuard
14
+ from .jwt_manager import JwtManager
15
+ from .password_hasher import PasswordHasher
16
+
17
+ __all__ = [
18
+ "get_auth",
19
+ "set_auth",
20
+ "JwtManager",
21
+ "PasswordHasher",
22
+ "JwtGuard",
23
+ "AuthManager",
24
+ ]
@@ -0,0 +1,122 @@
1
+ """
2
+ Auth Manager - 认证管理器
3
+
4
+ 管理多个认证守卫(类似 Laravel AuthManager):
5
+ - guard(): 获取指定守卫
6
+ - check(): 检查是否已认证
7
+ - user(): 获取当前用户
8
+ - id(): 获取当前用户 ID
9
+ """
10
+
11
+ import importlib
12
+ from typing import Optional
13
+
14
+ from fastv.auth.guard import JwtGuard
15
+ from fastv.auth.jwt_manager import JwtManager
16
+
17
+
18
+ class AuthManager:
19
+ """认证管理器"""
20
+
21
+ def __init__(self, config: dict):
22
+ self.config = config
23
+ self._guards = {}
24
+ self._current_guard_name = config.get("guards", {}).get("default", "jwt")
25
+
26
+ def guard(self, name: Optional[str] = None) -> JwtGuard:
27
+ """
28
+ 获取指定守卫
29
+
30
+ Args:
31
+ name: 守卫名称,默认使用配置中的 default
32
+
33
+ Returns:
34
+ JwtGuard 实例
35
+ """
36
+ guard_name = name or self._current_guard_name
37
+
38
+ if guard_name not in self._guards:
39
+ self._guards[guard_name] = self._create_guard(guard_name)
40
+
41
+ return self._guards[guard_name]
42
+
43
+ def _create_guard(self, name: str) -> JwtGuard:
44
+ """创建守卫实例"""
45
+ guards_config = self.config.get("guards", {}).get("guards", {})
46
+ guard_config = guards_config.get(name, {})
47
+
48
+ driver = guard_config.get("driver", "jwt")
49
+ provider_name = guard_config.get("provider", "users")
50
+
51
+ if driver != "jwt":
52
+ raise ValueError(f"Unsupported guard driver: {driver}")
53
+
54
+ # 获取用户提供者配置
55
+ providers_config = self.config.get("guards", {}).get("providers", {})
56
+ provider_config = providers_config.get(provider_name, {})
57
+
58
+ provider_driver = provider_config.get("driver", "eloquent")
59
+ model_path = provider_config.get("model", "app.model.user.User")
60
+
61
+ if provider_driver != "eloquent":
62
+ raise ValueError(f"Unsupported provider driver: {provider_driver}")
63
+
64
+ # 动态加载用户模型
65
+ user_model = self._load_model(model_path)
66
+
67
+ # 创建 JWT 管理器
68
+ jwt_manager = JwtManager(self.config)
69
+
70
+ return JwtGuard(user_model, jwt_manager)
71
+
72
+ @staticmethod
73
+ def _load_model(model_path: str):
74
+ """动态加载模型类"""
75
+ module_path, class_name = model_path.rsplit(".", 1)
76
+ module = importlib.import_module(module_path)
77
+ return getattr(module, class_name)
78
+
79
+ def check(self) -> bool:
80
+ """检查是否已认证"""
81
+ return self.guard().check()
82
+
83
+ def user(self) -> Optional[object]:
84
+ """获取当前用户"""
85
+ return self.guard().user()
86
+
87
+ def id(self) -> Optional[int]:
88
+ """获取当前用户 ID"""
89
+ return self.guard().id()
90
+
91
+ def logout(self):
92
+ """登出"""
93
+ self.guard().logout()
94
+
95
+
96
+ # 全局 Auth 实例(类似 Laravel Auth facade)
97
+ _auth_instance: Optional[AuthManager] = None
98
+
99
+
100
+ def get_auth() -> AuthManager:
101
+ """获取全局 Auth 实例"""
102
+ global _auth_instance
103
+ if _auth_instance is None:
104
+ # 从 Application 容器获取
105
+ try:
106
+ from fastv import get_app
107
+ app = get_app()
108
+ if app and app.has("auth"):
109
+ _auth_instance = app.make("auth")
110
+ except Exception:
111
+ pass
112
+
113
+ if _auth_instance is None:
114
+ raise RuntimeError("Auth not initialized. Call Application._init_auth() first.")
115
+
116
+ return _auth_instance
117
+
118
+
119
+ def set_auth(auth: AuthManager):
120
+ """设置全局 Auth 实例"""
121
+ global _auth_instance
122
+ _auth_instance = auth
fastv/auth/guard.py ADDED
@@ -0,0 +1,127 @@
1
+ """
2
+ JWT Guard - JWT 认证守卫
3
+
4
+ 类似 Laravel 的 JWT Guard,负责:
5
+ - attempt(): 尝试登录,验证凭证并返回 token
6
+ - validate_token(): 验证 token 并返回用户
7
+ - user(): 获取当前认证用户
8
+ - check(): 检查是否已认证
9
+ - id(): 获取当前用户 ID
10
+ - logout(): 登出
11
+ """
12
+
13
+ from typing import Optional
14
+
15
+ from fastv.auth.jwt_manager import JwtManager
16
+ from fastv.auth.password_hasher import PasswordHasher
17
+
18
+
19
+ class JwtGuard:
20
+ """JWT 认证守卫"""
21
+
22
+ def __init__(self, user_model, jwt_manager: JwtManager):
23
+ self.user_model = user_model
24
+ self.jwt_manager = jwt_manager
25
+ self.password_hasher = PasswordHasher()
26
+ self._user = None
27
+ self._user_id = None
28
+
29
+ async def attempt(self, credentials: dict) -> Optional[dict]:
30
+ """
31
+ 尝试登录
32
+
33
+ Args:
34
+ credentials: 登录凭证,包含 account_name/email 和 password
35
+
36
+ Returns:
37
+ 成功返回 {"token": "...", "user": {...}},失败返回 None
38
+ """
39
+ # 根据凭证查找用户
40
+ user = await self.user_model.find_by_credentials(credentials)
41
+ if not user:
42
+ return None
43
+
44
+ # 验证密码
45
+ password = credentials.get("password", "")
46
+ if not self.password_hasher.check(password, user.password or ""):
47
+ return None
48
+
49
+ # 检查用户状态
50
+ if hasattr(user, "is_active") and not user.is_active():
51
+ return None
52
+
53
+ # 生成 JWT token
54
+ token = self.jwt_manager.encode({
55
+ "user_id": user.id,
56
+ "account_name": user.account_name,
57
+ })
58
+
59
+ # 设置当前用户
60
+ self._user = user
61
+ self._user_id = user.id
62
+
63
+ # 返回 token 和用户信息
64
+ user_data = user.to_dict(hide=["password"]) if hasattr(user, "to_dict") else {
65
+ "id": user.id,
66
+ "account_name": user.account_name,
67
+ "real_name": user.real_name,
68
+ "email": user.email,
69
+ }
70
+
71
+ return {
72
+ "token": token,
73
+ "user": user_data,
74
+ }
75
+
76
+ async def validate_token(self, token: str) -> Optional[object]:
77
+ """
78
+ 验证 token 并返回用户
79
+
80
+ Args:
81
+ token: JWT token
82
+
83
+ Returns:
84
+ 成功返回用户对象,失败返回 None
85
+ """
86
+ try:
87
+ # 解析 token
88
+ payload = self.jwt_manager.decode(token)
89
+ user_id = payload.get("user_id")
90
+
91
+ if not user_id:
92
+ return None
93
+
94
+ # 从数据库获取用户
95
+ user = await self.user_model.find(user_id)
96
+ if not user:
97
+ return None
98
+
99
+ # 检查用户状态
100
+ if hasattr(user, "is_active") and not user.is_active():
101
+ return None
102
+
103
+ # 设置当前用户
104
+ self._user = user
105
+ self._user_id = user.id
106
+
107
+ return user
108
+
109
+ except Exception:
110
+ return None
111
+
112
+ def user(self) -> Optional[object]:
113
+ """获取当前认证用户"""
114
+ return self._user
115
+
116
+ def check(self) -> bool:
117
+ """检查是否已认证"""
118
+ return self._user is not None
119
+
120
+ def id(self) -> Optional[int]:
121
+ """获取当前用户 ID"""
122
+ return self._user_id
123
+
124
+ def logout(self):
125
+ """登出(清除当前用户)"""
126
+ self._user = None
127
+ self._user_id = None