quantum-framework 0.18.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 (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,394 @@
1
+ """
2
+ Auth Service for Quantum Admin
3
+ JWT-based authentication with bcrypt password hashing
4
+ """
5
+ import copy
6
+ import os
7
+ import secrets
8
+ import logging
9
+ from datetime import datetime, timedelta
10
+ from typing import Optional, Dict, Any
11
+ from dataclasses import dataclass
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Try to import JWT library
16
+ try:
17
+ import jwt
18
+ JWT_AVAILABLE = True
19
+ except ImportError:
20
+ JWT_AVAILABLE = False
21
+ logger.warning("PyJWT not installed. Run: pip install PyJWT")
22
+
23
+ # Try to import bcrypt
24
+ try:
25
+ import bcrypt
26
+ BCRYPT_AVAILABLE = True
27
+ except ImportError:
28
+ BCRYPT_AVAILABLE = False
29
+ logger.warning("bcrypt not installed. Run: pip install bcrypt")
30
+
31
+
32
+ @dataclass
33
+ class User:
34
+ id: int
35
+ username: str
36
+ role: str = "user" # user, admin
37
+
38
+
39
+ @dataclass
40
+ class TokenPayload:
41
+ user_id: int
42
+ username: str
43
+ role: str
44
+ exp: datetime
45
+ iat: datetime
46
+
47
+
48
+ class AuthConfigError(RuntimeError):
49
+ """The admin is configured in a way that cannot be served safely."""
50
+ pass
51
+
52
+
53
+ class AuthService:
54
+ """JWT authentication service with bcrypt password hashing"""
55
+
56
+ # Values that used to be the defaults, and are published in this file.
57
+ # Refused outright: someone setting them explicitly is almost always
58
+ # copying an old README, not making a decision.
59
+ UNSAFE_SECRETS = frozenset({
60
+ "quantum-admin-secret-change-in-production",
61
+ "change-me-in-production",
62
+ "changeme",
63
+ "secret",
64
+ })
65
+ UNSAFE_PASSWORDS = frozenset({"admin", "password", "changeme", "123456"})
66
+
67
+ # The admin account. The password is set per instance in __init__ — never
68
+ # here, because a value on the class is shared by every instance.
69
+ DEFAULT_USERS = {
70
+ "admin": {
71
+ "id": 1,
72
+ "password_hash": None,
73
+ "role": "admin"
74
+ }
75
+ }
76
+
77
+ def __init__(self, secret_key: str = None, expiry_hours: int = 24):
78
+ """
79
+ Initialize auth service.
80
+
81
+ Args:
82
+ secret_key: JWT signing key. Falls back to JWT_SECRET_KEY, and
83
+ to a random per-process key when neither is set.
84
+ expiry_hours: Token expiry in hours
85
+
86
+ There used to be constant fallbacks here: the signing key defaulted to
87
+ "quantum-admin-secret-change-in-production" and the admin password to
88
+ "admin". Both are in the public source, so an unconfigured admin
89
+ accepted a token anyone could forge — full admin without ever seeing
90
+ the login screen — and admin/admin as well.
91
+
92
+ Now an unconfigured instance gets random values. Development still
93
+ works (the generated password is printed once at boot); an attacker
94
+ gains nothing from reading this file. With
95
+ QUANTUM_ADMIN_ENV=production the service refuses to start at all
96
+ without explicit configuration, because a random key means every
97
+ restart and every extra worker invalidates the sessions.
98
+ """
99
+ strict = os.environ.get("QUANTUM_ADMIN_ENV", "").lower() in (
100
+ "production", "prod")
101
+
102
+ configured_key = secret_key or os.environ.get("JWT_SECRET_KEY")
103
+ if configured_key in self.UNSAFE_SECRETS:
104
+ raise AuthConfigError(
105
+ "JWT_SECRET_KEY is set to the placeholder value that ships in "
106
+ "the source. Anyone can forge an admin token with it. "
107
+ "Generate one: python -c \"import secrets; "
108
+ "print(secrets.token_hex(32))\""
109
+ )
110
+ if not configured_key:
111
+ if strict:
112
+ raise AuthConfigError(
113
+ "QUANTUM_ADMIN_ENV=production requires JWT_SECRET_KEY. "
114
+ "Without it each process signs with its own random key "
115
+ "and users are logged out at random."
116
+ )
117
+ configured_key = secrets.token_hex(32)
118
+ logger.warning(
119
+ "JWT_SECRET_KEY not set — signing with a random key for this "
120
+ "process only. Sessions will not survive a restart, and a "
121
+ "second worker will reject this one's tokens."
122
+ )
123
+ self.secret_key = configured_key
124
+
125
+ self.expiry_hours = expiry_hours
126
+ self.algorithm = "HS256"
127
+
128
+ # deepcopy, not copy: DEFAULT_USERS' inner dict is shared with the
129
+ # class, so a shallow copy let the first instance write the password
130
+ # hash onto the class and every later instance inherit it — including
131
+ # after ADMIN_PASSWORD changed.
132
+ self._users = copy.deepcopy(self.DEFAULT_USERS)
133
+
134
+ password = os.environ.get("ADMIN_PASSWORD")
135
+ if password in self.UNSAFE_PASSWORDS:
136
+ raise AuthConfigError(
137
+ f"ADMIN_PASSWORD is {password!r}, which is the first thing "
138
+ "anyone tries. Choose another."
139
+ )
140
+ if not password:
141
+ if strict:
142
+ raise AuthConfigError(
143
+ "QUANTUM_ADMIN_ENV=production requires ADMIN_PASSWORD."
144
+ )
145
+ password = secrets.token_urlsafe(18)
146
+ self.generated_password = password
147
+ # Printed, not just logged: a generated credential nobody can read
148
+ # is the same as no login at all.
149
+ banner = (
150
+ "\n" + "=" * 68 +
151
+ "\n Quantum Admin: no ADMIN_PASSWORD set, so one was generated"
152
+ "\n for this process. It changes on every restart."
153
+ f"\n\n user: admin\n password: {password}\n\n"
154
+ " Set ADMIN_PASSWORD to keep a stable login.\n" +
155
+ "=" * 68 + "\n"
156
+ )
157
+ print(banner)
158
+ logger.warning(
159
+ "ADMIN_PASSWORD not set — generated a random admin password "
160
+ "for this process (printed at startup)."
161
+ )
162
+ self._users["admin"]["password_hash"] = self._hash_password(password)
163
+
164
+ def _hash_password(self, password: str) -> str:
165
+ """Hash a password with bcrypt (secure, with salt)"""
166
+ if not BCRYPT_AVAILABLE:
167
+ # Fallback to less secure method if bcrypt not installed
168
+ import hashlib
169
+ logger.warning("Using SHA-256 fallback - install bcrypt for production!")
170
+ return "sha256:" + hashlib.sha256(password.encode()).hexdigest()
171
+
172
+ # bcrypt automatically generates a salt and includes it in the hash
173
+ salt = bcrypt.gensalt(rounds=12) # Cost factor of 12 is secure and fast enough
174
+ password_bytes = password.encode('utf-8')
175
+ hashed = bcrypt.hashpw(password_bytes, salt)
176
+ return "bcrypt:" + hashed.decode('utf-8')
177
+
178
+ def _verify_password(self, password: str, password_hash: str) -> bool:
179
+ """Verify a password against its hash"""
180
+ if password_hash.startswith("bcrypt:"):
181
+ if not BCRYPT_AVAILABLE:
182
+ logger.error("bcrypt required to verify password but not installed")
183
+ return False
184
+ stored_hash = password_hash[7:].encode('utf-8') # Remove "bcrypt:" prefix
185
+ password_bytes = password.encode('utf-8')
186
+ return bcrypt.checkpw(password_bytes, stored_hash)
187
+ elif password_hash.startswith("sha256:"):
188
+ # Legacy SHA-256 support (for migration)
189
+ import hashlib
190
+ stored_hash = password_hash[7:] # Remove "sha256:" prefix
191
+ return hashlib.sha256(password.encode()).hexdigest() == stored_hash
192
+ else:
193
+ # Very old format without prefix (assume SHA-256)
194
+ import hashlib
195
+ return hashlib.sha256(password.encode()).hexdigest() == password_hash
196
+
197
+ # =========================================================================
198
+ # Authentication
199
+ # =========================================================================
200
+
201
+ def authenticate(self, username: str, password: str) -> Optional[Dict[str, Any]]:
202
+ """
203
+ Authenticate user and return tokens
204
+
205
+ Args:
206
+ username: Username
207
+ password: Plain text password
208
+
209
+ Returns:
210
+ Dict with access_token and user info, or None if invalid
211
+ """
212
+ if not JWT_AVAILABLE:
213
+ logger.error("PyJWT not available")
214
+ return None
215
+
216
+ user_data = self._users.get(username)
217
+ if not user_data:
218
+ logger.warning(f"Authentication failed: user '{username}' not found")
219
+ return None
220
+
221
+ if not self._verify_password(password, user_data["password_hash"]):
222
+ logger.warning(f"Authentication failed: invalid password for '{username}'")
223
+ return None
224
+
225
+ # Migrate old SHA-256 passwords to bcrypt on successful login
226
+ if BCRYPT_AVAILABLE and not user_data["password_hash"].startswith("bcrypt:"):
227
+ logger.info(f"Migrating password hash to bcrypt for user '{username}'")
228
+ user_data["password_hash"] = self._hash_password(password)
229
+
230
+ # Generate token
231
+ user = User(
232
+ id=user_data["id"],
233
+ username=username,
234
+ role=user_data["role"]
235
+ )
236
+
237
+ token = self._create_token(user)
238
+
239
+ logger.info(f"User '{username}' authenticated successfully")
240
+ return {
241
+ "access_token": token,
242
+ "token_type": "bearer",
243
+ "expires_in": self.expiry_hours * 3600,
244
+ "user": {
245
+ "id": user.id,
246
+ "username": user.username,
247
+ "role": user.role
248
+ }
249
+ }
250
+
251
+ def _create_token(self, user: User) -> str:
252
+ """Create JWT token for user"""
253
+ now = datetime.utcnow()
254
+ payload = {
255
+ "sub": str(user.id), # JWT sub claim must be a string
256
+ "user_id": user.id,
257
+ "username": user.username,
258
+ "role": user.role,
259
+ "iat": now,
260
+ "exp": now + timedelta(hours=self.expiry_hours)
261
+ }
262
+ return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
263
+
264
+ def verify_token(self, token: str) -> Optional[TokenPayload]:
265
+ """
266
+ Verify JWT token and return payload
267
+
268
+ Args:
269
+ token: JWT token string
270
+
271
+ Returns:
272
+ TokenPayload or None if invalid
273
+ """
274
+ if not JWT_AVAILABLE:
275
+ return None
276
+
277
+ try:
278
+ payload = jwt.decode(
279
+ token,
280
+ self.secret_key,
281
+ algorithms=[self.algorithm]
282
+ )
283
+ return TokenPayload(
284
+ user_id=payload.get("user_id", int(payload["sub"])),
285
+ username=payload["username"],
286
+ role=payload["role"],
287
+ exp=datetime.fromtimestamp(payload["exp"]),
288
+ iat=datetime.fromtimestamp(payload["iat"])
289
+ )
290
+ except jwt.ExpiredSignatureError:
291
+ logger.warning("Token expired")
292
+ return None
293
+ except jwt.InvalidTokenError as e:
294
+ logger.warning(f"Invalid token: {e}")
295
+ return None
296
+
297
+ def get_current_user(self, token: str) -> Optional[User]:
298
+ """Get current user from token"""
299
+ payload = self.verify_token(token)
300
+ if not payload:
301
+ return None
302
+
303
+ return User(
304
+ id=payload.user_id,
305
+ username=payload.username,
306
+ role=payload.role
307
+ )
308
+
309
+ # =========================================================================
310
+ # User Management
311
+ # =========================================================================
312
+
313
+ def create_user(
314
+ self,
315
+ username: str,
316
+ password: str,
317
+ role: str = "user"
318
+ ) -> Optional[User]:
319
+ """Create a new user"""
320
+ if username in self._users:
321
+ logger.error(f"User '{username}' already exists")
322
+ return None
323
+
324
+ user_id = max(u["id"] for u in self._users.values()) + 1
325
+
326
+ self._users[username] = {
327
+ "id": user_id,
328
+ "password_hash": self._hash_password(password),
329
+ "role": role
330
+ }
331
+
332
+ logger.info(f"User '{username}' created with role '{role}'")
333
+ return User(id=user_id, username=username, role=role)
334
+
335
+ def change_password(self, username: str, new_password: str) -> bool:
336
+ """Change user password"""
337
+ if username not in self._users:
338
+ return False
339
+
340
+ self._users[username]["password_hash"] = self._hash_password(new_password)
341
+ logger.info(f"Password changed for user '{username}'")
342
+ return True
343
+
344
+ def delete_user(self, username: str) -> bool:
345
+ """Delete a user"""
346
+ if username not in self._users:
347
+ return False
348
+
349
+ if username == "admin":
350
+ logger.error("Cannot delete admin user")
351
+ return False
352
+
353
+ del self._users[username]
354
+ logger.info(f"User '{username}' deleted")
355
+ return True
356
+
357
+ def list_users(self) -> list:
358
+ """List all users (without passwords)"""
359
+ return [
360
+ {"id": data["id"], "username": username, "role": data["role"]}
361
+ for username, data in self._users.items()
362
+ ]
363
+
364
+ # =========================================================================
365
+ # Role Checking
366
+ # =========================================================================
367
+
368
+ def require_role(self, token: str, required_role: str) -> bool:
369
+ """Check if token has required role"""
370
+ payload = self.verify_token(token)
371
+ if not payload:
372
+ return False
373
+
374
+ # Admin has access to everything
375
+ if payload.role == "admin":
376
+ return True
377
+
378
+ return payload.role == required_role
379
+
380
+ def is_admin(self, token: str) -> bool:
381
+ """Check if token belongs to admin"""
382
+ return self.require_role(token, "admin")
383
+
384
+
385
+ # Singleton instance
386
+ _auth_service = None
387
+
388
+
389
+ def get_auth_service() -> AuthService:
390
+ """Get singleton instance of AuthService"""
391
+ global _auth_service
392
+ if _auth_service is None:
393
+ _auth_service = AuthService()
394
+ return _auth_service