regstack 0.2.5__tar.gz → 0.2.6__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.
- {regstack-0.2.5 → regstack-0.2.6}/CHANGELOG.md +13 -0
- {regstack-0.2.5 → regstack-0.2.6}/PKG-INFO +1 -1
- {regstack-0.2.5 → regstack-0.2.6}/docs/changelog.md +30 -0
- {regstack-0.2.5 → regstack-0.2.6}/pyproject.toml +1 -1
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/pending_repo.py +4 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/protocols.py +16 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/repositories/pending_repo.py +9 -1
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/admin.py +1 -7
- regstack-0.2.6/src/regstack/version.py +1 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_admin_router.py +56 -0
- {regstack-0.2.5 → regstack-0.2.6}/uv.lock +1 -1
- regstack-0.2.5/src/regstack/version.py +0 -1
- {regstack-0.2.5 → regstack-0.2.6}/.github/workflows/publish.yml +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/.github/workflows/test.yml +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/.gitignore +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/.python-version +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/.readthedocs.yaml +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/CLAUDE.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/LICENSE +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/NOTICE +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/README.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/SECURITY.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/_static/.gitkeep +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/_templates/.gitkeep +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/api.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/architecture.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/cli.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/conf.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/configuration.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/embedding.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/index.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/quickstart.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/security.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/docs/theming.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/_common/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/_common/app.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/mongo/README.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/mongo/main.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/mongo/regstack.toml +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/postgres/README.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/postgres/main.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/postgres/regstack.toml +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/sqlite/README.md +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/sqlite/main.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/regstack.toml.example +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/app.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/clock.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/dependencies.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/password.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/base.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/factory.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/indexes.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/cli/__main__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/cli/admin.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/cli/doctor.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/cli/init.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/config/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/config/loader.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/config/schema.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/config/secrets.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/base.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/composer.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/console.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/factory.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/ses.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/smtp.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/hooks/events.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/models/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/models/user.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/account.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/login.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/logout.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/password.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/phone.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/register.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/routers/verify.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/sms/base.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/sms/factory.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/sms/null.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/sms/sns.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/pages.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tasks.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/conftest.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_account_management.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_happy_path.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_indexes.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_mfa.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/integration/test_verification.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/__init__.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_cli.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_cli_doctor.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_cli_init.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_jwt.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_lockout.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_password.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_sms.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.2.5 → regstack-0.2.6}/tests/unit/test_ui_env.py +0 -0
|
@@ -5,6 +5,19 @@ authoritative copy lives at
|
|
|
5
5
|
[`docs/changelog.md`](docs/changelog.md) and is rendered into the
|
|
6
6
|
Sphinx docs.
|
|
7
7
|
|
|
8
|
+
## 0.2.6 — 2026-04-28
|
|
9
|
+
|
|
10
|
+
Bug fix.
|
|
11
|
+
|
|
12
|
+
- **Fix:** `/admin/stats` reported `pending_registrations: 0` on
|
|
13
|
+
every SQL backend. The route reached into the Mongo repo's private
|
|
14
|
+
`_collection` attribute and silently fell back to `0` when the
|
|
15
|
+
attribute was absent. Added `count_unexpired(now=None)` to
|
|
16
|
+
`PendingRepoProtocol` with Mongo + SQL implementations and routed
|
|
17
|
+
through `rs.clock.now()` so the count respects the injected clock.
|
|
18
|
+
New parametrized integration test exercises the count on every
|
|
19
|
+
backend.
|
|
20
|
+
|
|
8
21
|
## 0.2.5 — 2026-04-28
|
|
9
22
|
|
|
10
23
|
Bug fix + tooling.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jdrumgoole/regstack
|
|
6
6
|
Project-URL: Repository, https://github.com/jdrumgoole/regstack
|
|
@@ -3,6 +3,36 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Versions follow
|
|
4
4
|
[Semantic Versioning](https://semver.org/) once `1.0.0` ships.
|
|
5
5
|
|
|
6
|
+
## 0.2.6 — 2026-04-28
|
|
7
|
+
|
|
8
|
+
**Bug fix.**
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- ``/admin/stats`` reported ``pending_registrations: 0`` on every SQL
|
|
13
|
+
backend. The route reached into the Mongo repo's private
|
|
14
|
+
``_collection`` attribute and silently fell back to ``0`` when the
|
|
15
|
+
attribute was absent — the kind of failure that survives a
|
|
16
|
+
multi-backend refactor when the integration tests don't pin the
|
|
17
|
+
number.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- ``PendingRepoProtocol.count_unexpired(now=None) -> int``, with Mongo
|
|
22
|
+
and SQL implementations. "Unexpired" rather than a raw row count
|
|
23
|
+
because SQL backends accumulate dead rows until ``purge_expired``
|
|
24
|
+
runs; an admin looking at "pending: 47" wants 47 *live* rows.
|
|
25
|
+
- The admin stats route now routes the count through
|
|
26
|
+
``rs.clock.now()``. Without this, ``FrozenClock``-driven tests
|
|
27
|
+
would see every row as "expired" because the route would be reading
|
|
28
|
+
wall-clock time while the rest of the system runs on the injected
|
|
29
|
+
clock. Same shape of clock-injection drift the bulk-revoke fix
|
|
30
|
+
closed earlier.
|
|
31
|
+
- New parametrized integration test
|
|
32
|
+
``test_stats_pending_registrations_count_unexpired`` runs against
|
|
33
|
+
SQLite + Mongo + Postgres and confirms the count excludes expired
|
|
34
|
+
rows on every backend.
|
|
35
|
+
|
|
6
36
|
## 0.2.5 — 2026-04-28
|
|
7
37
|
|
|
8
38
|
**Bug fix + tooling.**
|
|
@@ -64,6 +64,10 @@ class PendingRepo:
|
|
|
64
64
|
result = await self._collection.delete_many({"expires_at": {"$lt": cutoff}})
|
|
65
65
|
return int(result.deleted_count)
|
|
66
66
|
|
|
67
|
+
async def count_unexpired(self, now: datetime | None = None) -> int:
|
|
68
|
+
cutoff = now or datetime.now(UTC)
|
|
69
|
+
return await self._collection.count_documents({"expires_at": {"$gt": cutoff}})
|
|
70
|
+
|
|
67
71
|
@staticmethod
|
|
68
72
|
def _hydrate(doc: dict[str, Any] | None) -> PendingRegistration | None:
|
|
69
73
|
if doc is None:
|
|
@@ -154,6 +154,22 @@ class PendingRepoProtocol(Protocol):
|
|
|
154
154
|
"""
|
|
155
155
|
...
|
|
156
156
|
|
|
157
|
+
async def count_unexpired(self, now: datetime | None = None) -> int:
|
|
158
|
+
"""Count pending-registration rows whose ``expires_at`` is in the future.
|
|
159
|
+
|
|
160
|
+
"Unexpired" rather than a raw row-count because SQL backends
|
|
161
|
+
accumulate dead rows until ``purge_expired`` runs — a raw
|
|
162
|
+
count would double-report a verification email that's been
|
|
163
|
+
unanswered for a month and a fresh one sent today.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
now: Reference instant. Defaults to ``datetime.now(UTC)``.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Number of pending rows with ``expires_at > now``.
|
|
170
|
+
"""
|
|
171
|
+
...
|
|
172
|
+
|
|
157
173
|
|
|
158
174
|
@runtime_checkable
|
|
159
175
|
class BlacklistRepoProtocol(Protocol):
|
|
@@ -4,7 +4,7 @@ import uuid
|
|
|
4
4
|
from datetime import UTC, datetime
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from sqlalchemy import delete, select
|
|
7
|
+
from sqlalchemy import delete, func, select
|
|
8
8
|
|
|
9
9
|
from regstack.backends.sql.schema import pending_table
|
|
10
10
|
from regstack.models.pending_registration import PendingRegistration
|
|
@@ -56,6 +56,14 @@ class SqlPendingRepo:
|
|
|
56
56
|
result = await conn.execute(delete(self._t).where(self._t.c.expires_at < cutoff))
|
|
57
57
|
return int(result.rowcount or 0)
|
|
58
58
|
|
|
59
|
+
async def count_unexpired(self, now: datetime | None = None) -> int:
|
|
60
|
+
cutoff = now or datetime.now(UTC)
|
|
61
|
+
async with self._engine.connect() as conn:
|
|
62
|
+
result = await conn.execute(
|
|
63
|
+
select(func.count()).select_from(self._t).where(self._t.c.expires_at > cutoff)
|
|
64
|
+
)
|
|
65
|
+
return int(result.scalar_one())
|
|
66
|
+
|
|
59
67
|
|
|
60
68
|
def _pending_values(p: PendingRegistration) -> dict:
|
|
61
69
|
return {
|
|
@@ -50,13 +50,7 @@ def build_admin_router(rs: RegStack) -> APIRouter:
|
|
|
50
50
|
active = await rs.users.count(is_active=True)
|
|
51
51
|
verified = await rs.users.count(is_verified=True)
|
|
52
52
|
supers = await rs.users.count(is_superuser=True)
|
|
53
|
-
|
|
54
|
-
# mongo backend; SQL backends will add a typed count too.
|
|
55
|
-
pending_count_doc = getattr(rs.pending, "_collection", None)
|
|
56
|
-
if pending_count_doc is not None:
|
|
57
|
-
pending = await pending_count_doc.count_documents({})
|
|
58
|
-
else: # pragma: no cover — used by SQL backends in Phase 2
|
|
59
|
-
pending = 0
|
|
53
|
+
pending = await rs.pending.count_unexpired(rs.clock.now())
|
|
60
54
|
return AdminStats(
|
|
61
55
|
total_users=total,
|
|
62
56
|
active_users=active,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.6"
|
|
@@ -161,6 +161,62 @@ async def test_admin_resend_verification(make_client) -> None:
|
|
|
161
161
|
assert r.status_code == 400
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
@pytest.mark.asyncio
|
|
165
|
+
async def test_stats_pending_registrations_count_unexpired(make_client) -> None:
|
|
166
|
+
"""Admin stats must report pending-registration count correctly on every backend.
|
|
167
|
+
|
|
168
|
+
Before this test landed, the stats route reached into Mongo's private
|
|
169
|
+
``_collection`` attribute and silently returned 0 on SQL backends. The
|
|
170
|
+
parametrized ``backend_kind`` fixture means this asserts the count
|
|
171
|
+
against SQLite, MongoDB, and PostgreSQL in turn — a gap that survived
|
|
172
|
+
the multi-backend refactor because the existing admin tests didn't
|
|
173
|
+
pin this number.
|
|
174
|
+
|
|
175
|
+
Also seeds an already-expired pending row directly via the repo to
|
|
176
|
+
confirm the count excludes it (the SQL backend has no TTL reaper, so
|
|
177
|
+
this distinction matters).
|
|
178
|
+
"""
|
|
179
|
+
from datetime import timedelta
|
|
180
|
+
|
|
181
|
+
from regstack.models.pending_registration import PendingRegistration
|
|
182
|
+
|
|
183
|
+
async with make_client(
|
|
184
|
+
require_verification=True,
|
|
185
|
+
enable_admin_router=True,
|
|
186
|
+
) as (rs, client):
|
|
187
|
+
await rs.bootstrap_admin("admin@example.com", "adminadminadmin")
|
|
188
|
+
admin_token = await _login(client, "admin@example.com", "adminadminadmin")
|
|
189
|
+
headers = {"authorization": f"Bearer {admin_token}"}
|
|
190
|
+
|
|
191
|
+
# Two fresh registrations → two unexpired pending rows.
|
|
192
|
+
await client.post(REGISTER, json=ALICE)
|
|
193
|
+
await client.post(REGISTER, json=BOB)
|
|
194
|
+
|
|
195
|
+
r = await client.get(ADMIN_STATS, headers=headers)
|
|
196
|
+
assert r.status_code == 200
|
|
197
|
+
assert r.json()["pending_registrations"] == 2
|
|
198
|
+
|
|
199
|
+
# Insert a stale pending row directly, anchored to ``rs.clock`` so
|
|
200
|
+
# it's "in the past" from the route's POV (which also reads the
|
|
201
|
+
# injected clock). Mongo would reap it via TTL eventually; SQL
|
|
202
|
+
# leaves it in place until purge_expired runs. Either way,
|
|
203
|
+
# count_unexpired must exclude it.
|
|
204
|
+
stale = PendingRegistration(
|
|
205
|
+
email="stale@example.com",
|
|
206
|
+
hashed_password="x",
|
|
207
|
+
full_name="Stale",
|
|
208
|
+
token_hash="stale-token-hash",
|
|
209
|
+
expires_at=rs.clock.now() - timedelta(hours=1),
|
|
210
|
+
)
|
|
211
|
+
await rs.pending.upsert(stale)
|
|
212
|
+
|
|
213
|
+
r = await client.get(ADMIN_STATS, headers=headers)
|
|
214
|
+
assert r.status_code == 200
|
|
215
|
+
assert r.json()["pending_registrations"] == 2, (
|
|
216
|
+
f"stale row should not be counted: {r.json()}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
164
220
|
@pytest.mark.asyncio
|
|
165
221
|
async def test_admin_404_for_unknown_user(make_client) -> None:
|
|
166
222
|
async with make_client(enable_admin_router=True) as (rs, client):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/blacklist_repo.py
RENAMED
|
File without changes
|
{regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/mongo/repositories/login_attempt_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.2.5 → regstack-0.2.6}/src/regstack/backends/sql/repositories/login_attempt_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|