appmanager-server 0.3.1__tar.gz → 0.4.1__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 (135) hide show
  1. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/CHANGELOG.md +54 -0
  2. {appmanager_server-0.3.1/appmanager_server.egg-info → appmanager_server-0.4.1}/PKG-INFO +95 -1
  3. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/README.md +94 -0
  4. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/__init__.py +1 -1
  5. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/admin/app_installer.py +195 -7
  6. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/admin/registry.py +4 -1
  7. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/admin/routes.py +265 -21
  8. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/api/routes.py +14 -2
  9. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/app_config.py +57 -0
  10. appmanager_server-0.4.1/appmanager/audit.py +65 -0
  11. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/auth/routes.py +17 -0
  12. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/bridge.py +40 -0
  13. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/cli.py +73 -10
  14. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/config.py +1 -0
  15. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/database.py +95 -0
  16. appmanager_server-0.4.1/appmanager/db_access.py +378 -0
  17. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/extension.py +114 -5
  18. appmanager_server-0.4.1/appmanager/host_settings.py +172 -0
  19. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/middleware.py +163 -0
  20. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/models.py +205 -0
  21. appmanager_server-0.4.1/appmanager/ratelimit.py +68 -0
  22. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/security.py +71 -1
  23. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/css/app.css +227 -36
  24. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/admin/app_detail.html +142 -0
  25. appmanager_server-0.4.1/appmanager/templates/admin/dashboard.html +1753 -0
  26. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/auth/login.html +9 -0
  27. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/base.html +17 -1
  28. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/index.html +8 -1
  29. appmanager_server-0.4.1/appmanager/templates/privacy.html +23 -0
  30. appmanager_server-0.4.1/appmanager/templates/terms.html +26 -0
  31. {appmanager_server-0.3.1 → appmanager_server-0.4.1/appmanager_server.egg-info}/PKG-INFO +95 -1
  32. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager_server.egg-info/SOURCES.txt +8 -0
  33. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/pyproject.toml +1 -1
  34. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_auth.py +25 -5
  35. appmanager_server-0.4.1/tests/test_db_access.py +236 -0
  36. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_invalid_install_failures.py +127 -0
  37. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_packaging_and_cli.py +27 -2
  38. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_security_hardening.py +20 -0
  39. appmanager_server-0.4.1/tests/test_seo_and_settings.py +333 -0
  40. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_user_elevation_and_visibility.py +20 -3
  41. appmanager_server-0.3.1/appmanager/templates/admin/dashboard.html +0 -1030
  42. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/LICENSE +0 -0
  43. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/MANIFEST.in +0 -0
  44. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/SECURITY.md +0 -0
  45. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/admin/__init__.py +0 -0
  46. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/admin/members.py +0 -0
  47. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/api/__init__.py +0 -0
  48. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/auth/__init__.py +0 -0
  49. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/auth/oauth.py +0 -0
  50. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/auth/utils.py +0 -0
  51. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/dependency_manager.py +0 -0
  52. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/extensions.py +0 -0
  53. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/health.py +0 -0
  54. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/hooks.py +0 -0
  55. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/py.typed +0 -0
  56. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/sdk.py +0 -0
  57. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/security_scanner.py +0 -0
  58. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/signals.py +0 -0
  59. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/fonts.css +0 -0
  60. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-0.woff2 +0 -0
  61. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-1.woff2 +0 -0
  62. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-10.woff2 +0 -0
  63. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-11.woff2 +0 -0
  64. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-12.woff2 +0 -0
  65. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-13.woff2 +0 -0
  66. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-14.woff2 +0 -0
  67. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-15.woff2 +0 -0
  68. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-16.woff2 +0 -0
  69. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-17.woff2 +0 -0
  70. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-18.woff2 +0 -0
  71. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-19.woff2 +0 -0
  72. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-2.woff2 +0 -0
  73. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-20.woff2 +0 -0
  74. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-21.woff2 +0 -0
  75. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-22.woff2 +0 -0
  76. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-23.woff2 +0 -0
  77. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-3.woff2 +0 -0
  78. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-4.woff2 +0 -0
  79. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-5.woff2 +0 -0
  80. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-6.woff2 +0 -0
  81. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-7.woff2 +0 -0
  82. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-8.woff2 +0 -0
  83. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-9.woff2 +0 -0
  84. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-0.woff2 +0 -0
  85. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-1.woff2 +0 -0
  86. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-10.woff2 +0 -0
  87. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-11.woff2 +0 -0
  88. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-2.woff2 +0 -0
  89. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-3.woff2 +0 -0
  90. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-4.woff2 +0 -0
  91. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-5.woff2 +0 -0
  92. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-6.woff2 +0 -0
  93. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-7.woff2 +0 -0
  94. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-8.woff2 +0 -0
  95. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-9.woff2 +0 -0
  96. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/static/vendor/htmx/htmx.min.js +0 -0
  97. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/admin/_member_drawer.html +0 -0
  98. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/admin/_members_table.html +0 -0
  99. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/admin/flairs.html +0 -0
  100. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/admin/permissions.html +0 -0
  101. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/admin/roles.html +0 -0
  102. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/admin/users.html +0 -0
  103. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/auth/profile.html +0 -0
  104. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/errors/401.html +0 -0
  105. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/errors/403.html +0 -0
  106. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/errors/404.html +0 -0
  107. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager/templates/errors/500.html +0 -0
  108. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager_server.egg-info/dependency_links.txt +0 -0
  109. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager_server.egg-info/entry_points.txt +0 -0
  110. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager_server.egg-info/requires.txt +0 -0
  111. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/appmanager_server.egg-info/top_level.txt +0 -0
  112. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/requirements.txt +0 -0
  113. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/setup.cfg +0 -0
  114. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_api.py +0 -0
  115. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_app_settings.py +0 -0
  116. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_app_settings_config.py +0 -0
  117. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_app_updates.py +0 -0
  118. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_cli_dev_and_templates.py +0 -0
  119. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_cli_health.py +0 -0
  120. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_cli_scaffolding.py +0 -0
  121. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_dependency_manager.py +0 -0
  122. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_error_pages.py +0 -0
  123. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_extension_apps.py +0 -0
  124. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_extension_pattern.py +0 -0
  125. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_hooks.py +0 -0
  126. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_installer.py +0 -0
  127. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_manifest_sdk.py +0 -0
  128. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_middleware.py +0 -0
  129. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_nested_portal.py +0 -0
  130. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_role_management.py +0 -0
  131. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_sdk.py +0 -0
  132. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_security_precheck_flow.py +0 -0
  133. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_security_scanner.py +0 -0
  134. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_signals.py +0 -0
  135. {appmanager_server-0.3.1 → appmanager_server-0.4.1}/tests/test_user_tracking.py +0 -0
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [Unreleased]
11
+
12
+ ### Added
13
+ - **SEO capabilities**: apps can declare SEO metadata in their manifest (`seo`
14
+ block) which the host persists to `InstalledApp` and renders into served HTML.
15
+ - New `InstalledApp` SEO columns (`seo_title`, `seo_description`, `seo_keywords`,
16
+ `seo_canonical_url`, `seo_og_image`, `seo_robots`, `seo_json_ld`) + auto-migration.
17
+ - `base.html` `head_meta` block renders portal SEO (title, description, keywords,
18
+ canonical, OG, Twitter, robots, JSON-LD).
19
+ - Dispatcher middleware injects app SEO into sub-app HTML `<head>` (no duplicate
20
+ tags; auth apps forced to `noindex` when configured).
21
+ - `/robots.txt` and `/sitemap.xml` routes (sitemap lists public apps).
22
+ - Admin app detail page shows a read-only **SEO** panel.
23
+ - **Admin Settings page** (`/admin/settings`) with three sections:
24
+ - **SEO**: master toggle, portal SEO fields, per-app override, auth noindex,
25
+ sitemap toggle.
26
+ - **Dashboard & Login**: login-required toggle, dashboard on/off, default-app
27
+ redirect.
28
+ - **Visibility**: show/hide login-required apps; adaptive button labels
29
+ (Login / Permission Required / Launch).
30
+ - **Host settings store**: new `HostSetting` model + `host_settings` table and
31
+ `appmanager.host_settings` helpers (typed get/set with canonical defaults).
32
+
33
+ ---
34
+
35
+ ## [0.4.1] - 2026-09-09
36
+
37
+ ### Performance
38
+ - **Batch admin dashboard queries**: health-log fetch for all apps is now a single
39
+ batched query (was 2 queries per app) and role member counts use one `GROUP BY`
40
+ instead of one count query per role. Dashboard health: 60 → 21 queries;
41
+ role counts: 4 → 1 query.
42
+ - **Batch permissions matrix update**: the `/admin/permissions` POST handler now
43
+ loads existing permissions into a lookup map once instead of issuing one query
44
+ per user × app cell (was O(users × apps) queries). 405 → 31 queries, ~4.2×
45
+ faster wall-clock on 20 apps × 10 users.
46
+ - **Bulk-insert grant-all permissions on install**: `app_installer` now inserts
47
+ per-user permissions in a single `bulk_insert_mappings` statement (was one
48
+ INSERT per user), skipping users who already have a row.
49
+ - **Windowed health-log purge**: the CLI maintenance command keeps the latest 100
50
+ health logs per app with a single windowed `DELETE` (was a per-app loop of
51
+ row-by-row deletes).
52
+ - **Batch admin-panel sync**: `sync_panels` loads existing panels for an app once
53
+ into a map instead of one query per declared panel.
54
+
55
+ ---
56
+
57
+ ## [0.4.0] - 2026-08-28
58
+
59
+ ### Added
60
+ - **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.
61
+
62
+ ---
63
+
10
64
  ## [0.3.1] - 2026-08-28
11
65
 
12
66
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmanager-server
3
- Version: 0.3.1
3
+ Version: 0.4.1
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
@@ -120,6 +120,11 @@ graph LR
120
120
  - 🩺 **Automated Health Monitoring**: Sub-app health evaluation contract (`/health` endpoint or `get_health()` callable) tracked in `AppHealthLog` with one-click admin execution.
121
121
  - 📊 **In-Process Telemetry Bridge**: High-performance telemetry reporting allowing sub-apps to record events and metrics directly to the host database with zero network overhead.
122
122
  - 🔒 **Granular Role & Permissions Matrix**: Full RBAC role management and per-user permission matrix controlling access to every installed sub-app.
123
+ - 🗄️ **Per-App Database Access**: Apps can request scoped or full access to the host database; the admin approves/denies at install and can adjust later (with optional data migration).
124
+ - 🔐 **Read-Only Auth Access**: Apps can request a narrow read-only auth subset (login state, display name, role) — never email, id, or tokens.
125
+ - 🔑 **Per-App API Keys**: Each installed app gets a generated API key for service-to-service auth back to the host REST API.
126
+ - 📜 **Audit Log**: Append-only trail of install/uninstall, permission grants/revokes, config changes, and API key rotation.
127
+ - 🚦 **Bridge Rate Limiting**: Token-bucket rate limiting on in-process telemetry/storage calls to stop one app from flooding the host.
123
128
 
124
129
  ---
125
130
 
@@ -177,6 +182,21 @@ Debug and test your sub-app locally with mock user authentication headers:
177
182
  appmanager dev analytics --port 5001 --email dev@example.com --role admin
178
183
  ```
179
184
 
185
+ ### Cryptographic Token & Secrets Generator
186
+
187
+ Generate production-ready secrets (`SECRET_KEY`, `JWT_SECRET`, `AIC_TOKEN_SECRET`) or individual secure tokens:
188
+
189
+ ```bash
190
+ # Generate complete set of formatted .env secrets
191
+ appmanager generate-tokens --env
192
+
193
+ # Generate a single 256-bit URL-safe token
194
+ appmanager generate-tokens
195
+
196
+ # Generate a 256-bit Hexadecimal token
197
+ appmanager generate-tokens -f hex
198
+ ```
199
+
180
200
  ---
181
201
 
182
202
  ## 🛠️ Sub-App Specification (`manifest.json`)
@@ -246,6 +266,80 @@ if __name__ == "__main__":
246
266
 
247
267
  ---
248
268
 
269
+ ## 🗄️ Per-App Database Access & Permissions
270
+
271
+ An app can request access to the host's shared database by declaring it in its
272
+ manifest. On install, the admin approves or denies the request (default is
273
+ **deny**). Permissions are stored in the `app_db_permissions` table and can be
274
+ adjusted after install from the app's **Permissions** panel.
275
+
276
+ ### Manifest fields
277
+
278
+ ```json
279
+ {
280
+ "requests_database": true,
281
+ "database_access_level": "scoped",
282
+ "database_description": "Stores user preferences in a scoped table.",
283
+ "requests_auth_readonly": true
284
+ }
285
+ ```
286
+
287
+ - `requests_database` — request shared DB access.
288
+ - `database_access_level` — `"scoped"` (own table prefix / MySQL schema) or
289
+ `"full"` (raw host DB, trusted only).
290
+ - `database_description` — shown to the admin at approval time.
291
+ - `requests_auth_readonly` — request read-only access to login state, display
292
+ name, and role only.
293
+
294
+ ### Approval flow
295
+
296
+ 1. The install security report shows a **Permission Requests** panel.
297
+ 2. The admin chooses **Deny**, **Scoped**, or **Full** for DB access, and
298
+ **Deny**/**Grant** for read-only auth access.
299
+ 3. The decision is stored; the app's SDK receives an in-process engine (never
300
+ raw credentials) when granted, or falls back to its own local SQLite when
301
+ denied.
302
+
303
+ ### Adjusting permissions after install
304
+
305
+ From the app's **Permissions** panel, the admin can change the DB scope or
306
+ revoke auth access at any time. If the DB scope changes, the admin is asked
307
+ whether to **migrate** the app's existing data (migration is a distinct,
308
+ confirmable action — never automatic).
309
+
310
+ ### Security model
311
+
312
+ - **Scoped** access hands the app a SQLAlchemy engine with its own table prefix
313
+ (`app_<slug>_`) or dedicated MySQL schema.
314
+ - **Full** access hands a raw engine to the host DB and shows a prominent
315
+ warning at approval.
316
+ - **Auth read-only** exposes only login state, display name, and role — never
317
+ email, user id, passwords, or tokens.
318
+ - Credentials never exist as strings in app memory and are never sent over the
319
+ network or written to files.
320
+
321
+ ### Per-app API keys
322
+
323
+ Each installed app gets a generated API key (stored as a secret config row).
324
+ The SDK sends it in `X-AppManager-App-Key` to authenticate service-to-service
325
+ calls back to the host REST API. The key is injected by the dispatcher, so the
326
+ app never stores or manages credentials itself.
327
+
328
+ ### Audit log
329
+
330
+ Security-relevant actions (install/uninstall, permission grants/revokes, API
331
+ key rotation) are recorded in the `audit_log` table and viewable at
332
+ `/admin/audit-log`.
333
+
334
+ ### Bridge rate limiting
335
+
336
+ In-process telemetry/storage calls (`report_event`, `report_metric`) are
337
+ rate-limited per app via a token bucket (default 100 events/min, configurable
338
+ via `BRIDGE_RATE_LIMIT_RATE` / `BRIDGE_RATE_LIMIT_BURST` /
339
+ `BRIDGE_RATE_LIMIT_ENABLED`).
340
+
341
+ ---
342
+
249
343
  ## 🧩 Pluggable UI Slots & Extension Hooks
250
344
 
251
345
  Extensions can inject components into host slots:
@@ -59,6 +59,11 @@ graph LR
59
59
  - 🩺 **Automated Health Monitoring**: Sub-app health evaluation contract (`/health` endpoint or `get_health()` callable) tracked in `AppHealthLog` with one-click admin execution.
60
60
  - 📊 **In-Process Telemetry Bridge**: High-performance telemetry reporting allowing sub-apps to record events and metrics directly to the host database with zero network overhead.
61
61
  - 🔒 **Granular Role & Permissions Matrix**: Full RBAC role management and per-user permission matrix controlling access to every installed sub-app.
62
+ - 🗄️ **Per-App Database Access**: Apps can request scoped or full access to the host database; the admin approves/denies at install and can adjust later (with optional data migration).
63
+ - 🔐 **Read-Only Auth Access**: Apps can request a narrow read-only auth subset (login state, display name, role) — never email, id, or tokens.
64
+ - 🔑 **Per-App API Keys**: Each installed app gets a generated API key for service-to-service auth back to the host REST API.
65
+ - 📜 **Audit Log**: Append-only trail of install/uninstall, permission grants/revokes, config changes, and API key rotation.
66
+ - 🚦 **Bridge Rate Limiting**: Token-bucket rate limiting on in-process telemetry/storage calls to stop one app from flooding the host.
62
67
 
63
68
  ---
64
69
 
@@ -116,6 +121,21 @@ Debug and test your sub-app locally with mock user authentication headers:
116
121
  appmanager dev analytics --port 5001 --email dev@example.com --role admin
117
122
  ```
118
123
 
124
+ ### Cryptographic Token & Secrets Generator
125
+
126
+ Generate production-ready secrets (`SECRET_KEY`, `JWT_SECRET`, `AIC_TOKEN_SECRET`) or individual secure tokens:
127
+
128
+ ```bash
129
+ # Generate complete set of formatted .env secrets
130
+ appmanager generate-tokens --env
131
+
132
+ # Generate a single 256-bit URL-safe token
133
+ appmanager generate-tokens
134
+
135
+ # Generate a 256-bit Hexadecimal token
136
+ appmanager generate-tokens -f hex
137
+ ```
138
+
119
139
  ---
120
140
 
121
141
  ## 🛠️ Sub-App Specification (`manifest.json`)
@@ -185,6 +205,80 @@ if __name__ == "__main__":
185
205
 
186
206
  ---
187
207
 
208
+ ## 🗄️ Per-App Database Access & Permissions
209
+
210
+ An app can request access to the host's shared database by declaring it in its
211
+ manifest. On install, the admin approves or denies the request (default is
212
+ **deny**). Permissions are stored in the `app_db_permissions` table and can be
213
+ adjusted after install from the app's **Permissions** panel.
214
+
215
+ ### Manifest fields
216
+
217
+ ```json
218
+ {
219
+ "requests_database": true,
220
+ "database_access_level": "scoped",
221
+ "database_description": "Stores user preferences in a scoped table.",
222
+ "requests_auth_readonly": true
223
+ }
224
+ ```
225
+
226
+ - `requests_database` — request shared DB access.
227
+ - `database_access_level` — `"scoped"` (own table prefix / MySQL schema) or
228
+ `"full"` (raw host DB, trusted only).
229
+ - `database_description` — shown to the admin at approval time.
230
+ - `requests_auth_readonly` — request read-only access to login state, display
231
+ name, and role only.
232
+
233
+ ### Approval flow
234
+
235
+ 1. The install security report shows a **Permission Requests** panel.
236
+ 2. The admin chooses **Deny**, **Scoped**, or **Full** for DB access, and
237
+ **Deny**/**Grant** for read-only auth access.
238
+ 3. The decision is stored; the app's SDK receives an in-process engine (never
239
+ raw credentials) when granted, or falls back to its own local SQLite when
240
+ denied.
241
+
242
+ ### Adjusting permissions after install
243
+
244
+ From the app's **Permissions** panel, the admin can change the DB scope or
245
+ revoke auth access at any time. If the DB scope changes, the admin is asked
246
+ whether to **migrate** the app's existing data (migration is a distinct,
247
+ confirmable action — never automatic).
248
+
249
+ ### Security model
250
+
251
+ - **Scoped** access hands the app a SQLAlchemy engine with its own table prefix
252
+ (`app_<slug>_`) or dedicated MySQL schema.
253
+ - **Full** access hands a raw engine to the host DB and shows a prominent
254
+ warning at approval.
255
+ - **Auth read-only** exposes only login state, display name, and role — never
256
+ email, user id, passwords, or tokens.
257
+ - Credentials never exist as strings in app memory and are never sent over the
258
+ network or written to files.
259
+
260
+ ### Per-app API keys
261
+
262
+ Each installed app gets a generated API key (stored as a secret config row).
263
+ The SDK sends it in `X-AppManager-App-Key` to authenticate service-to-service
264
+ calls back to the host REST API. The key is injected by the dispatcher, so the
265
+ app never stores or manages credentials itself.
266
+
267
+ ### Audit log
268
+
269
+ Security-relevant actions (install/uninstall, permission grants/revokes, API
270
+ key rotation) are recorded in the `audit_log` table and viewable at
271
+ `/admin/audit-log`.
272
+
273
+ ### Bridge rate limiting
274
+
275
+ In-process telemetry/storage calls (`report_event`, `report_metric`) are
276
+ rate-limited per app via a token bucket (default 100 events/min, configurable
277
+ via `BRIDGE_RATE_LIMIT_RATE` / `BRIDGE_RATE_LIMIT_BURST` /
278
+ `BRIDGE_RATE_LIMIT_ENABLED`).
279
+
280
+ ---
281
+
188
282
  ## 🧩 Pluggable UI Slots & Extension Hooks
189
283
 
190
284
  Extensions can inject components into host slots:
@@ -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.1"
11
+ __version__ = "0.4.1"
12
12
 
13
13
  __all__ = [
14
14
  "AppManager",
@@ -296,6 +296,8 @@ def load_wsgi_app_from_path(app_dir: str, entry_point: str = "app:app") -> Any:
296
296
 
297
297
  mod = importlib.util.module_from_spec(spec)
298
298
  sys.modules[scoped_module_name] = mod
299
+ if app_dir not in sys.path:
300
+ sys.path.insert(0, app_dir)
299
301
  spec.loader.exec_module(mod)
300
302
 
301
303
  # Check for Flask app instance, callable create_app factory, or WSGI middleware
@@ -594,15 +596,53 @@ def stage_zip_file(
594
596
  return staging_id, scan_report, manifest
595
597
 
596
598
 
599
+ def slug_conflict_info(
600
+ slug: str, new_manifest: Optional[Dict[str, Any]] = None
601
+ ) -> Optional[Dict[str, Any]]:
602
+ """
603
+ If an app with ``slug`` is already installed, returns a dict describing the
604
+ conflict (existing app id/name/version vs the incoming manifest's version).
605
+ Returns None when there is no conflict.
606
+ """
607
+ if not slug:
608
+ return None
609
+ existing = InstalledApp.query.filter_by(slug=slug).first()
610
+ if not existing:
611
+ return None
612
+
613
+ existing_version = None
614
+ try:
615
+ from appmanager.admin.registry import manifest_for
616
+
617
+ existing_version = (manifest_for(existing) or {}).get("version")
618
+ except Exception:
619
+ pass
620
+
621
+ new_version = (new_manifest or {}).get("version")
622
+ return {
623
+ "slug_conflict": True,
624
+ "existing_app_id": existing.id,
625
+ "existing_name": existing.name,
626
+ "existing_version": existing_version,
627
+ "new_version": new_version,
628
+ "versions_differ": (existing_version or "") != (new_version or ""),
629
+ }
630
+
631
+
597
632
  def finalize_staged_installation(
598
633
  staging_id: str,
599
634
  name: Optional[str] = None,
600
635
  slug: Optional[str] = None,
601
636
  entry_point: Optional[str] = None,
637
+ replace_existing: bool = False,
602
638
  ) -> InstalledApp:
603
639
  """
604
640
  Finalizes installation of a staged application: moves files into INSTALLED_APPS_DIR,
605
641
  installs requirements, registers in database, configures settings and permissions.
642
+
643
+ If ``replace_existing`` is True and an app with the target slug is already installed,
644
+ its files are atomically replaced (preserving the app record, permissions, and settings)
645
+ instead of raising a conflict error.
606
646
  """
607
647
  session = get_staged_session(staging_id)
608
648
  if not session:
@@ -620,10 +660,53 @@ def finalize_staged_installation(
620
660
  target_dir = os.path.join(current_app.config["INSTALLED_APPS_DIR"], final_slug)
621
661
  logger.info("Finalizing installation of staged app '%s' into '%s'", final_slug, target_dir)
622
662
 
623
- if os.path.exists(target_dir):
624
- if os.path.exists(staging_dir):
625
- shutil.rmtree(staging_dir, ignore_errors=True)
626
- raise ValueError(f"An app with slug '{final_slug}' is already installed.")
663
+ existing_app = InstalledApp.query.filter_by(slug=final_slug).first()
664
+ if os.path.exists(target_dir) or existing_app:
665
+ if not replace_existing:
666
+ if os.path.exists(staging_dir):
667
+ shutil.rmtree(staging_dir, ignore_errors=True)
668
+ raise ValueError(f"An app with slug '{final_slug}' is already installed.")
669
+
670
+ # ---- Replace existing app's files atomically (preserve record/permissions) ----
671
+ if not existing_app:
672
+ raise ValueError(
673
+ f"App directory for slug '{final_slug}' exists but no matching app record was found."
674
+ )
675
+ _replace_staged_files(staging_dir, target_dir)
676
+
677
+ # Refresh manifest-derived metadata on the existing record.
678
+ manifest = parse_manifest(target_dir) or session.get("manifest") or {}
679
+ if manifest.get("name"):
680
+ existing_app.name = manifest["name"]
681
+ if manifest.get("description"):
682
+ existing_app.description = manifest["description"]
683
+ if manifest.get("entry_point"):
684
+ existing_app.entry_point = manifest["entry_point"]
685
+ existing_app.set_seo(manifest.get("seo"))
686
+ db.session.commit()
687
+
688
+ # Clear WSGI cache and trigger reload signals.
689
+ if hasattr(current_app, "extensions") and "appmanager" in current_app.extensions:
690
+ current_app.extensions["appmanager"].clear_cache(slug=existing_app.slug)
691
+ try:
692
+ subapp_reloaded.send(None, slug=existing_app.slug)
693
+ subapp_updated.send(
694
+ None, app_slug=existing_app.slug, app_id=existing_app.id, update_type="install"
695
+ )
696
+ except Exception:
697
+ pass
698
+ try:
699
+ from appmanager.hooks import trigger_hook
700
+
701
+ trigger_hook(
702
+ "on_app_updated",
703
+ app_id=existing_app.id,
704
+ app_slug=existing_app.slug,
705
+ update_type="install",
706
+ )
707
+ except Exception:
708
+ pass
709
+ return existing_app
627
710
 
628
711
  # Remove staging metadata file before moving into installed_apps
629
712
  meta_path = os.path.join(staging_dir, "_staging_meta.json")
@@ -687,13 +770,55 @@ def finalize_staged_installation(
687
770
  if initial_settings:
688
771
  installed_app.set_settings(initial_settings)
689
772
 
773
+ # Persist declarative SEO metadata from the manifest (if any).
774
+ installed_app.set_seo(manifest.get("seo"))
775
+
690
776
  db.session.add(installed_app)
691
777
  db.session.commit()
692
778
 
779
+ # Create default (denied) permission rows for DB / auth-readonly requests.
780
+ try:
781
+ from appmanager.db_access import ensure_permission_rows
782
+
783
+ ensure_permission_rows(installed_app, manifest)
784
+ except Exception as e:
785
+ logger.warning("Failed to seed permission rows for '%s': %s", final_slug, e)
786
+
787
+ # Generate a per-app API key (stored as a secret config row).
788
+ try:
789
+ from appmanager.app_config import generate_app_api_key
790
+
791
+ generate_app_api_key(installed_app.id)
792
+ except Exception as e:
793
+ logger.warning("Failed to generate API key for '%s': %s", final_slug, e)
794
+
795
+ # Record the install in the audit log.
796
+ try:
797
+ from appmanager.audit import log_action
798
+
799
+ log_action(
800
+ "app_install",
801
+ actor_type="admin",
802
+ actor_id=getattr(installed_app, "installed_by", None),
803
+ app_id=installed_app.id,
804
+ details={"slug": final_slug, "source_type": session.get("source_type")},
805
+ )
806
+ except Exception:
807
+ pass
808
+
693
809
  users = User.query.all()
694
- for u in users:
695
- perm = UserAppPermission(user_id=u.id, app_id=installed_app.id, can_access=True)
696
- db.session.add(perm)
810
+ # Bulk-insert a grant-all permission for every user (one statement instead
811
+ # of one INSERT per user). Skip users who already have a row for this app.
812
+ existing_user_ids = {
813
+ p.user_id for p in UserAppPermission.query.filter_by(app_id=installed_app.id).all()
814
+ }
815
+ new_perms = [
816
+ {"user_id": u.id, "app_id": installed_app.id, "can_access": True}
817
+ for u in users
818
+ if u.id not in existing_user_ids
819
+ ]
820
+ if new_perms:
821
+ db.session.bulk_insert_mappings(UserAppPermission, new_perms)
697
822
  db.session.commit()
698
823
 
699
824
  # Sync declared admin panels from the manifest
@@ -881,6 +1006,7 @@ def update_app_from_git(app_id_or_slug: Any) -> Tuple[bool, str, Dict[str, Any]]
881
1006
  app_record.description = manifest["description"]
882
1007
  if manifest.get("entry_point"):
883
1008
  app_record.entry_point = manifest["entry_point"]
1009
+ app_record.set_seo(manifest.get("seo"))
884
1010
 
885
1011
  db.session.commit()
886
1012
 
@@ -913,6 +1039,46 @@ def update_app_from_git(app_id_or_slug: Any) -> Tuple[bool, str, Dict[str, Any]]
913
1039
  )
914
1040
 
915
1041
 
1042
+ def _replace_staged_files(staging_dir: str, target_dir: str) -> None:
1043
+ """
1044
+ Atomically replaces the contents of ``target_dir`` with the staged files in
1045
+ ``staging_dir``. On failure, restores the original target contents.
1046
+ """
1047
+ import tempfile
1048
+
1049
+ temp_backup_dir = None
1050
+ try:
1051
+ if os.path.exists(target_dir):
1052
+ temp_backup_dir = tempfile.mkdtemp(prefix="app_backup_")
1053
+ for item in os.listdir(target_dir):
1054
+ shutil.move(os.path.join(target_dir, item), os.path.join(temp_backup_dir, item))
1055
+
1056
+ # Remove staging metadata before moving files in.
1057
+ meta_path = os.path.join(staging_dir, "_staging_meta.json")
1058
+ if os.path.exists(meta_path):
1059
+ try:
1060
+ os.remove(meta_path)
1061
+ except Exception:
1062
+ pass
1063
+
1064
+ os.makedirs(target_dir, exist_ok=True)
1065
+ for item in os.listdir(staging_dir):
1066
+ shutil.move(os.path.join(staging_dir, item), os.path.join(target_dir, item))
1067
+
1068
+ if os.path.exists(staging_dir):
1069
+ shutil.rmtree(staging_dir, ignore_errors=True)
1070
+ if temp_backup_dir and os.path.exists(temp_backup_dir):
1071
+ shutil.rmtree(temp_backup_dir, ignore_errors=True)
1072
+ except Exception as e:
1073
+ if temp_backup_dir and os.path.exists(temp_backup_dir) and os.path.exists(target_dir):
1074
+ shutil.rmtree(target_dir, ignore_errors=True)
1075
+ os.makedirs(target_dir, exist_ok=True)
1076
+ for item in os.listdir(temp_backup_dir):
1077
+ shutil.move(os.path.join(temp_backup_dir, item), os.path.join(target_dir, item))
1078
+ shutil.rmtree(temp_backup_dir, ignore_errors=True)
1079
+ raise RuntimeError(f"Failed to replace application files: {e}")
1080
+
1081
+
916
1082
  def stage_zip_replacement(
917
1083
  app_id_or_slug: Any,
918
1084
  zip_file_storage: Any,
@@ -1025,6 +1191,7 @@ def finalize_zip_replacement(staging_id: str) -> InstalledApp:
1025
1191
  app_record.description = manifest["description"]
1026
1192
  if manifest.get("entry_point"):
1027
1193
  app_record.entry_point = manifest["entry_point"]
1194
+ app_record.set_seo(manifest.get("seo"))
1028
1195
 
1029
1196
  db.session.commit()
1030
1197
 
@@ -1060,6 +1227,27 @@ def uninstall_app(app_id: int) -> Tuple[bool, str]:
1060
1227
  if hasattr(current_app, "extensions") and "appmanager" in current_app.extensions:
1061
1228
  current_app.extensions["appmanager"].clear_cache(slug=slug)
1062
1229
 
1230
+ # Clean up DB permissions and any scoped tables created for this app.
1231
+ try:
1232
+ from appmanager.db_access import cleanup_app_data
1233
+
1234
+ cleanup_app_data(app_record.id, slug)
1235
+ except Exception as e:
1236
+ logger.warning("Failed to clean up DB data for '%s': %s", slug, e)
1237
+
1238
+ # Record the uninstall in the audit log.
1239
+ try:
1240
+ from appmanager.audit import log_action
1241
+
1242
+ log_action(
1243
+ "app_uninstall",
1244
+ actor_type="admin",
1245
+ app_id=app_id,
1246
+ details={"slug": slug, "name": app_record.name},
1247
+ )
1248
+ except Exception:
1249
+ pass
1250
+
1063
1251
  db.session.delete(app_record)
1064
1252
  db.session.commit()
1065
1253
 
@@ -86,11 +86,14 @@ def sync_panels(app_record, manifest=None):
86
86
  ~AppAdminPanel.panel_id.in_(declared_ids) if declared_ids else True
87
87
  ).delete(synchronize_session=False)
88
88
 
89
+ # Load existing panels for this app once (avoids one query per declared panel).
90
+ existing = {p.panel_id: p for p in AppAdminPanel.query.filter_by(app_id=app_record.id).all()}
91
+
89
92
  for i, p in enumerate(declared):
90
93
  pid = p.get("id")
91
94
  if not pid:
92
95
  continue
93
- row = AppAdminPanel.query.filter_by(app_id=app_record.id, panel_id=pid).first()
96
+ row = existing.get(pid)
94
97
  if not row:
95
98
  row = AppAdminPanel(app_id=app_record.id, panel_id=pid)
96
99
  db.session.add(row)