appmanager-server 0.3.0__tar.gz → 0.4.0__tar.gz

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 (128) hide show
  1. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/CHANGELOG.md +14 -0
  2. appmanager_server-0.4.0/MANIFEST.in +17 -0
  3. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/PKG-INFO +1 -1
  4. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/__init__.py +1 -1
  5. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/admin/app_installer.py +46 -5
  6. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/api/routes.py +23 -11
  7. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/auth/routes.py +2 -0
  8. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/cli.py +13 -0
  9. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/config.py +19 -2
  10. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/dependency_manager.py +66 -20
  11. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/extension.py +23 -0
  12. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/middleware.py +38 -4
  13. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/security.py +19 -4
  14. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/dashboard.html +10 -13
  15. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager_server.egg-info/PKG-INFO +1 -1
  16. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager_server.egg-info/SOURCES.txt +0 -1
  17. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/pyproject.toml +1 -1
  18. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_dependency_manager.py +21 -0
  19. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_packaging_and_cli.py +2 -2
  20. appmanager_server-0.3.0/.env.example +0 -51
  21. appmanager_server-0.3.0/MANIFEST.in +0 -11
  22. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/LICENSE +0 -0
  23. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/README.md +0 -0
  24. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/SECURITY.md +0 -0
  25. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/admin/__init__.py +0 -0
  26. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/admin/members.py +0 -0
  27. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/admin/registry.py +0 -0
  28. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/admin/routes.py +0 -0
  29. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/api/__init__.py +0 -0
  30. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/app_config.py +0 -0
  31. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/auth/__init__.py +0 -0
  32. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/auth/oauth.py +0 -0
  33. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/auth/utils.py +0 -0
  34. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/bridge.py +0 -0
  35. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/database.py +0 -0
  36. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/extensions.py +0 -0
  37. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/health.py +0 -0
  38. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/hooks.py +0 -0
  39. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/models.py +0 -0
  40. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/py.typed +0 -0
  41. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/sdk.py +0 -0
  42. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/security_scanner.py +0 -0
  43. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/signals.py +0 -0
  44. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/css/app.css +0 -0
  45. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/fonts.css +0 -0
  46. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-0.woff2 +0 -0
  47. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-1.woff2 +0 -0
  48. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-10.woff2 +0 -0
  49. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-11.woff2 +0 -0
  50. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-12.woff2 +0 -0
  51. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-13.woff2 +0 -0
  52. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-14.woff2 +0 -0
  53. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-15.woff2 +0 -0
  54. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-16.woff2 +0 -0
  55. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-17.woff2 +0 -0
  56. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-18.woff2 +0 -0
  57. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-19.woff2 +0 -0
  58. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-2.woff2 +0 -0
  59. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-20.woff2 +0 -0
  60. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-21.woff2 +0 -0
  61. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-22.woff2 +0 -0
  62. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-23.woff2 +0 -0
  63. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-3.woff2 +0 -0
  64. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-4.woff2 +0 -0
  65. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-5.woff2 +0 -0
  66. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-6.woff2 +0 -0
  67. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-7.woff2 +0 -0
  68. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-8.woff2 +0 -0
  69. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/jetbrainsmono-9.woff2 +0 -0
  70. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-0.woff2 +0 -0
  71. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-1.woff2 +0 -0
  72. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-10.woff2 +0 -0
  73. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-11.woff2 +0 -0
  74. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-2.woff2 +0 -0
  75. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-3.woff2 +0 -0
  76. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-4.woff2 +0 -0
  77. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-5.woff2 +0 -0
  78. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-6.woff2 +0 -0
  79. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-7.woff2 +0 -0
  80. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-8.woff2 +0 -0
  81. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/fonts/spacegrotesk-9.woff2 +0 -0
  82. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/static/vendor/htmx/htmx.min.js +0 -0
  83. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/_member_drawer.html +0 -0
  84. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/_members_table.html +0 -0
  85. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/app_detail.html +0 -0
  86. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/flairs.html +0 -0
  87. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/permissions.html +0 -0
  88. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/roles.html +0 -0
  89. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/admin/users.html +0 -0
  90. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/auth/login.html +0 -0
  91. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/auth/profile.html +0 -0
  92. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/base.html +0 -0
  93. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/errors/401.html +0 -0
  94. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/errors/403.html +0 -0
  95. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/errors/404.html +0 -0
  96. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/errors/500.html +0 -0
  97. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager/templates/index.html +0 -0
  98. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager_server.egg-info/dependency_links.txt +0 -0
  99. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager_server.egg-info/entry_points.txt +0 -0
  100. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager_server.egg-info/requires.txt +0 -0
  101. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/appmanager_server.egg-info/top_level.txt +0 -0
  102. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/requirements.txt +0 -0
  103. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/setup.cfg +0 -0
  104. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_api.py +0 -0
  105. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_app_settings.py +0 -0
  106. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_app_settings_config.py +0 -0
  107. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_app_updates.py +0 -0
  108. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_auth.py +0 -0
  109. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_cli_dev_and_templates.py +0 -0
  110. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_cli_health.py +0 -0
  111. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_cli_scaffolding.py +0 -0
  112. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_error_pages.py +0 -0
  113. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_extension_apps.py +0 -0
  114. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_extension_pattern.py +0 -0
  115. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_hooks.py +0 -0
  116. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_installer.py +0 -0
  117. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_invalid_install_failures.py +0 -0
  118. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_manifest_sdk.py +0 -0
  119. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_middleware.py +0 -0
  120. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_nested_portal.py +0 -0
  121. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_role_management.py +0 -0
  122. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_sdk.py +0 -0
  123. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_security_hardening.py +0 -0
  124. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_security_precheck_flow.py +0 -0
  125. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_security_scanner.py +0 -0
  126. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_signals.py +0 -0
  127. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_user_elevation_and_visibility.py +0 -0
  128. {appmanager_server-0.3.0 → appmanager_server-0.4.0}/tests/test_user_tracking.py +0 -0
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.4.0] - 2026-08-28
11
+
12
+ ### Added
13
+ - **Automatic Sub-App Root Module Resolution**: Automatically prepends sub-app root directory (`app_dir`) to `sys.path` during dynamic module loading in `load_wsgi_app_from_path`, enabling seamless relative/module imports within sub-app packages.
14
+
15
+ ---
16
+
17
+ ## [0.3.1] - 2026-08-28
18
+
19
+ ### Fixed
20
+ - **App & Role Deletion Type-To-Confirm Modal**: Fixed modal validation logic and dynamic form URL in Admin Dashboard where typing the exact app/role slug did not enable the confirmation submission.
21
+
22
+ ---
23
+
10
24
  ## [0.3.0] - 2026-08-28
11
25
 
12
26
  ### Added
@@ -0,0 +1,17 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include SECURITY.md
5
+ include requirements.txt
6
+ include .env.example
7
+ graft appmanager/templates
8
+ graft appmanager/static
9
+ global-exclude __pycache__
10
+ global-exclude *.py[cod]
11
+ global-exclude .env*
12
+ global-exclude *.db
13
+ global-exclude *.sqlite*
14
+ prune instance
15
+ prune installed_apps
16
+ prune .venv
17
+ prune .pytest_cache
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmanager-server
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: High-performance multi-tenant WSGI portal, dynamic sub-app dispatcher, and extension framework for Python/Flask.
5
5
  Author: AppManager Contributors
6
6
  License: MIT
@@ -8,7 +8,7 @@ from appmanager.database import db
8
8
  from appmanager.extension import AppManager
9
9
  from appmanager.middleware import DynamicAppDispatcherMiddleware
10
10
 
11
- __version__ = "0.3.0"
11
+ __version__ = "0.4.0"
12
12
 
13
13
  __all__ = [
14
14
  "AppManager",
@@ -2,6 +2,7 @@ import ast
2
2
  import importlib.util
3
3
  import inspect
4
4
  import json
5
+ import logging
5
6
  import os
6
7
  import shutil
7
8
  import subprocess
@@ -30,6 +31,8 @@ from appmanager.security import (
30
31
  from appmanager.security_scanner import SecurityScanReport, run_security_scan
31
32
  from appmanager.signals import subapp_installed, subapp_reloaded, subapp_uninstalled, subapp_updated
32
33
 
34
+ logger = logging.getLogger("appmanager.installer")
35
+
33
36
 
34
37
  def sanitize_slug(slug_text: str) -> str:
35
38
  clean = secure_filename(slug_text.lower().replace(" ", "-"))
@@ -217,19 +220,35 @@ def validate_subapp_package(path_or_zip: str) -> Tuple[bool, List[str], Dict[str
217
220
 
218
221
  def discover_entrypoint(app_dir: str) -> str:
219
222
  """
220
- Search for common Flask entrypoint files (app.py, wsgi.py, main.py, run.py) or manifest.json.
221
- Returns 'module_name:app_variable' string e.g. 'app:app'
223
+ Search for common Flask entrypoint files (app.py, wsgi.py, main.py, run.py, src/app.py, packages)
224
+ or manifest.json. Returns 'module_name:app_variable' string e.g. 'app:app'
222
225
  """
223
226
  manifest = parse_manifest(app_dir)
224
227
  if manifest and "entry_point" in manifest:
225
228
  return manifest["entry_point"]
226
229
 
227
- candidates = ["app.py", "wsgi.py", "main.py", "run.py"]
230
+ candidates = [
231
+ "app.py",
232
+ "wsgi.py",
233
+ "main.py",
234
+ "run.py",
235
+ "server.py",
236
+ os.path.join("src", "app.py"),
237
+ os.path.join("src", "main.py"),
238
+ os.path.join("src", "wsgi.py"),
239
+ ]
228
240
  for candidate in candidates:
229
241
  if os.path.exists(os.path.join(app_dir, candidate)):
230
- module_name = candidate[:-3] # Strip .py
242
+ module_name = candidate[:-3].replace(os.sep, ".") # Strip .py and normalize slashes
231
243
  return f"{module_name}:app"
232
244
 
245
+ # Check for package directories with __init__.py (e.g. appmanager, app, <slug>)
246
+ slug = os.path.basename(app_dir.rstrip("/\\"))
247
+ for pkg_cand in [slug, slug.replace("-", "_"), "app", "appmanager"]:
248
+ init_file = os.path.join(app_dir, pkg_cand, "__init__.py")
249
+ if os.path.exists(init_file):
250
+ return f"{pkg_cand}:create_app"
251
+
233
252
  # Default fallback
234
253
  return "app:app"
235
254
 
@@ -263,7 +282,9 @@ def load_wsgi_app_from_path(app_dir: str, entry_point: str = "app:app") -> Any:
263
282
 
264
283
  module_path = module_path_or_err
265
284
  if not os.path.exists(module_path):
266
- raise FileNotFoundError(f"Entrypoint file '{module_name}.py' not found in {app_dir}")
285
+ raise FileNotFoundError(
286
+ f"Entrypoint module/file '{module_name}' was not found in sub-app package."
287
+ )
267
288
 
268
289
  # Unique scoped module spec name to avoid sys.path pollution
269
290
  slug = os.path.basename(app_dir)
@@ -275,6 +296,8 @@ def load_wsgi_app_from_path(app_dir: str, entry_point: str = "app:app") -> Any:
275
296
 
276
297
  mod = importlib.util.module_from_spec(spec)
277
298
  sys.modules[scoped_module_name] = mod
299
+ if app_dir not in sys.path:
300
+ sys.path.insert(0, app_dir)
278
301
  spec.loader.exec_module(mod)
279
302
 
280
303
  # Check for Flask app instance, callable create_app factory, or WSGI middleware
@@ -402,6 +425,7 @@ def stage_git_repo(
402
425
  if not is_safe_repo_url(repo_url):
403
426
  raise ValueError(f"Invalid or unsafe git repository URL: '{repo_url}'")
404
427
 
428
+ logger.info("Staging Git repository installation from '%s'", repo_url)
405
429
  cleanup_expired_staged_sessions()
406
430
 
407
431
  import tempfile
@@ -426,6 +450,7 @@ def stage_git_repo(
426
450
  res.stderr.strip() or f"Failed to clone repository from '{repo_url}'."
427
451
  )
428
452
  except Exception as git_err:
453
+ logger.error("Failed to clone Git repository '%s': %s", repo_url, git_err)
429
454
  if os.path.exists(temp_stage_dir):
430
455
  shutil.rmtree(temp_stage_dir, ignore_errors=True)
431
456
  raise ValueError(f"Failed to clone Git repository: {git_err}")
@@ -471,6 +496,13 @@ def stage_git_repo(
471
496
  "created_at": time.time(),
472
497
  }
473
498
  _write_staging_metadata(temp_stage_dir, _staged_installations[staging_id])
499
+ logger.info(
500
+ "Staged Git app '%s' (staging_id=%s, risk=%s, safe=%s)",
501
+ resolved_slug,
502
+ staging_id,
503
+ scan_report.risk_level,
504
+ scan_report.is_safe,
505
+ )
474
506
 
475
507
  return staging_id, scan_report, manifest
476
508
 
@@ -553,6 +585,13 @@ def stage_zip_file(
553
585
  "created_at": time.time(),
554
586
  }
555
587
  _write_staging_metadata(temp_stage_dir, _staged_installations[staging_id])
588
+ logger.info(
589
+ "Staged ZIP package '%s' (filename: %s, risk=%s, safe=%s)",
590
+ resolved_slug,
591
+ filename,
592
+ scan_report.risk_level,
593
+ scan_report.is_safe,
594
+ )
556
595
 
557
596
  return staging_id, scan_report, manifest
558
597
 
@@ -581,6 +620,7 @@ def finalize_staged_installation(
581
620
 
582
621
  final_slug = sanitize_slug(slug or session.get("slug") or session.get("name") or "sub-app")
583
622
  target_dir = os.path.join(current_app.config["INSTALLED_APPS_DIR"], final_slug)
623
+ logger.info("Finalizing installation of staged app '%s' into '%s'", final_slug, target_dir)
584
624
 
585
625
  if os.path.exists(target_dir):
586
626
  if os.path.exists(staging_dir):
@@ -631,6 +671,7 @@ def finalize_staged_installation(
631
671
  else:
632
672
  initial_settings[s_key] = s_val
633
673
 
674
+ logger.info("Installing dependencies for '%s'...", final_slug)
634
675
  install_dependencies(target_dir)
635
676
 
636
677
  installed_app = InstalledApp(
@@ -73,22 +73,34 @@ def system_health():
73
73
 
74
74
  db.session.execute(text("SELECT 1"))
75
75
  except Exception as e:
76
+ import logging
77
+
78
+ logging.getLogger("appmanager.api").exception(f"Database health check failed: {e}")
76
79
  db_ok = False
77
- db_error = str(e)
80
+ if current_app.config.get("DEBUG", False):
81
+ db_error = str(e)
78
82
 
79
- total_apps = InstalledApp.query.count()
80
- active_apps = InstalledApp.query.filter_by(is_active=True).count()
83
+ total_apps = 0
84
+ active_apps = 0
85
+ if db_ok:
86
+ try:
87
+ total_apps = InstalledApp.query.count()
88
+ active_apps = InstalledApp.query.filter_by(is_active=True).count()
89
+ except Exception:
90
+ pass
81
91
 
82
92
  overall_status = "healthy" if db_ok else "unhealthy"
83
93
 
84
- return jsonify(
85
- {
86
- "status": overall_status,
87
- "timestamp": datetime.now(timezone.utc).isoformat(),
88
- "database": {"connected": db_ok, "error": db_error},
89
- "apps": {"total": total_apps, "active": active_apps},
90
- }
91
- ), 200 if db_ok else 503
94
+ resp_data = {
95
+ "status": overall_status,
96
+ "timestamp": datetime.now(timezone.utc).isoformat(),
97
+ "database": {"connected": db_ok},
98
+ "apps": {"total": total_apps, "active": active_apps},
99
+ }
100
+ if db_error is not None:
101
+ resp_data["database"]["error"] = db_error
102
+
103
+ return jsonify(resp_data), 200 if db_ok else 503
92
104
 
93
105
 
94
106
  @api_bp.route("/apps", methods=["GET"])
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from datetime import datetime, timezone
2
3
 
3
4
  from flask import (
@@ -23,6 +24,7 @@ from appmanager.database import db
23
24
  from appmanager.models import InstalledApp, MagicLinkToken, User, UserAppPermission
24
25
  from appmanager.security import check_rate_limit, is_safe_redirect_url
25
26
 
27
+ logger = logging.getLogger("appmanager.auth")
26
28
  auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
27
29
 
28
30
 
@@ -983,6 +983,19 @@ def check_deps_cli(
983
983
  query = query.filter_by(slug=slug)
984
984
  apps = query.all()
985
985
 
986
+ if not apps and slug and slug.lower() != "all":
987
+ target_dir = os.path.join(installed_apps_dir, slug) if not os.path.isabs(slug) else slug
988
+ if os.path.isdir(target_dir):
989
+ manifest = parse_manifest(target_dir) or {}
990
+ app_name = manifest.get("name", slug)
991
+ apps = [
992
+ type(
993
+ "DiskApp",
994
+ (),
995
+ {"name": app_name, "slug": os.path.basename(target_dir.rstrip("/"))},
996
+ )()
997
+ ]
998
+
986
999
  if not apps:
987
1000
  print("No matching installed applications found.\n")
988
1001
  return 0
@@ -6,7 +6,17 @@ from dotenv import load_dotenv
6
6
  # Automatically load .env if present in current working directory or base directory
7
7
  load_dotenv()
8
8
 
9
- DEFAULT_BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
9
+ # Resolve base directory: when running as installed package, use current working directory
10
+ _pkg_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
11
+ if (
12
+ "site-packages" in _pkg_root
13
+ or "dist-packages" in _pkg_root
14
+ or not os.path.exists(os.path.join(_pkg_root, "pyproject.toml"))
15
+ ):
16
+ DEFAULT_BASE_DIR = os.path.abspath(os.getcwd())
17
+ else:
18
+ DEFAULT_BASE_DIR = _pkg_root
19
+
10
20
  BASE_DIR = os.getenv("APPMANAGER_BASE_DIR", DEFAULT_BASE_DIR)
11
21
 
12
22
 
@@ -73,9 +83,16 @@ class Config:
73
83
 
74
84
  # Security settings
75
85
  SESSION_COOKIE_HTTPONLY = True
76
- SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE", "Lax")
86
+ SESSION_COOKIE_SAMESITE = "Lax"
77
87
  SESSION_COOKIE_SECURE = os.getenv("SESSION_COOKIE_SECURE", "false").lower() in (
78
88
  "true",
79
89
  "1",
80
90
  "yes",
81
91
  )
92
+
93
+ # Logging Configuration
94
+ LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
95
+ LOG_FORMAT = os.getenv(
96
+ "LOG_FORMAT",
97
+ "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
98
+ )
@@ -282,12 +282,18 @@ def install_app_dependencies(
282
282
  timeout: int = 180,
283
283
  ) -> Tuple[bool, str]:
284
284
  """
285
- Installs sub-app dependencies from requirements.txt based on configured venv mode.
285
+ Installs sub-app dependencies from requirements.txt and installs the sub-app itself
286
+ into the virtual environment if it is an installable Python package (has pyproject.toml or setup.py).
286
287
  Returns (success, output_or_error_message).
287
288
  """
288
289
  req_file = os.path.join(app_dir, "requirements.txt")
289
- if not os.path.exists(req_file):
290
- return True, "No requirements.txt found (nothing to install)."
290
+ has_reqs = os.path.exists(req_file)
291
+ has_pkg = os.path.exists(os.path.join(app_dir, "pyproject.toml")) or os.path.exists(
292
+ os.path.join(app_dir, "setup.py")
293
+ )
294
+
295
+ if not has_reqs and not has_pkg:
296
+ return True, "No requirements.txt or pyproject.toml found (nothing to install)."
291
297
 
292
298
  if venv_mode == "isolated":
293
299
  venv_dir = os.path.join(app_dir, ".venv")
@@ -314,26 +320,66 @@ def install_app_dependencies(
314
320
  except Exception:
315
321
  pass
316
322
 
317
- install_cmd = [pip_path, "install", "-r", req_file]
323
+ pip_base = [pip_path, "install"]
318
324
  else:
319
325
  # Singular shared host virtual environment
320
- install_cmd = [sys.executable, "-m", "pip", "install", "-r", req_file]
326
+ pip_base = [sys.executable, "-m", "pip", "install"]
321
327
 
322
- try:
323
- proc = subprocess.run(
324
- install_cmd,
325
- capture_output=True,
326
- text=True,
327
- timeout=timeout,
328
- )
329
- if proc.returncode == 0:
330
- return True, proc.stdout or "Dependencies installed successfully."
331
- else:
332
- return False, f"Pip installation failed (code {proc.returncode}): {proc.stderr}"
333
- except subprocess.TimeoutExpired:
334
- return False, f"Dependency installation timed out after {timeout} seconds."
335
- except Exception as e:
336
- return False, f"Unexpected error during dependency installation: {str(e)}"
328
+ outputs: List[str] = []
329
+
330
+ # 1. Install requirements.txt if present
331
+ if has_reqs:
332
+ try:
333
+ proc = subprocess.run(
334
+ pip_base + ["-r", req_file],
335
+ capture_output=True,
336
+ text=True,
337
+ timeout=timeout,
338
+ )
339
+ if proc.returncode != 0:
340
+ return (
341
+ False,
342
+ f"Pip requirements installation failed (code {proc.returncode}): {proc.stderr}",
343
+ )
344
+ if proc.stdout:
345
+ outputs.append(proc.stdout.strip())
346
+ except subprocess.TimeoutExpired:
347
+ return False, f"Dependency installation timed out after {timeout} seconds."
348
+ except Exception as e:
349
+ return False, f"Unexpected error during requirements installation: {str(e)}"
350
+
351
+ # 2. Install package itself if installable (pyproject.toml or setup.py present)
352
+ if has_pkg:
353
+ try:
354
+ # Attempt editable install, fallback to standard directory install
355
+ proc = subprocess.run(
356
+ pip_base + ["-e", app_dir],
357
+ capture_output=True,
358
+ text=True,
359
+ timeout=timeout,
360
+ )
361
+ if proc.returncode != 0:
362
+ proc = subprocess.run(
363
+ pip_base + [app_dir],
364
+ capture_output=True,
365
+ text=True,
366
+ timeout=timeout,
367
+ )
368
+ if proc.returncode != 0:
369
+ return (
370
+ False,
371
+ f"Package self-installation failed (code {proc.returncode}): {proc.stderr}",
372
+ )
373
+ if proc.stdout:
374
+ outputs.append(proc.stdout.strip())
375
+ except subprocess.TimeoutExpired:
376
+ return False, f"Package installation timed out after {timeout} seconds."
377
+ except Exception as e:
378
+ return False, f"Unexpected error during package installation: {str(e)}"
379
+
380
+ return True, "\n".join(
381
+ outputs
382
+ ) if outputs else "Package and dependencies installed successfully."
337
383
 
338
384
 
339
385
  def analyze_uninstall_dependencies(
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import os
2
3
  from typing import Any, List, Optional, Union
3
4
 
@@ -15,6 +16,26 @@ from appmanager.middleware import DynamicAppDispatcherMiddleware
15
16
  from appmanager.models import InstalledApp, UserAppPermission
16
17
 
17
18
 
19
+ def _setup_logging(app: Flask) -> None:
20
+ """
21
+ Configures structured logging for the AppManager package.
22
+ """
23
+ level_name = app.config.get("LOG_LEVEL", "INFO").upper()
24
+ log_level = getattr(logging, level_name, logging.INFO)
25
+ log_format = app.config.get(
26
+ "LOG_FORMAT",
27
+ "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
28
+ )
29
+
30
+ logger = logging.getLogger("appmanager")
31
+ logger.setLevel(log_level)
32
+
33
+ if not logger.handlers:
34
+ handler = logging.StreamHandler()
35
+ handler.setFormatter(logging.Formatter(log_format))
36
+ logger.addHandler(handler)
37
+
38
+
18
39
  class AppManager:
19
40
  """
20
41
  Standard Flask extension class for AppManager.
@@ -53,6 +74,8 @@ class AppManager:
53
74
  if key.isupper() and key not in app.config:
54
75
  app.config[key] = getattr(Config, key)
55
76
 
77
+ _setup_logging(app)
78
+
56
79
  # Ensure base directories exist
57
80
  base_dir = app.config.get("BASE_DIR", Config.BASE_DIR)
58
81
  os.makedirs(app.config["INSTALLED_APPS_DIR"], exist_ok=True)
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import os
2
3
  import re
3
4
  from typing import Any, Callable, Dict, Optional
@@ -10,6 +11,7 @@ from appmanager.database import db
10
11
  from appmanager.models import InstalledApp, User, UserAppPermission
11
12
  from appmanager.signals import subapp_reloaded
12
13
 
14
+ logger = logging.getLogger("appmanager.dispatcher")
13
15
  VALID_SLUG_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
14
16
 
15
17
 
@@ -206,22 +208,42 @@ class DynamicAppDispatcherMiddleware:
206
208
  environ["HTTP_X_APPMANAGER_SUBAPP_SLUG"] = slug
207
209
  environ["HTTP_X_FORWARDED_PREFIX"] = f"/apps/{slug}"
208
210
 
211
+ logger.debug(
212
+ "Dispatching request '%s %s' to sub-app '%s' (user: %s)",
213
+ environ.get("REQUEST_METHOD", "GET"),
214
+ path_info,
215
+ slug,
216
+ user.email if user else "anonymous",
217
+ )
218
+
209
219
  # 3. Load or Retrieve Sub-App WSGI Instance
210
220
  app_dir = os.path.join(
211
221
  self.main_app.config["INSTALLED_APPS_DIR"], app_record.slug
212
222
  )
213
223
  if not os.path.exists(app_dir):
224
+ logger.warning("Sub-app directory for '%s' not found at %s", slug, app_dir)
225
+ is_admin = bool(user and getattr(user, "is_admin", False))
226
+ debug_mode = bool(self.main_app.config.get("DEBUG", False))
227
+ if is_admin or debug_mode:
228
+ msg = f"App directory for '{app_record.name}' was not found on the server filesystem."
229
+ else:
230
+ msg = f"The '{app_record.name}' application is currently unavailable."
214
231
  return self._send_html_response(
215
232
  start_response,
216
233
  500,
217
- "Installation Error",
218
- f"App directory for '{app_record.name}' was not found on the server filesystem.",
234
+ "Application Unavailable",
235
+ msg,
219
236
  link_url="/",
220
237
  link_text="Return to Home",
221
238
  )
222
239
 
223
240
  try:
224
241
  if slug not in self.sub_app_cache:
242
+ logger.info(
243
+ "Loading sub-app '%s' into WSGI cache (entrypoint: %s)",
244
+ slug,
245
+ app_record.entry_point,
246
+ )
225
247
  sub_app_obj = load_wsgi_app_from_path(app_dir, app_record.entry_point)
226
248
  # Get standard wsgi_app callable if it's a Flask instance
227
249
  wsgi_callable = getattr(sub_app_obj, "wsgi_app", sub_app_obj)
@@ -233,11 +255,23 @@ class DynamicAppDispatcherMiddleware:
233
255
 
234
256
  wsgi_callable = self.sub_app_cache[slug]
235
257
  except Exception as e:
258
+ import logging
259
+
260
+ logging.getLogger("appmanager.dispatcher").exception(
261
+ f"Error loading sub-app '{app_record.slug}': {e}"
262
+ )
263
+ is_admin = bool(user and getattr(user, "is_admin", False))
264
+ debug_mode = bool(self.main_app.config.get("DEBUG", False))
265
+ if is_admin or debug_mode:
266
+ err_msg = f"Failed to load Flask sub-app '{app_record.name}': {str(e)}"
267
+ else:
268
+ err_msg = f"The '{app_record.name}' application encountered an error while starting. Please contact an administrator."
269
+
236
270
  return self._send_html_response(
237
271
  start_response,
238
272
  500,
239
- "App Load Error",
240
- f"Failed to load Flask sub-app '{app_record.name}': {str(e)}",
273
+ "Application Error",
274
+ err_msg,
241
275
  link_url="/",
242
276
  link_text="Return to Home",
243
277
  )
@@ -116,20 +116,35 @@ def is_safe_repo_url(repo_url: str) -> bool:
116
116
  def validate_entrypoint_path(app_dir: str, entry_point: str) -> Tuple[bool, str]:
117
117
  """
118
118
  Ensures an entrypoint string does not escape the app directory via directory traversal.
119
+ Supports module names ('app'), dotted paths ('src.app'), and package paths ('appmanager').
119
120
  """
120
121
  if ":" in entry_point:
121
122
  module_name = entry_point.split(":", 1)[0]
122
123
  else:
123
124
  module_name = entry_point
124
125
 
125
- # Disallow path separators in module name
126
- if "/" in module_name or "\\" in module_name or ".." in module_name:
126
+ # Disallow parent references
127
+ if ".." in module_name:
127
128
  return False, "Entrypoint module name cannot contain path separators or parent references."
128
129
 
129
- module_file = os.path.join(app_dir, f"{module_name}.py")
130
- abs_module = os.path.abspath(module_file)
131
130
  abs_app_dir = os.path.abspath(app_dir)
132
131
 
132
+ # 1. Direct .py file path
133
+ if module_name.endswith(".py"):
134
+ cand_path = os.path.join(app_dir, module_name)
135
+ else:
136
+ cand_path = os.path.join(app_dir, f"{module_name}.py")
137
+ if not os.path.exists(cand_path):
138
+ # 2. Check dotted or nested path: e.g. src.app -> src/app.py or pkg/__init__.py
139
+ parts = module_name.replace("\\", "/").replace(".", "/").split("/")
140
+ cand_py = os.path.join(app_dir, *parts) + ".py"
141
+ cand_init = os.path.join(app_dir, *parts, "__init__.py")
142
+ if os.path.exists(cand_py):
143
+ cand_path = cand_py
144
+ elif os.path.exists(cand_init):
145
+ cand_path = cand_init
146
+
147
+ abs_module = os.path.abspath(cand_path)
133
148
  if not abs_module.startswith(abs_app_dir):
134
149
  return False, "Entrypoint file escapes application directory."
135
150
 
@@ -207,7 +207,7 @@
207
207
  <button type="submit" class="btn btn-ghost btn-sm" title="Set as default landing app">Default</button>
208
208
  {% endif %}
209
209
  </form>
210
- <button type="button" class="btn btn-danger btn-sm" onclick="openConfirm('uninstall-{{ app.id }}', 'Uninstall {{ app.name }}?', 'Type <code>{{ app.slug }}</code> to confirm')">Uninstall</button>
210
+ <button type="button" class="btn btn-danger btn-sm" onclick="openConfirm('/admin/apps/{{ app.id }}/delete', '{{ app.slug }}', 'Uninstall {{ app.name }}?', 'Type <code>{{ app.slug }}</code> to confirm')">Uninstall</button>
211
211
  </div>
212
212
  </td>
213
213
  </tr>
@@ -333,7 +333,7 @@
333
333
  <td class="num">{{ role_counts.get(r.slug, 0) }}</td>
334
334
  <td>
335
335
  {% if not r.is_system %}
336
- <button type="button" class="btn btn-danger btn-sm" onclick="openConfirm('role-{{ r.id }}', 'Delete role {{ r.name }}?', 'Type <code>{{ r.slug }}</code> to confirm')">Delete</button>
336
+ <button type="button" class="btn btn-danger btn-sm" onclick="openConfirm('/admin/roles/{{ r.id }}/delete', '{{ r.slug }}', 'Delete role {{ r.name }}?', 'Type <code>{{ r.slug }}</code> to confirm')">Delete</button>
337
337
  {% else %}
338
338
  <span class="text-faint" style="font-size:0.75rem;">Protected</span>
339
339
  {% endif %}
@@ -621,18 +621,15 @@
621
621
  }
622
622
 
623
623
  // Type-to-confirm destructive action modal
624
- var confirmSlug = '';
625
- function openConfirm(slug, title, desc) {
626
- confirmSlug = slug;
627
- document.getElementById('confirm-title').textContent = title;
628
- document.getElementById('confirm-desc').innerHTML = desc;
624
+ var expectedConfirmText = '';
625
+ function openConfirm(actionUrl, expectedSlug, title, desc) {
626
+ expectedConfirmText = (expectedSlug || '').trim();
627
+ document.getElementById('confirm-title').textContent = title || 'Confirm Action';
628
+ document.getElementById('confirm-desc').innerHTML = desc || ('Type <code>' + expectedConfirmText + '</code> to confirm');
629
629
  document.getElementById('confirm-input').value = '';
630
+ document.getElementById('confirm-input').placeholder = expectedConfirmText;
630
631
  document.getElementById('confirm-submit').disabled = true;
631
- document.getElementById('confirm-form').action = '/admin/apps/' + slug.split('-').pop() + '/delete';
632
- // For roles, the action differs
633
- if (slug.indexOf('role-') === 0) {
634
- document.getElementById('confirm-form').action = '/admin/roles/' + slug.split('-').pop() + '/delete';
635
- }
632
+ document.getElementById('confirm-form').action = actionUrl;
636
633
  document.getElementById('confirm-modal').style.display = 'flex';
637
634
  document.getElementById('confirm-input').focus();
638
635
  }
@@ -640,7 +637,7 @@
640
637
  document.getElementById('confirm-modal').style.display = 'none';
641
638
  }
642
639
  document.getElementById('confirm-input').addEventListener('input', function (e) {
643
- document.getElementById('confirm-submit').disabled = (e.target.value.trim() !== confirmSlug.split('-').pop());
640
+ document.getElementById('confirm-submit').disabled = (e.target.value.trim() !== expectedConfirmText);
644
641
  });
645
642
  document.getElementById('confirm-modal').addEventListener('click', function (e) {
646
643
  if (e.target === this) closeConfirm();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmanager-server
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: High-performance multi-tenant WSGI portal, dynamic sub-app dispatcher, and extension framework for Python/Flask.
5
5
  Author: AppManager Contributors
6
6
  License: MIT
@@ -1,4 +1,3 @@
1
- .env.example
2
1
  CHANGELOG.md
3
2
  LICENSE
4
3
  MANIFEST.in
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "appmanager-server"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "High-performance multi-tenant WSGI portal, dynamic sub-app dispatcher, and extension framework for Python/Flask."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -116,3 +116,24 @@ def test_install_app_dependencies_no_reqs(tmp_path):
116
116
  ok, msg = install_app_dependencies(str(empty_dir), venv_mode="singular")
117
117
  assert ok is True
118
118
  assert "nothing to install" in msg
119
+
120
+
121
+ def test_install_app_dependencies_package_structure(tmp_path, monkeypatch):
122
+ pkg_dir = tmp_path / "pkg_app"
123
+ pkg_dir.mkdir()
124
+ (pkg_dir / "pyproject.toml").write_text("[project]\nname = 'pkg-app'\nversion = '0.1.0'\n")
125
+
126
+ # Mock subprocess.run to verify pip install -e is invoked
127
+ called_cmds = []
128
+
129
+ def mock_run(cmd, **kwargs):
130
+ called_cmds.append(cmd)
131
+ return type(
132
+ "Proc", (), {"returncode": 0, "stdout": "Successfully installed pkg-app", "stderr": ""}
133
+ )()
134
+
135
+ monkeypatch.setattr("subprocess.run", mock_run)
136
+
137
+ ok, msg = install_app_dependencies(str(pkg_dir), venv_mode="singular")
138
+ assert ok is True
139
+ assert any("-e" in c and str(pkg_dir) in c for c in called_cmds)
@@ -12,7 +12,7 @@ from appmanager.cli import main
12
12
 
13
13
 
14
14
  def test_package_exports():
15
- assert __version__ == "0.3.0"
15
+ assert __version__ == "0.4.0"
16
16
  assert callable(create_app)
17
17
  assert callable(create_dispatchable_app)
18
18
  assert DynamicAppDispatcherMiddleware is not None
@@ -25,7 +25,7 @@ def test_cli_version(capsys):
25
25
  main(["--version"])
26
26
  assert exc.value.code == 0
27
27
  captured = capsys.readouterr()
28
- assert "0.3.0" in captured.out or "0.3.0" in captured.err
28
+ assert __version__ in captured.out or __version__ in captured.err
29
29
 
30
30
 
31
31
  def test_cli_help(capsys):
@@ -1,51 +0,0 @@
1
- # ==============================================================================
2
- # AppManager Configuration
3
- # Copy this file to .env and adjust the values as needed.
4
- # ==============================================================================
5
-
6
- # Core Application Security
7
- # IMPORTANT: Generate strong random secrets for production!
8
- SECRET_KEY=appmanager-change-this-to-a-secure-random-string-in-production-min-32-chars
9
- JWT_SECRET=jwt-change-this-to-a-secure-random-string-in-production-min-32-chars
10
- JWT_ACCESS_TOKEN_EXPIRES_DAYS=7
11
- MAGIC_LINK_EXPIRES_MINUTES=15
12
-
13
- # Server & Host URL
14
- APP_BASE_URL=http://localhost:5000
15
- PORT=5000
16
- HOST=0.0.0.0
17
- TEMPLATES_AUTO_RELOAD=true
18
- SESSION_COOKIE_SAMESITE=Lax
19
- SESSION_COOKIE_SECURE=false
20
-
21
- # Database Configuration (SQLite default, PostgreSQL/MySQL supported)
22
- # Examples:
23
- # SQLite: sqlite:///instance/appmanager.db
24
- # PostgreSQL: postgresql://user:password@localhost:5432/appmanager
25
- # MySQL: mysql+pymysql://user:password@localhost:3306/appmanager
26
- DATABASE_URL=sqlite:///instance/appmanager.db
27
-
28
- # Storage Directories
29
- # INSTALLED_APPS_DIR=./installed_apps
30
- # TEMP_UPLOAD_DIR=./instance/uploads
31
-
32
- # Google OAuth 2.0 (Optional - for Google Login)
33
- GOOGLE_CLIENT_ID=
34
- GOOGLE_CLIENT_SECRET=
35
- GOOGLE_DISCOVERY_URL=https://accounts.google.com/.well-known/openid-configuration
36
-
37
- # SMTP Email Configuration (Required for Magic Link email delivery in production)
38
- SMTP_SERVER=
39
- SMTP_PORT=587
40
- SMTP_USER=
41
- SMTP_PASSWORD=
42
- MAIL_DEFAULT_SENDER=noreply@appmanager.local
43
-
44
- # Developer Magic Login (Set to true to show one-click login button when SMTP is disabled)
45
- ALLOW_DEV_MAGIC_LOGIN=false
46
-
47
- # Role & Admin Provisioning
48
- # FIRST_USER_IS_ADMIN: If true, the very first user who registers will be granted the admin role
49
- FIRST_USER_IS_ADMIN=true
50
- # ADMIN_EMAILS: Comma-separated list of emails that automatically receive admin role upon registration
51
- ADMIN_EMAILS=
@@ -1,11 +0,0 @@
1
- include LICENSE
2
- include README.md
3
- include CHANGELOG.md
4
- include SECURITY.md
5
- include requirements.txt
6
- include .env.example
7
- include appmanager/py.typed
8
- recursive-include appmanager/templates *
9
- recursive-include appmanager/static *
10
- recursive-exclude * __pycache__
11
- recursive-exclude * *.py[co]