appmanager-server 0.2.0__tar.gz → 0.2.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 (123) hide show
  1. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/.env.example +10 -1
  2. appmanager_server-0.2.1/CHANGELOG.md +31 -0
  3. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/MANIFEST.in +1 -0
  4. {appmanager_server-0.2.0/appmanager_server.egg-info → appmanager_server-0.2.1}/PKG-INFO +1 -1
  5. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/__init__.py +1 -1
  6. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/auth/routes.py +45 -12
  7. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/config.py +15 -0
  8. {appmanager_server-0.2.0 → appmanager_server-0.2.1/appmanager_server.egg-info}/PKG-INFO +1 -1
  9. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager_server.egg-info/SOURCES.txt +1 -0
  10. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/pyproject.toml +1 -1
  11. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_auth.py +47 -2
  12. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_packaging_and_cli.py +2 -2
  13. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/LICENSE +0 -0
  14. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/README.md +0 -0
  15. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/SECURITY.md +0 -0
  16. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/admin/__init__.py +0 -0
  17. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/admin/app_installer.py +0 -0
  18. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/admin/members.py +0 -0
  19. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/admin/registry.py +0 -0
  20. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/admin/routes.py +0 -0
  21. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/api/__init__.py +0 -0
  22. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/api/routes.py +0 -0
  23. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/app_config.py +0 -0
  24. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/auth/__init__.py +0 -0
  25. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/auth/oauth.py +0 -0
  26. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/auth/utils.py +0 -0
  27. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/bridge.py +0 -0
  28. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/cli.py +0 -0
  29. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/database.py +0 -0
  30. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/extension.py +0 -0
  31. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/extensions.py +0 -0
  32. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/health.py +0 -0
  33. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/hooks.py +0 -0
  34. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/middleware.py +0 -0
  35. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/models.py +0 -0
  36. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/py.typed +0 -0
  37. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/sdk.py +0 -0
  38. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/security.py +0 -0
  39. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/security_scanner.py +0 -0
  40. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/signals.py +0 -0
  41. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/css/app.css +0 -0
  42. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/fonts.css +0 -0
  43. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-0.woff2 +0 -0
  44. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-1.woff2 +0 -0
  45. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-10.woff2 +0 -0
  46. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-11.woff2 +0 -0
  47. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-12.woff2 +0 -0
  48. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-13.woff2 +0 -0
  49. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-14.woff2 +0 -0
  50. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-15.woff2 +0 -0
  51. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-16.woff2 +0 -0
  52. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-17.woff2 +0 -0
  53. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-18.woff2 +0 -0
  54. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-19.woff2 +0 -0
  55. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-2.woff2 +0 -0
  56. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-20.woff2 +0 -0
  57. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-21.woff2 +0 -0
  58. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-22.woff2 +0 -0
  59. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-23.woff2 +0 -0
  60. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-3.woff2 +0 -0
  61. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-4.woff2 +0 -0
  62. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-5.woff2 +0 -0
  63. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-6.woff2 +0 -0
  64. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-7.woff2 +0 -0
  65. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-8.woff2 +0 -0
  66. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/jetbrainsmono-9.woff2 +0 -0
  67. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-0.woff2 +0 -0
  68. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-1.woff2 +0 -0
  69. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-10.woff2 +0 -0
  70. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-11.woff2 +0 -0
  71. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-2.woff2 +0 -0
  72. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-3.woff2 +0 -0
  73. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-4.woff2 +0 -0
  74. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-5.woff2 +0 -0
  75. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-6.woff2 +0 -0
  76. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-7.woff2 +0 -0
  77. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-8.woff2 +0 -0
  78. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/fonts/spacegrotesk-9.woff2 +0 -0
  79. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/static/vendor/htmx/htmx.min.js +0 -0
  80. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/_member_drawer.html +0 -0
  81. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/_members_table.html +0 -0
  82. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/app_detail.html +0 -0
  83. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/dashboard.html +0 -0
  84. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/flairs.html +0 -0
  85. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/permissions.html +0 -0
  86. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/roles.html +0 -0
  87. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/admin/users.html +0 -0
  88. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/auth/login.html +0 -0
  89. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/auth/profile.html +0 -0
  90. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/base.html +0 -0
  91. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/errors/401.html +0 -0
  92. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/errors/403.html +0 -0
  93. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/errors/404.html +0 -0
  94. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/errors/500.html +0 -0
  95. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager/templates/index.html +0 -0
  96. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager_server.egg-info/dependency_links.txt +0 -0
  97. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager_server.egg-info/entry_points.txt +0 -0
  98. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager_server.egg-info/requires.txt +0 -0
  99. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/appmanager_server.egg-info/top_level.txt +0 -0
  100. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/requirements.txt +0 -0
  101. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/setup.cfg +0 -0
  102. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_api.py +0 -0
  103. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_app_settings.py +0 -0
  104. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_app_settings_config.py +0 -0
  105. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_cli_dev_and_templates.py +0 -0
  106. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_cli_health.py +0 -0
  107. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_cli_scaffolding.py +0 -0
  108. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_error_pages.py +0 -0
  109. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_extension_apps.py +0 -0
  110. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_extension_pattern.py +0 -0
  111. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_hooks.py +0 -0
  112. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_installer.py +0 -0
  113. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_invalid_install_failures.py +0 -0
  114. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_manifest_sdk.py +0 -0
  115. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_middleware.py +0 -0
  116. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_role_management.py +0 -0
  117. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_sdk.py +0 -0
  118. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_security_hardening.py +0 -0
  119. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_security_precheck_flow.py +0 -0
  120. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_security_scanner.py +0 -0
  121. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_signals.py +0 -0
  122. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_user_elevation_and_visibility.py +0 -0
  123. {appmanager_server-0.2.0 → appmanager_server-0.2.1}/tests/test_user_tracking.py +0 -0
@@ -34,9 +34,18 @@ GOOGLE_CLIENT_ID=
34
34
  GOOGLE_CLIENT_SECRET=
35
35
  GOOGLE_DISCOVERY_URL=https://accounts.google.com/.well-known/openid-configuration
36
36
 
37
- # SMTP Email Configuration (Optional - logs to terminal console if left blank)
37
+ # SMTP Email Configuration (Required for Magic Link email delivery in production)
38
38
  SMTP_SERVER=
39
39
  SMTP_PORT=587
40
40
  SMTP_USER=
41
41
  SMTP_PASSWORD=
42
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=
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to **`appmanager-server`** will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [0.2.1] - 2026-08-28
11
+
12
+ ### Added
13
+ - **Configurable Dev Magic Login (`ALLOW_DEV_MAGIC_LOGIN`)**: Prevents showing one-click developer login buttons in production when SMTP is unconfigured unless explicitly permitted.
14
+ - **Configurable Admin Role Provisioning (`ADMIN_EMAILS` & `FIRST_USER_IS_ADMIN`)**:
15
+ - `ADMIN_EMAILS`: Specify comma-separated list of admin email addresses.
16
+ - `FIRST_USER_IS_ADMIN`: Controls whether the very first registered user in a blank database receives the admin role (defaults to `true`).
17
+
18
+ ### Changed
19
+ - Improved error messaging on `/auth/login` when SMTP is unconfigured.
20
+ - Expanded authentication test suite.
21
+
22
+ ---
23
+
24
+ ## [0.2.0] - 2026-08-28
25
+
26
+ ### Added
27
+ - Initial PyPI release under package name **`appmanager-server`**.
28
+ - CLI commands and aliases: `appmanager-server`, `appmgr-server`, `appmanager`, and `appmgr`.
29
+ - AST Security Scanner (`appmanager.security_scanner.SecurityScanner`) for ZIP and Git package pre-installation checks.
30
+ - Python 3.10-3.13 support.
31
+ - PyPI Trusted Publishing via GitHub Actions OIDC.
@@ -1,5 +1,6 @@
1
1
  include LICENSE
2
2
  include README.md
3
+ include CHANGELOG.md
3
4
  include SECURITY.md
4
5
  include requirements.txt
5
6
  include .env.example
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmanager-server
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Multi-tenant Flask sub-application portal & dynamic WSGI dispatcher with in-process isolation, telemetry, and automated health checks.
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.2.0"
11
+ __version__ = "0.2.1"
12
12
 
13
13
  __all__ = [
14
14
  "AppManager",
@@ -39,6 +39,25 @@ def login():
39
39
  return render_template("auth/login.html", next=next_url, google_configured=google_configured)
40
40
 
41
41
 
42
+ def resolve_initial_user_role(email: str) -> str:
43
+ """
44
+ Resolves the initial role ('admin' or 'user') for a newly created user:
45
+ 1. Checks if email is in ADMIN_EMAILS.
46
+ 2. If FIRST_USER_IS_ADMIN is True and no users exist in the database yet, returns 'admin'.
47
+ 3. Defaults to 'user'.
48
+ """
49
+ clean_email = email.strip().lower()
50
+ admin_emails = current_app.config.get("ADMIN_EMAILS", [])
51
+ if clean_email in [e.strip().lower() for e in admin_emails if e.strip()]:
52
+ return "admin"
53
+
54
+ if current_app.config.get("FIRST_USER_IS_ADMIN", True):
55
+ if User.query.count() == 0:
56
+ return "admin"
57
+
58
+ return "user"
59
+
60
+
42
61
  @auth_bp.route("/magic-link", methods=["POST"])
43
62
  def request_magic_link():
44
63
  client_ip = request.remote_addr or "unknown"
@@ -55,11 +74,23 @@ def request_magic_link():
55
74
  flash("Please enter a valid email address.", "warning")
56
75
  return redirect(url_for("auth.login", next=next_url))
57
76
 
77
+ smtp_configured = bool(current_app.config.get("SMTP_SERVER"))
78
+ allow_dev_magic = current_app.config.get("ALLOW_DEV_MAGIC_LOGIN", False)
79
+
80
+ # If SMTP is not configured and developer preview is disabled, block and show error
81
+ if not smtp_configured and not allow_dev_magic:
82
+ flash(
83
+ "Email delivery (SMTP) is not configured on this server. "
84
+ "Please configure SMTP settings in .env (or enable ALLOW_DEV_MAGIC_LOGIN=true for local testing).",
85
+ "danger",
86
+ )
87
+ return redirect(url_for("auth.login", next=next_url))
88
+
58
89
  magic_url = create_magic_link(email)
59
90
 
60
- # In dev mode without SMTP, pass the magic link URL to the template for quick developer login
91
+ # In dev mode with ALLOW_DEV_MAGIC_LOGIN=True, pass the magic link URL to the template for quick login
61
92
  dev_magic_url = None
62
- if not current_app.config.get("SMTP_SERVER"):
93
+ if not smtp_configured and allow_dev_magic:
63
94
  dev_magic_url = magic_url
64
95
 
65
96
  return render_template(
@@ -99,9 +130,7 @@ def verify_magic():
99
130
  # Find or create user
100
131
  user = User.query.filter_by(email=magic_token.email).first()
101
132
  if not user:
102
- # First user created in the system becomes Admin automatically
103
- user_count = User.query.count()
104
- user_role = "admin" if user_count == 0 else "user"
133
+ user_role = resolve_initial_user_role(magic_token.email)
105
134
  user = User(
106
135
  email=magic_token.email,
107
136
  name=magic_token.email.split("@")[0].capitalize(),
@@ -128,10 +157,7 @@ def verify_magic():
128
157
  jwt_token = generate_jwt(user)
129
158
  flash(f"Welcome back, {user.name}!", "success")
130
159
 
131
- next_url = request.args.get("next", "")
132
- target_redirect = next_url if is_safe_redirect_url(next_url) else url_for("auth.profile")
133
-
134
- response = make_response(redirect(target_redirect))
160
+ response = make_response(redirect(url_for("auth.profile")))
135
161
 
136
162
  response.set_cookie(
137
163
  JWT_COOKIE_NAME,
@@ -148,7 +174,7 @@ def verify_magic():
148
174
  def google_login():
149
175
  auth_url, error = get_google_auth_url()
150
176
  if error:
151
- flash(f"Google OAuth Error: {error}", "danger")
177
+ flash(f"Google Login Error: {error}", "danger")
152
178
  return redirect(url_for("auth.login"))
153
179
  return redirect(auth_url)
154
180
 
@@ -170,11 +196,18 @@ def google_callback():
170
196
 
171
197
  user = User.query.filter((User.google_id == google_id) | (User.email == email)).first()
172
198
  if not user:
173
- user_count = User.query.count()
174
- user_role = "admin" if user_count == 0 else "user"
199
+ user_role = resolve_initial_user_role(email)
175
200
  user = User(email=email, name=name, google_id=google_id, role=user_role)
176
201
  db.session.add(user)
177
202
  db.session.commit()
203
+
204
+ # If non-admin user, assign default permissions for active apps
205
+ if user_role != "admin":
206
+ active_apps = InstalledApp.query.filter_by(is_active=True).all()
207
+ for app in active_apps:
208
+ perm = UserAppPermission(user_id=user.id, app_id=app.id, can_access=True)
209
+ db.session.add(perm)
210
+ db.session.commit()
178
211
  else:
179
212
  if not user.google_id:
180
213
  user.google_id = google_id
@@ -48,6 +48,21 @@ class Config:
48
48
  MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER", "noreply@appmanager.local")
49
49
  APP_BASE_URL = os.getenv("APP_BASE_URL", "http://localhost:5000")
50
50
 
51
+ # Developer & Admin Provisioning Settings
52
+ ALLOW_DEV_MAGIC_LOGIN = os.getenv("ALLOW_DEV_MAGIC_LOGIN", "false").lower() in (
53
+ "true",
54
+ "1",
55
+ "yes",
56
+ )
57
+ FIRST_USER_IS_ADMIN = os.getenv("FIRST_USER_IS_ADMIN", "true").lower() in (
58
+ "true",
59
+ "1",
60
+ "yes",
61
+ )
62
+ ADMIN_EMAILS = [
63
+ e.strip().lower() for e in os.getenv("ADMIN_EMAILS", "").split(",") if e.strip()
64
+ ]
65
+
51
66
  # Security settings
52
67
  SESSION_COOKIE_HTTPONLY = True
53
68
  SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE", "Lax")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmanager-server
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Multi-tenant Flask sub-application portal & dynamic WSGI dispatcher with in-process isolation, telemetry, and automated health checks.
5
5
  Author: AppManager Contributors
6
6
  License: MIT
@@ -1,4 +1,5 @@
1
1
  .env.example
2
+ CHANGELOG.md
2
3
  LICENSE
3
4
  MANIFEST.in
4
5
  README.md
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "appmanager-server"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Multi-tenant Flask sub-application portal & dynamic WSGI dispatcher with in-process isolation, telemetry, and automated health checks."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -17,6 +17,7 @@ def app():
17
17
  "INSTALLED_APPS_DIR": "/tmp/test_installed_apps",
18
18
  "TEMP_UPLOAD_DIR": "/tmp/test_uploads",
19
19
  "BASE_DIR": "/tmp",
20
+ "ALLOW_DEV_MAGIC_LOGIN": True, # Default to True in base test fixture
20
21
  }
21
22
  )
22
23
  with app.app_context():
@@ -31,10 +32,11 @@ def client(app):
31
32
  return app.test_client()
32
33
 
33
34
 
34
- def test_magic_link_flow(client, app):
35
- # 1. Request magic link
35
+ def test_magic_link_flow_dev_mode(client, app):
36
+ # 1. Request magic link with ALLOW_DEV_MAGIC_LOGIN=True
36
37
  res = client.post("/auth/magic-link", data={"email": "test@example.com"})
37
38
  assert res.status_code == 200
39
+ assert b"Developer Mode Preview" in res.data
38
40
 
39
41
  # 2. Check token in DB
40
42
  with app.app_context():
@@ -54,6 +56,49 @@ def test_magic_link_flow(client, app):
54
56
  assert user.role == "admin"
55
57
 
56
58
 
59
+ def test_magic_link_fails_when_smtp_and_dev_mode_disabled(app):
60
+ app.config["ALLOW_DEV_MAGIC_LOGIN"] = False
61
+ app.config["SMTP_SERVER"] = ""
62
+ client = app.test_client()
63
+
64
+ res = client.post("/auth/magic-link", data={"email": "user@example.com"}, follow_redirects=True)
65
+ assert res.status_code == 200
66
+ assert b"Email delivery (SMTP) is not configured" in res.data
67
+
68
+ with app.app_context():
69
+ token = MagicLinkToken.query.filter_by(email="user@example.com").first()
70
+ assert token is None
71
+
72
+
73
+ def test_admin_emails_config_provisioning(client, app):
74
+ app.config["ADMIN_EMAILS"] = ["designated-admin@example.com"]
75
+ app.config["FIRST_USER_IS_ADMIN"] = False
76
+
77
+ # First user who is NOT in ADMIN_EMAILS
78
+ client.post("/auth/magic-link", data={"email": "regular@example.com"})
79
+ with app.app_context():
80
+ t1 = MagicLinkToken.query.filter_by(email="regular@example.com").first()
81
+ token1 = t1.token
82
+
83
+ client.get(f"/auth/verify-magic?token={token1}")
84
+ with app.app_context():
85
+ u1 = User.query.filter_by(email="regular@example.com").first()
86
+ assert u1 is not None
87
+ assert u1.role == "user"
88
+
89
+ # Second user who IS in ADMIN_EMAILS
90
+ client.post("/auth/magic-link", data={"email": "designated-admin@example.com"})
91
+ with app.app_context():
92
+ t2 = MagicLinkToken.query.filter_by(email="designated-admin@example.com").first()
93
+ token2 = t2.token
94
+
95
+ client.get(f"/auth/verify-magic?token={token2}")
96
+ with app.app_context():
97
+ u2 = User.query.filter_by(email="designated-admin@example.com").first()
98
+ assert u2 is not None
99
+ assert u2.role == "admin"
100
+
101
+
57
102
  def test_second_user_is_regular_user(client, app):
58
103
  # Create first user
59
104
  with app.app_context():
@@ -12,7 +12,7 @@ from appmanager.cli import main
12
12
 
13
13
 
14
14
  def test_package_exports():
15
- assert __version__ == "0.2.0"
15
+ assert __version__ == "0.2.1"
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.2.0" in captured.out or "0.2.0" in captured.err
28
+ assert "0.2.1" in captured.out or "0.2.1" in captured.err
29
29
 
30
30
 
31
31
  def test_cli_help(capsys):