simple-module-users 0.0.37__tar.gz → 0.0.38__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.
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/.gitignore +1 -2
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/PKG-INFO +10 -10
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/README.md +3 -3
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/package.json +1 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/pyproject.toml +10 -7
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/_middleware_support.py +6 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/_users_app_builders.py +53 -7
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/conftest.py +8 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_admin_resend_invite.py +7 -5
- simple_module_users-0.0.38/tests/test_api_admin_pagination_bounds.py +31 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_auth_screens_props.py +1 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_bearer_token_expiry.py +7 -3
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_bootstrap.py +31 -3
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_bulk_invite_message.py +10 -8
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_demo_account.py +11 -15
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_edit_recent_activity.py +12 -10
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_invite_flow.py +27 -26
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_invite_reuse.py +18 -18
- simple_module_users-0.0.38/tests/test_mailer.py +69 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_profile_password.py +2 -2
- simple_module_users-0.0.38/tests/test_rate_limit.py +28 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_session_revocation.py +1 -1
- simple_module_users-0.0.38/tests/test_session_version_atomic.py +35 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_settings.py +14 -43
- simple_module_users-0.0.38/tests/test_users_admin_session_revocation.py +106 -0
- simple_module_users-0.0.38/tests/test_users_admin_unlock.py +100 -0
- simple_module_users-0.0.38/tests/test_users_directory.py +131 -0
- simple_module_users-0.0.38/tests/test_users_lockout_columns.py +31 -0
- simple_module_users-0.0.38/tests/test_users_lockout_enumeration.py +125 -0
- simple_module_users-0.0.38/tests/test_users_lockout_service.py +202 -0
- simple_module_users-0.0.38/tests/test_users_login_lockout.py +149 -0
- simple_module_users-0.0.38/tests/test_users_mailer_core_sender.py +100 -0
- simple_module_users-0.0.38/tests/test_users_password_change_keeps_api.py +98 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_provider.py +10 -5
- simple_module_users-0.0.38/tests/test_users_reset_link_origin.py +48 -0
- simple_module_users-0.0.38/tests-js/AccountCardLocked.test.tsx +50 -0
- simple_module_users-0.0.38/tests-js/users_Login.test.tsx +94 -0
- simple_module_users-0.0.38/tests-js/users_actions.test.tsx +88 -0
- simple_module_users-0.0.38/tests-js/users_fixtures.ts +23 -0
- simple_module_users-0.0.38/tests-js/users_profile_cards.test.tsx +56 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/api.py +20 -6
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/bulk_invite.py +4 -10
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/StatusPill.tsx +18 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/UserCards.tsx +1 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/UserRow.tsx +2 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/useUserRowActions.ts +26 -14
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/user-list-item.ts +2 -0
- simple_module_users-0.0.38/users/admin/links.py +38 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/queries.py +51 -24
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/recent_activity.py +28 -59
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/service.py +27 -7
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/views.py +15 -14
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/audit.py +12 -32
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/api.py +31 -41
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/AuthIntro.tsx +2 -2
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/PasswordCard.tsx +19 -10
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/ProfileDetailsCard.tsx +11 -9
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/ResendVerification.tsx +3 -6
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/SessionsCard.tsx +26 -13
- simple_module_users-0.0.38/users/auth_local/lockout.py +289 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/rate_limit.py +6 -34
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/self_account.py +49 -22
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/token_api.py +27 -10
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/backend.py +30 -5
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/bootstrap.py +13 -6
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/constants.py +13 -0
- simple_module_users-0.0.38/users/contracts/roles.py +11 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/contracts/schemas.py +3 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/demo.py +3 -3
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/deps.py +2 -1
- simple_module_users-0.0.38/users/directory.py +118 -0
- simple_module_users-0.0.38/users/locales/de.json +419 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/locales/en.json +11 -5
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/mailer/__init__.py +4 -19
- simple_module_users-0.0.38/users/mailer/templated.py +77 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/manager.py +13 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/user.py +15 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/module.py +22 -15
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/AcceptInvite.tsx +16 -18
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/ForgotPassword.tsx +7 -11
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Login.tsx +36 -49
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Profile.tsx +2 -4
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Register.tsx +9 -6
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/ResetPassword.tsx +19 -32
- simple_module_users-0.0.38/users/pages/Users/AddPeople.tsx +136 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/Edit.tsx +18 -13
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/Index.tsx +9 -10
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/AccountCard.tsx +24 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/DangerZone.tsx +22 -19
- simple_module_users-0.0.38/users/pages/Users/components/MailerBanner.tsx +37 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/RecentActivityCard.tsx +3 -3
- simple_module_users-0.0.38/users/pages/Users/components/useAddPeople.ts +157 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/useEditUserForm.ts +22 -15
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/useUserActions.ts +49 -14
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/VerifyEmail.tsx +11 -22
- simple_module_users-0.0.38/users/pages/api.generated.ts +1923 -0
- simple_module_users-0.0.38/users/pages/api.ts +9 -0
- simple_module_users-0.0.38/users/pages/failureMessage.ts +16 -0
- simple_module_users-0.0.38/users/pages/signIn.ts +25 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/provider.py +2 -17
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/registration.py +1 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/session_revocation.py +22 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/settings.py +31 -16
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/setup_action.py +1 -1
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/startup.py +6 -37
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/state.py +1 -2
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/token_strategy.py +5 -5
- simple_module_users-0.0.37/tests/test_mailer.py +0 -130
- simple_module_users-0.0.37/tests/test_rate_limit.py +0 -132
- simple_module_users-0.0.37/users/health.py +0 -47
- simple_module_users-0.0.37/users/mailer/console.py +0 -61
- simple_module_users-0.0.37/users/mailer/smtp.py +0 -123
- simple_module_users-0.0.37/users/pages/Users/AddPeople.tsx +0 -298
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/LICENSE +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/.gitkeep +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/_demo_support.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/_users_oauth_support.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_access_token_model.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_activity_summary.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_api_admin.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_api_admin_crud.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_api_admin_filters.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_api_auth.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_backend.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_bearer_token_age.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_bootstrap_resolution.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_cli.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_constants.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_db_adapter.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_demo_guard.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_demo_login.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_external_users.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_legacy_cached_tenant.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_legacy_session_expiry.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_login_redirect_root_mount.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_negative_authz.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_oauth.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_oauth_routes.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_remember_me.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_revocation_broadcast.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_role_model.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_service_admin.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_service_admin_crud.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_session_expiry.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_session_version_cache.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_session_version_failopen.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_session_version_hardening.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_sessions_revoke.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_token_api.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_user_manager.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_user_model.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_user_role_model.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_user_service.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_user_state.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_bearer_scheme.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_bulk_invite.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_default_role.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_deps.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_list_invites.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_login_deep_link.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_middleware.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_middleware_public_paths.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_middleware_resolvers.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_oauth_signup_gate.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_oauth_unverified_link.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_register_role_durable.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_require_verification.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_seed_role_migration.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_setup_lock.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_setup_wizard.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_users_shared_props.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_views.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_views_admin.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests-js/EmailChipInput.test.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests-js/LoginForm.test.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests-js/deriveState.test.ts +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests-js/invite-emails.test.ts +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests-js/registerErrors.test.ts +0 -0
- /simple_module_users-0.0.37/tests-js/Register.test.tsx → /simple_module_users-0.0.38/tests-js/users_Register.test.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/tsconfig.json +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/__init__.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/__init__.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/IndexFilters.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/RolesTab.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/UserRowMenu.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/UsersEmpty.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/components/UsersTable.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/resend_invite.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/admin/user_state.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/__init__.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/AuthField.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/AuthStateCard.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/DemoSignIn.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/InviteSummary.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/LoginForm.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/PasswordFields.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/PreferencesCard.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/ResetLinkSent.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/WaitingOnYou.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/components/profile-user.ts +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/demo_api.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/invite_preview.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/registerErrors.ts +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/token_preview.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/auth_local/views.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/cli.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/contracts/__init__.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/contracts/events.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/db_adapter.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/demo_guard.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/exceptions.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/mailer/templates/.gitkeep +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/mailer/templates/invite.txt +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/mailer/templates/reset_password.txt +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/mailer/templates/verify_email.txt +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/middleware.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/__init__.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/_base.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/access_token.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/oauth_account.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/refresh_token.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/role.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/models/user_role.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/oauth/__init__.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/oauth/api.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/oauth/providers.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/.gitkeep +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/CreateUserFields.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/DetailsCard.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/EditHeaderActions.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/EmailChipInput.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/InviteFields.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/InviteResults.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/RolePicker.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/components/UserStats.tsx +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/pages/Users/invite-emails.ts +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/py.typed +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/roles_cache.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/session_version_cache.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/setup.py +0 -0
- {simple_module_users-0.0.37 → simple_module_users-0.0.38}/users/shared_props.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_users
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.38
|
|
4
4
|
Summary: Email + password user management, admin invites, RBAC-ready — replaces Keycloak for simple_module apps
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -21,14 +21,14 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
|
-
Requires-Dist: aiosmtplib>=3.0
|
|
25
24
|
Requires-Dist: cachetools>=5.3
|
|
26
25
|
Requires-Dist: fastapi-users[oauth,sqlalchemy]<16,>=15
|
|
27
|
-
Requires-Dist: simple-module-auth==0.0.
|
|
28
|
-
Requires-Dist: simple-module-core==0.0.
|
|
29
|
-
Requires-Dist: simple-module-db==0.0.
|
|
30
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
31
|
-
Requires-Dist: simple-module-
|
|
26
|
+
Requires-Dist: simple-module-auth==0.0.38
|
|
27
|
+
Requires-Dist: simple-module-core==0.0.38
|
|
28
|
+
Requires-Dist: simple-module-db==0.0.38
|
|
29
|
+
Requires-Dist: simple-module-hosting==0.0.38
|
|
30
|
+
Requires-Dist: simple-module-mail==0.0.38
|
|
31
|
+
Requires-Dist: simple-module-settings==0.0.38
|
|
32
32
|
Requires-Dist: typer>=0.12
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
@@ -52,7 +52,7 @@ Pre-wired into any app scaffolded with `smpy new`.
|
|
|
52
52
|
- Bootstrap admin via env vars (`SM_USERS_BOOTSTRAP_EMAIL` + `SM_USERS_BOOTSTRAP_PASSWORD`) — idempotent, only creates if the users table is empty.
|
|
53
53
|
- `smpy users create-admin` CLI for ad-hoc admin creation.
|
|
54
54
|
- Inertia pages for login/register/invite-accept/admin-invite.
|
|
55
|
-
-
|
|
55
|
+
- Sends verification, reset and invite emails through the framework mail sender (configure the transport in the `mail` module).
|
|
56
56
|
- Demo mode — one-click, read-only shared **admin** and **standard user** accounts on the sign-in card, for public showcase instances.
|
|
57
57
|
|
|
58
58
|
## Usage
|
|
@@ -73,7 +73,7 @@ SM_USERS_BOOTSTRAP_PASSWORD=change-me
|
|
|
73
73
|
Program:
|
|
74
74
|
|
|
75
75
|
```python
|
|
76
|
-
from auth.deps import CurrentUser
|
|
76
|
+
from auth.deps import CurrentUser
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
@router.get("/profile")
|
|
@@ -84,7 +84,7 @@ async def profile(user: CurrentUser):
|
|
|
84
84
|
## Depends on
|
|
85
85
|
|
|
86
86
|
- `simple_module_core`, `simple_module_db`, `simple_module_hosting`, `simple_module_settings`, `simple_module_auth`
|
|
87
|
-
- `fastapi-users[sqlalchemy,oauth]>=15,<16`, `
|
|
87
|
+
- `fastapi-users[sqlalchemy,oauth]>=15,<16`, `cachetools`, `typer`
|
|
88
88
|
|
|
89
89
|
## Demo mode
|
|
90
90
|
|
|
@@ -18,7 +18,7 @@ Pre-wired into any app scaffolded with `smpy new`.
|
|
|
18
18
|
- Bootstrap admin via env vars (`SM_USERS_BOOTSTRAP_EMAIL` + `SM_USERS_BOOTSTRAP_PASSWORD`) — idempotent, only creates if the users table is empty.
|
|
19
19
|
- `smpy users create-admin` CLI for ad-hoc admin creation.
|
|
20
20
|
- Inertia pages for login/register/invite-accept/admin-invite.
|
|
21
|
-
-
|
|
21
|
+
- Sends verification, reset and invite emails through the framework mail sender (configure the transport in the `mail` module).
|
|
22
22
|
- Demo mode — one-click, read-only shared **admin** and **standard user** accounts on the sign-in card, for public showcase instances.
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
@@ -39,7 +39,7 @@ SM_USERS_BOOTSTRAP_PASSWORD=change-me
|
|
|
39
39
|
Program:
|
|
40
40
|
|
|
41
41
|
```python
|
|
42
|
-
from auth.deps import CurrentUser
|
|
42
|
+
from auth.deps import CurrentUser
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
@router.get("/profile")
|
|
@@ -50,7 +50,7 @@ async def profile(user: CurrentUser):
|
|
|
50
50
|
## Depends on
|
|
51
51
|
|
|
52
52
|
- `simple_module_core`, `simple_module_db`, `simple_module_hosting`, `simple_module_settings`, `simple_module_auth`
|
|
53
|
-
- `fastapi-users[sqlalchemy,oauth]>=15,<16`, `
|
|
53
|
+
- `fastapi-users[sqlalchemy,oauth]>=15,<16`, `cachetools`, `typer`
|
|
54
54
|
|
|
55
55
|
## Demo mode
|
|
56
56
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_users"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.38"
|
|
4
4
|
description = "Email + password user management, admin invites, RBAC-ready — replaces Keycloak for simple_module apps"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,16 +21,18 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"simple_module_core==0.0.
|
|
25
|
-
"simple_module_db==0.0.
|
|
26
|
-
"simple_module_hosting==0.0.
|
|
27
|
-
"simple_module_settings==0.0.
|
|
28
|
-
"simple_module_auth==0.0.
|
|
24
|
+
"simple_module_core==0.0.38",
|
|
25
|
+
"simple_module_db==0.0.38",
|
|
26
|
+
"simple_module_hosting==0.0.38",
|
|
27
|
+
"simple_module_settings==0.0.38",
|
|
28
|
+
"simple_module_auth==0.0.38",
|
|
29
|
+
# Sends through the framework mail sender; `mail` owns the SMTP transport
|
|
30
|
+
# (GH #496), so an upgrade keeps delivering mail.
|
|
31
|
+
"simple_module_mail==0.0.38",
|
|
29
32
|
# Pinned to a narrow range: `deps.py` relies on mutating CookieTransport
|
|
30
33
|
# fields after construction (see reconfigure_cookie_transport in backend.py).
|
|
31
34
|
# Bumping the major version requires re-checking those field names.
|
|
32
35
|
"fastapi-users[sqlalchemy,oauth]>=15,<16",
|
|
33
|
-
"aiosmtplib>=3.0",
|
|
34
36
|
"cachetools>=5.3",
|
|
35
37
|
"typer>=0.12",
|
|
36
38
|
]
|
|
@@ -63,3 +65,4 @@ simple_module_db = { workspace = true }
|
|
|
63
65
|
simple_module_hosting = { workspace = true }
|
|
64
66
|
simple_module_settings = { workspace = true }
|
|
65
67
|
simple_module_auth = { workspace = true }
|
|
68
|
+
simple_module_mail = { workspace = true }
|
|
@@ -60,8 +60,14 @@ async def _build_app(db_state, inner_handler=None, *, principal_resolvers=None):
|
|
|
60
60
|
|
|
61
61
|
app = FastAPI()
|
|
62
62
|
app.state.sm = SimpleNamespace(db=db_state)
|
|
63
|
+
from simple_module_core.public_routes import PublicRouteRegistry
|
|
64
|
+
from users.module import UsersModule
|
|
63
65
|
from users.provider import UsersAuthProvider
|
|
64
66
|
|
|
67
|
+
# The host fills this from every module's register_public_routes at boot.
|
|
68
|
+
app.state.public_routes = PublicRouteRegistry()
|
|
69
|
+
UsersModule().register_public_routes(app.state.public_routes)
|
|
70
|
+
|
|
65
71
|
app.state.auth = AuthState(
|
|
66
72
|
auth_provider=UsersAuthProvider(),
|
|
67
73
|
principal_resolvers=list(principal_resolvers or []),
|
|
@@ -7,9 +7,12 @@ wiring. Not named ``test_*`` so pytest does not collect it.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import httpx
|
|
10
11
|
from simple_module_hosting.settings import Settings
|
|
11
12
|
from users.constants import ADMIN_ROLE_ID, USER_ROLE_ID
|
|
12
13
|
|
|
14
|
+
ALICE = "alice@example.com"
|
|
15
|
+
|
|
13
16
|
# ---------------------------------------------------------------------------
|
|
14
17
|
|
|
15
18
|
|
|
@@ -17,7 +20,6 @@ def _users_overrides(allow_signup: bool = False) -> dict[str, tuple[str, str]]:
|
|
|
17
20
|
"""Values seeded into the ``SettingsStore`` before the app lifespan runs."""
|
|
18
21
|
return {
|
|
19
22
|
"allow_signup": (str(allow_signup).lower(), "bool"),
|
|
20
|
-
"mailer": ("console", "string"),
|
|
21
23
|
"base_url": ("http://testserver", "string"),
|
|
22
24
|
"cookie_secure": ("false", "bool"),
|
|
23
25
|
# 32+ bytes to clear pyjwt's InsecureKeyLengthWarning for HMAC-SHA256.
|
|
@@ -84,7 +86,9 @@ async def _seed_roles(application) -> None:
|
|
|
84
86
|
await session.commit()
|
|
85
87
|
|
|
86
88
|
|
|
87
|
-
async def _seed_users_settings(
|
|
89
|
+
async def _seed_users_settings(
|
|
90
|
+
application, *, allow_signup: bool, overrides: dict[str, tuple[str, str]] | None = None
|
|
91
|
+
) -> None:
|
|
88
92
|
"""Write the test UsersSettings overrides to the DB before hydrate runs.
|
|
89
93
|
|
|
90
94
|
Hydrate fires inside the lifespan ``__aenter__``, so this needs to land
|
|
@@ -95,7 +99,8 @@ async def _seed_users_settings(application, *, allow_signup: bool) -> None:
|
|
|
95
99
|
|
|
96
100
|
async with application.state.sm.db.session_factory() as session:
|
|
97
101
|
store = SettingsStore(SettingService(session))
|
|
98
|
-
|
|
102
|
+
rows = _users_overrides(allow_signup=allow_signup) | (overrides or {})
|
|
103
|
+
for field, (raw, vtype) in rows.items():
|
|
99
104
|
await store.set_override("users", field, raw, vtype)
|
|
100
105
|
await session.commit()
|
|
101
106
|
|
|
@@ -136,19 +141,33 @@ async def _make_standard_user(app, email: str = "user@example.com"):
|
|
|
136
141
|
return user
|
|
137
142
|
|
|
138
143
|
|
|
139
|
-
async def _build_users_app(
|
|
140
|
-
|
|
144
|
+
async def _build_users_app(
|
|
145
|
+
monkeypatch,
|
|
146
|
+
*,
|
|
147
|
+
allow_signup: bool,
|
|
148
|
+
seed_admin: bool = True,
|
|
149
|
+
database_url: str = "sqlite+aiosqlite:///:memory:",
|
|
150
|
+
overrides: dict[str, tuple[str, str]] | None = None,
|
|
151
|
+
):
|
|
152
|
+
"""Build a test FastAPI app with DB created, settings seeded, lifespan started.
|
|
153
|
+
|
|
154
|
+
``database_url`` lets several apps share one SQLite file, standing in for
|
|
155
|
+
workers of one deployment; ``overrides`` adds ``UsersSettings`` rows.
|
|
156
|
+
"""
|
|
141
157
|
from simple_module_hosting.app_builder import create_app
|
|
142
158
|
|
|
143
159
|
settings = Settings(
|
|
144
|
-
database_url=
|
|
160
|
+
database_url=database_url,
|
|
145
161
|
environment="testing",
|
|
146
162
|
secret_key="test-secret-key",
|
|
147
163
|
multi_tenant=False,
|
|
164
|
+
# Explicit: a developer's .env pointing at the shared Redis would make
|
|
165
|
+
# login-lockout counters outlive the test that wrote them.
|
|
166
|
+
redis_url=None,
|
|
148
167
|
)
|
|
149
168
|
application = create_app(settings)
|
|
150
169
|
await _setup_app_db(application)
|
|
151
|
-
await _seed_users_settings(application, allow_signup=allow_signup)
|
|
170
|
+
await _seed_users_settings(application, allow_signup=allow_signup, overrides=overrides)
|
|
152
171
|
|
|
153
172
|
ctx = application.router.lifespan_context(application)
|
|
154
173
|
await ctx.__aenter__()
|
|
@@ -161,3 +180,30 @@ async def _build_users_app(monkeypatch, *, allow_signup: bool, seed_admin: bool
|
|
|
161
180
|
if seed_admin:
|
|
162
181
|
await _make_admin_user(application)
|
|
163
182
|
return application, ctx
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Request and row helpers for the login-lockout tests
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _client(app, ip: str) -> httpx.AsyncClient:
|
|
191
|
+
"""A client whose requests arrive from *ip*."""
|
|
192
|
+
transport = httpx.ASGITransport(app=app, client=(ip, 1))
|
|
193
|
+
return httpx.AsyncClient(transport=transport, base_url="http://testserver")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def _login(app, ip: str, username: str, password: str = "wrong") -> httpx.Response:
|
|
197
|
+
async with _client(app, ip) as c:
|
|
198
|
+
return await c.post(
|
|
199
|
+
"/api/users/auth/login", data={"username": username, "password": password}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def user_row(app, email: str):
|
|
204
|
+
"""The ``User`` row for *email*, or ``None``, read in a fresh session."""
|
|
205
|
+
from sqlalchemy import select
|
|
206
|
+
from users.models import User
|
|
207
|
+
|
|
208
|
+
async with app.state.sm.db.session_factory() as session:
|
|
209
|
+
return (await session.execute(select(User).where(User.email == email))).scalar_one_or_none()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The ``users_app`` fixture builds a full FastAPI app via ``create_app`` but
|
|
4
4
|
with an in-memory SQLite database, seeded roles, and test-friendly settings
|
|
5
|
-
(
|
|
5
|
+
(no mail sender installed, signup disabled by default, short secrets).
|
|
6
6
|
|
|
7
7
|
The ``anon_client`` gives a plain httpx client.
|
|
8
8
|
The ``admin_client`` gives a client with a signed local-user session cookie
|
|
@@ -21,6 +21,7 @@ import pytest
|
|
|
21
21
|
# top-level modules on pytest's sys.path — same mechanism as the
|
|
22
22
|
# _middleware_support plugin loaded at the bottom of this file.
|
|
23
23
|
from _users_app_builders import (
|
|
24
|
+
ALICE,
|
|
24
25
|
_build_users_app,
|
|
25
26
|
_make_admin_user,
|
|
26
27
|
_make_standard_user,
|
|
@@ -148,6 +149,12 @@ async def user_client(users_app) -> AsyncGenerator[httpx.AsyncClient, None]:
|
|
|
148
149
|
yield c
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
@pytest.fixture
|
|
153
|
+
async def alice(users_app):
|
|
154
|
+
"""A standard user, ``ALICE``, for the login-lockout tests."""
|
|
155
|
+
return await _make_standard_user(users_app, ALICE)
|
|
156
|
+
|
|
157
|
+
|
|
151
158
|
# ---------------------------------------------------------------------------
|
|
152
159
|
# DB session fixture scoped to users_app
|
|
153
160
|
# ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,7 @@ message nobody received.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
|
+
import re
|
|
13
14
|
from datetime import UTC, datetime, timedelta
|
|
14
15
|
|
|
15
16
|
import pytest
|
|
@@ -174,14 +175,15 @@ class TestResendInvite:
|
|
|
174
175
|
|
|
175
176
|
class TestInviteTokenClaims:
|
|
176
177
|
@pytest.mark.anyio
|
|
177
|
-
async def test_invite_token_names_the_inviter(self, admin_client, users_app,
|
|
178
|
+
async def test_invite_token_names_the_inviter(self, admin_client, users_app, captured_mail):
|
|
178
179
|
"""The accept-invite card says who invited you; the claim carries it."""
|
|
179
180
|
import jwt
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
await _invite(admin_client, "whosent@example.com")
|
|
183
|
+
text = next(
|
|
184
|
+
m.text for m in captured_mail.to("whosent@example.com") if "invited you" in m.subject
|
|
185
|
+
)
|
|
186
|
+
token = re.search(r"token=(\S+)", text).group(1)
|
|
185
187
|
|
|
186
188
|
payload = jwt.decode(
|
|
187
189
|
token,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""``GET /api/users/admin`` validates paging like every other JSON list (#513)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from simple_module_db.pagination import MAX_PAGE, MAX_PER_PAGE
|
|
7
|
+
|
|
8
|
+
ADMIN_URL = "/api/users/admin"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize(
|
|
12
|
+
"params",
|
|
13
|
+
[
|
|
14
|
+
{"page": 0},
|
|
15
|
+
{"page": -3},
|
|
16
|
+
{"page": MAX_PAGE + 1},
|
|
17
|
+
{"per_page": 0},
|
|
18
|
+
{"per_page": -1},
|
|
19
|
+
{"per_page": MAX_PER_PAGE + 1},
|
|
20
|
+
{"per_page": 100000},
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
async def test_rejects_out_of_range(params: dict, admin_client, users_db) -> None:
|
|
24
|
+
resp = await admin_client.get(ADMIN_URL, params=params)
|
|
25
|
+
assert resp.status_code == 422
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def test_accepts_bounds(admin_client, users_db) -> None:
|
|
29
|
+
resp = await admin_client.get(ADMIN_URL, params={"page": MAX_PAGE, "per_page": MAX_PER_PAGE})
|
|
30
|
+
assert resp.status_code == 200
|
|
31
|
+
assert resp.json() == []
|
|
@@ -62,7 +62,7 @@ class TestForgotPasswordProps:
|
|
|
62
62
|
|
|
63
63
|
@pytest.mark.anyio
|
|
64
64
|
async def test_console_mailer_does_not_deliver(self, anon_client):
|
|
65
|
-
"""The test app
|
|
65
|
+
"""The test app has no delivering mail sender, so the amber callout must show."""
|
|
66
66
|
props = _props(await anon_client.get("/users/forgot-password", headers=_INERTIA_HEADERS))
|
|
67
67
|
assert props["mailer_delivers"] is False
|
|
68
68
|
|
|
@@ -192,16 +192,20 @@ class TestAPasswordChangeStrandsBearerTokens:
|
|
|
192
192
|
async def test_changing_the_password_deletes_the_rows(self, users_app, anon_client):
|
|
193
193
|
"""The stamped counter already strands them; deleting is what stops a
|
|
194
194
|
stolen token resolving a row at all, and stops the refresh tokens
|
|
195
|
-
minting fresh ones behind it.
|
|
195
|
+
minting fresh ones behind it. The one row left is the changing
|
|
196
|
+
browser's reissued ``sm_auth`` credential, minted under the new counter."""
|
|
196
197
|
assert (await anon_client.post(_LOGIN, data=_CREDS)).status_code == 204
|
|
197
|
-
|
|
198
|
+
before = {row.token for row in await _tokens(users_app)}
|
|
199
|
+
assert before
|
|
198
200
|
|
|
199
201
|
resp = await anon_client.post(
|
|
200
202
|
"/api/users/me/password",
|
|
201
203
|
json={"current_password": "AdminPass1!", "new_password": "NewAdminPass1!"},
|
|
202
204
|
)
|
|
203
205
|
assert resp.status_code == 204, resp.text
|
|
204
|
-
|
|
206
|
+
reissued = await _only_token(users_app)
|
|
207
|
+
assert reissued.token not in before
|
|
208
|
+
assert reissued.session_version == 1
|
|
205
209
|
|
|
206
210
|
|
|
207
211
|
class TestTheMintedCounterIsCurrent:
|
|
@@ -187,7 +187,7 @@ async def test_bootstrap_from_env_noop_when_unset(users_app_empty) -> None:
|
|
|
187
187
|
bootstrap_password="",
|
|
188
188
|
)
|
|
189
189
|
# Patch fake_app.state onto a real app-like object
|
|
190
|
-
await bootstrap_admin_from_env(fake_app)
|
|
190
|
+
await bootstrap_admin_from_env(fake_app)
|
|
191
191
|
|
|
192
192
|
# No users should have been created
|
|
193
193
|
async with users_app_empty.state.sm.db.session_factory() as s:
|
|
@@ -217,7 +217,7 @@ async def test_bootstrap_from_env_noop_when_table_nonempty(users_app_empty) -> N
|
|
|
217
217
|
bootstrap_email="admin@test.example",
|
|
218
218
|
bootstrap_password="AdminPass1!",
|
|
219
219
|
)
|
|
220
|
-
await bootstrap_admin_from_env(fake_app)
|
|
220
|
+
await bootstrap_admin_from_env(fake_app)
|
|
221
221
|
|
|
222
222
|
# Only the original dummy user should exist
|
|
223
223
|
async with users_app_empty.state.sm.db.session_factory() as s:
|
|
@@ -236,7 +236,7 @@ async def test_bootstrap_from_env_creates_admin_when_empty_and_configured(users_
|
|
|
236
236
|
bootstrap_email="bootstrap@test.example",
|
|
237
237
|
bootstrap_password="BootPass1!",
|
|
238
238
|
)
|
|
239
|
-
await bootstrap_admin_from_env(fake_app)
|
|
239
|
+
await bootstrap_admin_from_env(fake_app)
|
|
240
240
|
|
|
241
241
|
async with users_app_empty.state.sm.db.session_factory() as s:
|
|
242
242
|
user = (
|
|
@@ -247,3 +247,31 @@ async def test_bootstrap_from_env_creates_admin_when_empty_and_configured(users_
|
|
|
247
247
|
assert user.is_superuser is True
|
|
248
248
|
assert user.is_verified is True
|
|
249
249
|
assert user.is_active is True
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@pytest.mark.parametrize("generated", [False, True])
|
|
253
|
+
async def test_bootstrap_logs_generated_password_only_once(
|
|
254
|
+
users_app_empty, monkeypatch, caplog, generated
|
|
255
|
+
):
|
|
256
|
+
if generated:
|
|
257
|
+
monkeypatch.setenv("SM_USERS_BOOTSTRAP_PASSWORD_GENERATED", "1")
|
|
258
|
+
fake_app = _make_fake_app(
|
|
259
|
+
users_app_empty.state.sm.db.session_factory,
|
|
260
|
+
bootstrap_email="first@test.example",
|
|
261
|
+
bootstrap_password="GeneratedPass1!",
|
|
262
|
+
)
|
|
263
|
+
with caplog.at_level("WARNING", logger="users.bootstrap"):
|
|
264
|
+
await bootstrap_admin_from_env(fake_app)
|
|
265
|
+
first_messages = list(caplog.messages)
|
|
266
|
+
caplog.clear()
|
|
267
|
+
# A restart generates a different unused password for an existing install.
|
|
268
|
+
fake_app.state.users.settings.bootstrap_password = "UnusedPass2!"
|
|
269
|
+
await bootstrap_admin_from_env(fake_app)
|
|
270
|
+
assert caplog.messages == []
|
|
271
|
+
if generated:
|
|
272
|
+
assert first_messages == [
|
|
273
|
+
"first-boot admin created; store this password securely: "
|
|
274
|
+
"first@test.example / GeneratedPass1!"
|
|
275
|
+
]
|
|
276
|
+
else:
|
|
277
|
+
assert first_messages == []
|
|
@@ -8,8 +8,6 @@ rather than stopping at the API boundary.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import logging
|
|
12
|
-
|
|
13
11
|
import pytest
|
|
14
12
|
|
|
15
13
|
_URL = "/api/users/admin/invite/bulk"
|
|
@@ -111,11 +109,15 @@ class TestBulkInviteMessage:
|
|
|
111
109
|
assert row["state"] == "invited"
|
|
112
110
|
|
|
113
111
|
|
|
114
|
-
class
|
|
112
|
+
class TestTemplatedMailerMessage:
|
|
113
|
+
@pytest.mark.anyio
|
|
114
|
+
async def test_mailer_quotes_the_message(self, users_app, captured_mail):
|
|
115
|
+
mailer = users_app.state.users.mailer
|
|
116
|
+
await mailer.send_invite("x@example.com", "tok", "Alex Doyle", "See you Monday.")
|
|
117
|
+
assert "See you Monday." in captured_mail.to("x@example.com")[0].text
|
|
118
|
+
|
|
115
119
|
@pytest.mark.anyio
|
|
116
|
-
async def
|
|
120
|
+
async def test_blank_message_leaves_no_quote(self, users_app, captured_mail):
|
|
117
121
|
mailer = users_app.state.users.mailer
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
record = next(r for r in caplog.records if r.getMessage() == "users.invite.email")
|
|
121
|
-
assert record.message_body == "See you Monday."
|
|
122
|
+
await mailer.send_invite("x@example.com", "tok", "Alex Doyle", " \n ")
|
|
123
|
+
assert "See you" not in captured_mail.to("x@example.com")[0].text
|
|
@@ -9,6 +9,7 @@ feature is off. The third, a demo session that is allowed to write, is
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from _demo_support import DEMO_ADMIN_EMAIL, DEMO_USER_EMAIL
|
|
12
|
+
from _users_app_builders import user_row
|
|
12
13
|
from sqlalchemy import select
|
|
13
14
|
from users.constants import ADMIN_ROLE_NAME, USER_ROLE_NAME
|
|
14
15
|
from users.demo import (
|
|
@@ -75,11 +76,6 @@ def test_read_only_is_the_default_posture():
|
|
|
75
76
|
# ── seeding ─────────────────────────────────────────────────────────────────
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
async def _row(app, email: str) -> User | None:
|
|
79
|
-
async with app.state.sm.db.session_factory() as session:
|
|
80
|
-
return (await session.execute(select(User).where(User.email == email))).scalar_one_or_none()
|
|
81
|
-
|
|
82
|
-
|
|
83
79
|
async def _role_names(app, user_id) -> list[str]:
|
|
84
80
|
async with app.state.sm.db.session_factory() as session:
|
|
85
81
|
return list(
|
|
@@ -102,8 +98,8 @@ async def test_ensure_demo_users_seeds_both_accounts(users_app):
|
|
|
102
98
|
|
|
103
99
|
assert len(ids) == 2
|
|
104
100
|
assert users_app.state.users.demo_user_ids == ids
|
|
105
|
-
admin = await
|
|
106
|
-
user = await
|
|
101
|
+
admin = await user_row(users_app, DEMO_ADMIN_EMAIL)
|
|
102
|
+
user = await user_row(users_app, DEMO_USER_EMAIL)
|
|
107
103
|
assert admin is not None and admin.is_superuser
|
|
108
104
|
assert user is not None and not user.is_superuser
|
|
109
105
|
assert all(row.is_active and row.is_verified for row in (admin, user))
|
|
@@ -113,8 +109,8 @@ async def test_each_account_gets_its_own_role(users_app):
|
|
|
113
109
|
users_app.state.users.settings.demo_mode = True
|
|
114
110
|
await ensure_demo_users(users_app)
|
|
115
111
|
|
|
116
|
-
admin = await
|
|
117
|
-
user = await
|
|
112
|
+
admin = await user_row(users_app, DEMO_ADMIN_EMAIL)
|
|
113
|
+
user = await user_row(users_app, DEMO_USER_EMAIL)
|
|
118
114
|
assert await _role_names(users_app, admin.id) == [ADMIN_ROLE_NAME]
|
|
119
115
|
assert await _role_names(users_app, user.id) == [USER_ROLE_NAME]
|
|
120
116
|
|
|
@@ -124,8 +120,8 @@ async def test_a_blank_password_still_produces_usable_accounts(users_app):
|
|
|
124
120
|
users_app.state.users.settings.demo_mode = True
|
|
125
121
|
await ensure_demo_users(users_app)
|
|
126
122
|
|
|
127
|
-
assert (await
|
|
128
|
-
assert (await
|
|
123
|
+
assert (await user_row(users_app, DEMO_ADMIN_EMAIL)).hashed_password
|
|
124
|
+
assert (await user_row(users_app, DEMO_USER_EMAIL)).hashed_password
|
|
129
125
|
|
|
130
126
|
|
|
131
127
|
async def test_a_generated_password_is_not_re_rolled_on_every_boot(users_app):
|
|
@@ -133,23 +129,23 @@ async def test_a_generated_password_is_not_re_rolled_on_every_boot(users_app):
|
|
|
133
129
|
worker per restart, for no gain."""
|
|
134
130
|
users_app.state.users.settings.demo_mode = True
|
|
135
131
|
await ensure_demo_users(users_app)
|
|
136
|
-
first = (await
|
|
132
|
+
first = (await user_row(users_app, DEMO_ADMIN_EMAIL)).hashed_password
|
|
137
133
|
|
|
138
134
|
await ensure_demo_users(users_app)
|
|
139
135
|
|
|
140
|
-
assert (await
|
|
136
|
+
assert (await user_row(users_app, DEMO_ADMIN_EMAIL)).hashed_password == first
|
|
141
137
|
|
|
142
138
|
|
|
143
139
|
async def test_a_configured_password_is_reapplied(users_app):
|
|
144
140
|
"""Changing a demo password in the admin UI has to take effect."""
|
|
145
141
|
users_app.state.users.settings.demo_mode = True
|
|
146
142
|
await ensure_demo_users(users_app)
|
|
147
|
-
first = (await
|
|
143
|
+
first = (await user_row(users_app, DEMO_USER_EMAIL)).hashed_password
|
|
148
144
|
|
|
149
145
|
users_app.state.users.settings.demo_user_password = "a-published-demo-password"
|
|
150
146
|
await ensure_demo_users(users_app)
|
|
151
147
|
|
|
152
|
-
assert (await
|
|
148
|
+
assert (await user_row(users_app, DEMO_USER_EMAIL)).hashed_password != first
|
|
153
149
|
|
|
154
150
|
|
|
155
151
|
async def test_demo_off_clears_the_cached_ids(users_app):
|
{simple_module_users-0.0.37 → simple_module_users-0.0.38}/tests/test_edit_recent_activity.py
RENAMED
|
@@ -102,7 +102,7 @@ class TestRecentActivity:
|
|
|
102
102
|
settings.
|
|
103
103
|
"""
|
|
104
104
|
from audit_log.models import AuditEntry
|
|
105
|
-
from
|
|
105
|
+
from simple_module_db.stored_settings import Setting
|
|
106
106
|
|
|
107
107
|
user_id = await _admin_id(users_app)
|
|
108
108
|
async with users_app.state.sm.db.session_factory() as session:
|
|
@@ -147,17 +147,19 @@ class TestRecentActivity:
|
|
|
147
147
|
|
|
148
148
|
@pytest.mark.anyio
|
|
149
149
|
async def test_prop_is_none_when_audit_log_is_not_installed(self, admin_client, users_app):
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
150
|
+
"""No activity feed, absent card — not an empty one claiming no activity.
|
|
151
|
+
|
|
152
|
+
The card reads the core activity feed that ``audit_log`` installs
|
|
153
|
+
(GH #501); an app built without the module has none.
|
|
154
|
+
"""
|
|
155
|
+
from simple_module_core.activity_feed import get_activity_feed, set_activity_feed
|
|
156
|
+
|
|
157
|
+
feed = get_activity_feed(users_app)
|
|
158
|
+
assert feed is not None, "audit_log installed no feed, so this proves nothing"
|
|
159
|
+
set_activity_feed(users_app, None)
|
|
158
160
|
try:
|
|
159
161
|
user_id = await _admin_id(users_app)
|
|
160
162
|
props = await _edit_props(admin_client, user_id)
|
|
161
163
|
assert props["recent_activity"] is None
|
|
162
164
|
finally:
|
|
163
|
-
|
|
165
|
+
set_activity_feed(users_app, feed)
|