appmanager-server 0.4.0__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.
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/CHANGELOG.md +47 -0
- {appmanager_server-0.4.0/appmanager_server.egg-info → appmanager_server-0.4.1}/PKG-INFO +95 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/README.md +94 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/__init__.py +1 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/admin/app_installer.py +193 -7
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/admin/registry.py +4 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/admin/routes.py +265 -21
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/api/routes.py +14 -2
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/app_config.py +57 -0
- appmanager_server-0.4.1/appmanager/audit.py +65 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/auth/routes.py +17 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/bridge.py +40 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/cli.py +73 -10
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/config.py +1 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/database.py +95 -0
- appmanager_server-0.4.1/appmanager/db_access.py +378 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/extension.py +114 -5
- appmanager_server-0.4.1/appmanager/host_settings.py +172 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/middleware.py +163 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/models.py +205 -0
- appmanager_server-0.4.1/appmanager/ratelimit.py +68 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/security.py +71 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/css/app.css +227 -36
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/admin/app_detail.html +142 -0
- appmanager_server-0.4.1/appmanager/templates/admin/dashboard.html +1753 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/auth/login.html +9 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/base.html +17 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/index.html +8 -1
- appmanager_server-0.4.1/appmanager/templates/privacy.html +23 -0
- appmanager_server-0.4.1/appmanager/templates/terms.html +26 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1/appmanager_server.egg-info}/PKG-INFO +95 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager_server.egg-info/SOURCES.txt +8 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/pyproject.toml +1 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_auth.py +25 -5
- appmanager_server-0.4.1/tests/test_db_access.py +236 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_invalid_install_failures.py +127 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_packaging_and_cli.py +26 -1
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_security_hardening.py +20 -0
- appmanager_server-0.4.1/tests/test_seo_and_settings.py +333 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_user_elevation_and_visibility.py +20 -3
- appmanager_server-0.4.0/appmanager/templates/admin/dashboard.html +0 -1030
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/LICENSE +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/MANIFEST.in +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/SECURITY.md +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/admin/__init__.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/admin/members.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/api/__init__.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/auth/__init__.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/auth/oauth.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/auth/utils.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/dependency_manager.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/extensions.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/health.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/hooks.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/py.typed +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/sdk.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/security_scanner.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/signals.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/fonts.css +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-0.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-1.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-10.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-11.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-12.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-13.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-14.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-15.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-16.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-17.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-18.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-19.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-2.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-20.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-21.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-22.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-23.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-3.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-4.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-5.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-6.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-7.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-8.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/jetbrainsmono-9.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-0.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-1.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-10.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-11.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-2.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-3.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-4.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-5.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-6.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-7.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-8.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/fonts/spacegrotesk-9.woff2 +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/static/vendor/htmx/htmx.min.js +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/admin/_member_drawer.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/admin/_members_table.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/admin/flairs.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/admin/permissions.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/admin/roles.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/admin/users.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/auth/profile.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/errors/401.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/errors/403.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/errors/404.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager/templates/errors/500.html +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager_server.egg-info/dependency_links.txt +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager_server.egg-info/entry_points.txt +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager_server.egg-info/requires.txt +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/appmanager_server.egg-info/top_level.txt +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/requirements.txt +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/setup.cfg +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_api.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_app_settings.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_app_settings_config.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_app_updates.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_cli_dev_and_templates.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_cli_health.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_cli_scaffolding.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_dependency_manager.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_error_pages.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_extension_apps.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_extension_pattern.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_hooks.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_installer.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_manifest_sdk.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_middleware.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_nested_portal.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_role_management.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_sdk.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_security_precheck_flow.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_security_scanner.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_signals.py +0 -0
- {appmanager_server-0.4.0 → appmanager_server-0.4.1}/tests/test_user_tracking.py +0 -0
|
@@ -7,6 +7,53 @@ 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
|
+
|
|
10
57
|
## [0.4.0] - 2026-08-28
|
|
11
58
|
|
|
12
59
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: appmanager-server
|
|
3
|
-
Version: 0.4.
|
|
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:
|
|
@@ -596,15 +596,53 @@ def stage_zip_file(
|
|
|
596
596
|
return staging_id, scan_report, manifest
|
|
597
597
|
|
|
598
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
|
+
|
|
599
632
|
def finalize_staged_installation(
|
|
600
633
|
staging_id: str,
|
|
601
634
|
name: Optional[str] = None,
|
|
602
635
|
slug: Optional[str] = None,
|
|
603
636
|
entry_point: Optional[str] = None,
|
|
637
|
+
replace_existing: bool = False,
|
|
604
638
|
) -> InstalledApp:
|
|
605
639
|
"""
|
|
606
640
|
Finalizes installation of a staged application: moves files into INSTALLED_APPS_DIR,
|
|
607
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.
|
|
608
646
|
"""
|
|
609
647
|
session = get_staged_session(staging_id)
|
|
610
648
|
if not session:
|
|
@@ -622,10 +660,53 @@ def finalize_staged_installation(
|
|
|
622
660
|
target_dir = os.path.join(current_app.config["INSTALLED_APPS_DIR"], final_slug)
|
|
623
661
|
logger.info("Finalizing installation of staged app '%s' into '%s'", final_slug, target_dir)
|
|
624
662
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
|
629
710
|
|
|
630
711
|
# Remove staging metadata file before moving into installed_apps
|
|
631
712
|
meta_path = os.path.join(staging_dir, "_staging_meta.json")
|
|
@@ -689,13 +770,55 @@ def finalize_staged_installation(
|
|
|
689
770
|
if initial_settings:
|
|
690
771
|
installed_app.set_settings(initial_settings)
|
|
691
772
|
|
|
773
|
+
# Persist declarative SEO metadata from the manifest (if any).
|
|
774
|
+
installed_app.set_seo(manifest.get("seo"))
|
|
775
|
+
|
|
692
776
|
db.session.add(installed_app)
|
|
693
777
|
db.session.commit()
|
|
694
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
|
+
|
|
695
809
|
users = User.query.all()
|
|
696
|
-
for
|
|
697
|
-
|
|
698
|
-
|
|
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)
|
|
699
822
|
db.session.commit()
|
|
700
823
|
|
|
701
824
|
# Sync declared admin panels from the manifest
|
|
@@ -883,6 +1006,7 @@ def update_app_from_git(app_id_or_slug: Any) -> Tuple[bool, str, Dict[str, Any]]
|
|
|
883
1006
|
app_record.description = manifest["description"]
|
|
884
1007
|
if manifest.get("entry_point"):
|
|
885
1008
|
app_record.entry_point = manifest["entry_point"]
|
|
1009
|
+
app_record.set_seo(manifest.get("seo"))
|
|
886
1010
|
|
|
887
1011
|
db.session.commit()
|
|
888
1012
|
|
|
@@ -915,6 +1039,46 @@ def update_app_from_git(app_id_or_slug: Any) -> Tuple[bool, str, Dict[str, Any]]
|
|
|
915
1039
|
)
|
|
916
1040
|
|
|
917
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
|
+
|
|
918
1082
|
def stage_zip_replacement(
|
|
919
1083
|
app_id_or_slug: Any,
|
|
920
1084
|
zip_file_storage: Any,
|
|
@@ -1027,6 +1191,7 @@ def finalize_zip_replacement(staging_id: str) -> InstalledApp:
|
|
|
1027
1191
|
app_record.description = manifest["description"]
|
|
1028
1192
|
if manifest.get("entry_point"):
|
|
1029
1193
|
app_record.entry_point = manifest["entry_point"]
|
|
1194
|
+
app_record.set_seo(manifest.get("seo"))
|
|
1030
1195
|
|
|
1031
1196
|
db.session.commit()
|
|
1032
1197
|
|
|
@@ -1062,6 +1227,27 @@ def uninstall_app(app_id: int) -> Tuple[bool, str]:
|
|
|
1062
1227
|
if hasattr(current_app, "extensions") and "appmanager" in current_app.extensions:
|
|
1063
1228
|
current_app.extensions["appmanager"].clear_cache(slug=slug)
|
|
1064
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
|
+
|
|
1065
1251
|
db.session.delete(app_record)
|
|
1066
1252
|
db.session.commit()
|
|
1067
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 =
|
|
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)
|