regstack 0.2.2__tar.gz → 0.2.3__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.2 → regstack-0.2.3}/CHANGELOG.md +10 -0
- {regstack-0.2.2 → regstack-0.2.3}/PKG-INFO +1 -1
- regstack-0.2.3/docs/api.md +278 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/changelog.md +28 -0
- {regstack-0.2.2 → regstack-0.2.3}/pyproject.toml +1 -1
- regstack-0.2.3/src/regstack/app.py +330 -0
- regstack-0.2.3/src/regstack/auth/clock.py +73 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/dependencies.py +47 -5
- regstack-0.2.3/src/regstack/auth/jwt.py +256 -0
- regstack-0.2.3/src/regstack/auth/lockout.py +119 -0
- regstack-0.2.3/src/regstack/auth/password.py +65 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/protocols.py +31 -6
- regstack-0.2.3/src/regstack/email/base.py +71 -0
- regstack-0.2.3/src/regstack/hooks/events.py +98 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/__init__.py +21 -1
- regstack-0.2.3/src/regstack/sms/base.py +67 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/pages.py +49 -8
- regstack-0.2.3/src/regstack/version.py +1 -0
- {regstack-0.2.2 → regstack-0.2.3}/uv.lock +1 -1
- regstack-0.2.2/docs/api.md +0 -184
- regstack-0.2.2/src/regstack/app.py +0 -166
- regstack-0.2.2/src/regstack/auth/clock.py +0 -29
- regstack-0.2.2/src/regstack/auth/jwt.py +0 -145
- regstack-0.2.2/src/regstack/auth/lockout.py +0 -59
- regstack-0.2.2/src/regstack/auth/password.py +0 -20
- regstack-0.2.2/src/regstack/email/base.py +0 -23
- regstack-0.2.2/src/regstack/hooks/events.py +0 -59
- regstack-0.2.2/src/regstack/sms/base.py +0 -24
- regstack-0.2.2/src/regstack/version.py +0 -1
- {regstack-0.2.2 → regstack-0.2.3}/.github/workflows/publish.yml +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/.github/workflows/test.yml +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/.gitignore +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/.python-version +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/.readthedocs.yaml +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/CLAUDE.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/LICENSE +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/NOTICE +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/README.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/SECURITY.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/_static/.gitkeep +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/_templates/.gitkeep +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/architecture.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/cli.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/conf.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/configuration.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/embedding.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/index.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/quickstart.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/security.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/docs/theming.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/_common/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/_common/app.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/README.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/main.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/regstack.toml +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/postgres/README.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/postgres/main.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/postgres/regstack.toml +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/sqlite/README.md +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/sqlite/main.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/regstack.toml.example +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/base.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/factory.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/indexes.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/__main__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/admin.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/doctor.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/init.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/loader.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/schema.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/secrets.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/composer.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/console.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/factory.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/ses.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/smtp.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/user.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/account.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/admin.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/login.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/logout.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/password.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/phone.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/register.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/verify.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/factory.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/null.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/sns.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tasks.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/conftest.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_account_management.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_admin_router.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_happy_path.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_indexes.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_mfa.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_verification.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/__init__.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_cli.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_jwt.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_lockout.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_password.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_sms.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_ui_env.py +0 -0
|
@@ -5,6 +5,16 @@ 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.3 — 2026-04-28
|
|
9
|
+
|
|
10
|
+
Docs-only release. Restructured the API reference around the current
|
|
11
|
+
package layout (post multi-backend refactor) and added Google-style
|
|
12
|
+
docstrings (Args / Returns / Raises) to the public surface — RegStack,
|
|
13
|
+
JwtCodec, PasswordHasher, LockoutService, AuthDependencies,
|
|
14
|
+
HookRegistry, EmailService, SmsService, the router builders, and the
|
|
15
|
+
Clock implementations. Dataclass field docs moved to PEP 258
|
|
16
|
+
attribute docstrings. Sphinx builds clean under `-W` again.
|
|
17
|
+
|
|
8
18
|
## 0.2.2 — 2026-04-28
|
|
9
19
|
|
|
10
20
|
Docs-only release. The README and Sphinx docs landing page now lead
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
Auto-generated from docstrings. The most useful entry point is
|
|
4
|
+
[`regstack.RegStack`](#regstack.app.RegStack); everything else hangs
|
|
5
|
+
off it.
|
|
6
|
+
|
|
7
|
+
This page is organized by what you'd reach for, not by package
|
|
8
|
+
hierarchy. Each section starts with a one-paragraph orientation,
|
|
9
|
+
then the ``autoclass`` / ``autofunction`` directives pull the
|
|
10
|
+
docstrings, signatures, and parameter docs straight off the package
|
|
11
|
+
source.
|
|
12
|
+
|
|
13
|
+
## Top-level
|
|
14
|
+
|
|
15
|
+
The handful of things you import from `regstack` directly:
|
|
16
|
+
|
|
17
|
+
- [`RegStack`](#regstack.app.RegStack) — the embeddable façade.
|
|
18
|
+
- [`RegStackConfig`](#regstack.config.schema.RegStackConfig) — top-level config.
|
|
19
|
+
- [`EmailConfig`](#regstack.config.schema.EmailConfig) — email-backend sub-config.
|
|
20
|
+
- [`SmsConfig`](#regstack.config.schema.SmsConfig) — SMS-backend sub-config.
|
|
21
|
+
|
|
22
|
+
Most embeddings need only `RegStack` and `RegStackConfig`.
|
|
23
|
+
|
|
24
|
+
## Façade
|
|
25
|
+
|
|
26
|
+
`RegStack` is the embeddable façade. One per FastAPI application;
|
|
27
|
+
hosts mount its `router` and (optionally) `ui_router`. All collaborators
|
|
28
|
+
— password hasher, JWT codec, repos, hooks bus, email and SMS
|
|
29
|
+
services — hang off the instance.
|
|
30
|
+
|
|
31
|
+
```{eval-rst}
|
|
32
|
+
.. autoclass:: regstack.app.RegStack
|
|
33
|
+
:members:
|
|
34
|
+
:show-inheritance:
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
`RegStackConfig` is a `pydantic-settings` model loaded from
|
|
40
|
+
environment variables (`REGSTACK_*`), an optional
|
|
41
|
+
`regstack.secrets.env`, an optional `regstack.toml`, and programmatic
|
|
42
|
+
kwargs — in that priority order. See the
|
|
43
|
+
[Configuration guide](configuration.md) for every field with its
|
|
44
|
+
default.
|
|
45
|
+
|
|
46
|
+
```{eval-rst}
|
|
47
|
+
.. autoclass:: regstack.config.schema.RegStackConfig
|
|
48
|
+
:members:
|
|
49
|
+
:show-inheritance:
|
|
50
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
51
|
+
|
|
52
|
+
.. autoclass:: regstack.config.schema.EmailConfig
|
|
53
|
+
:members:
|
|
54
|
+
:show-inheritance:
|
|
55
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
56
|
+
|
|
57
|
+
.. autoclass:: regstack.config.schema.SmsConfig
|
|
58
|
+
:members:
|
|
59
|
+
:show-inheritance:
|
|
60
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
61
|
+
|
|
62
|
+
.. autofunction:: regstack.config.loader.load_config
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Auth primitives
|
|
66
|
+
|
|
67
|
+
The pieces that make authentication work: password hashing (Argon2id),
|
|
68
|
+
JWT issuance and validation with per-purpose derived keys, login
|
|
69
|
+
lockout, and the FastAPI dependency factory. Hosts rarely instantiate
|
|
70
|
+
these directly — they're built and wired by the `RegStack` constructor
|
|
71
|
+
— but the docstrings on the public methods explain the contract.
|
|
72
|
+
|
|
73
|
+
```{eval-rst}
|
|
74
|
+
.. autoclass:: regstack.auth.password.PasswordHasher
|
|
75
|
+
:members:
|
|
76
|
+
|
|
77
|
+
.. autoclass:: regstack.auth.jwt.JwtCodec
|
|
78
|
+
:members:
|
|
79
|
+
|
|
80
|
+
.. autoclass:: regstack.auth.jwt.TokenPayload
|
|
81
|
+
:members:
|
|
82
|
+
|
|
83
|
+
.. autoexception:: regstack.auth.jwt.TokenError
|
|
84
|
+
|
|
85
|
+
.. autofunction:: regstack.auth.jwt.is_payload_bulk_revoked
|
|
86
|
+
|
|
87
|
+
.. autoclass:: regstack.auth.lockout.LockoutService
|
|
88
|
+
:members:
|
|
89
|
+
|
|
90
|
+
.. autoclass:: regstack.auth.lockout.LockoutDecision
|
|
91
|
+
:members:
|
|
92
|
+
|
|
93
|
+
.. autoclass:: regstack.auth.dependencies.AuthDependencies
|
|
94
|
+
:members:
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Time
|
|
98
|
+
|
|
99
|
+
Every time-sensitive operation reads `now()` through a `Clock`. Tests
|
|
100
|
+
inject `FrozenClock` to make assertions deterministic.
|
|
101
|
+
|
|
102
|
+
```{eval-rst}
|
|
103
|
+
.. autoclass:: regstack.auth.clock.Clock
|
|
104
|
+
:members:
|
|
105
|
+
|
|
106
|
+
.. autoclass:: regstack.auth.clock.SystemClock
|
|
107
|
+
:members:
|
|
108
|
+
|
|
109
|
+
.. autoclass:: regstack.auth.clock.FrozenClock
|
|
110
|
+
:members:
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Backends
|
|
114
|
+
|
|
115
|
+
regstack ships three storage backends behind one set of `Protocol`
|
|
116
|
+
classes: SQLite (default, no infrastructure), PostgreSQL, and MongoDB.
|
|
117
|
+
The active backend is auto-built from `config.database_url`'s URL
|
|
118
|
+
scheme via `build_backend`. Hosts that need to share a connection pool
|
|
119
|
+
with their own application can pass an explicit `Backend` to the
|
|
120
|
+
`RegStack` constructor.
|
|
121
|
+
|
|
122
|
+
```{eval-rst}
|
|
123
|
+
.. autoclass:: regstack.backends.base.Backend
|
|
124
|
+
:members:
|
|
125
|
+
:show-inheritance:
|
|
126
|
+
|
|
127
|
+
.. autoclass:: regstack.backends.base.BackendKind
|
|
128
|
+
:members:
|
|
129
|
+
|
|
130
|
+
.. autofunction:: regstack.backends.factory.build_backend
|
|
131
|
+
|
|
132
|
+
.. autoclass:: regstack.backends.mongo.MongoBackend
|
|
133
|
+
:members:
|
|
134
|
+
:show-inheritance:
|
|
135
|
+
|
|
136
|
+
.. autoclass:: regstack.backends.sql.SqlBackend
|
|
137
|
+
:members:
|
|
138
|
+
:show-inheritance:
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Repository protocols
|
|
142
|
+
|
|
143
|
+
The five repository protocols are the contract every backend
|
|
144
|
+
implements. Routers and services depend only on these — switching
|
|
145
|
+
backends is a wiring change, not a code change.
|
|
146
|
+
|
|
147
|
+
```{eval-rst}
|
|
148
|
+
.. autoclass:: regstack.backends.protocols.UserRepoProtocol
|
|
149
|
+
:members:
|
|
150
|
+
|
|
151
|
+
.. autoclass:: regstack.backends.protocols.PendingRepoProtocol
|
|
152
|
+
:members:
|
|
153
|
+
|
|
154
|
+
.. autoclass:: regstack.backends.protocols.BlacklistRepoProtocol
|
|
155
|
+
:members:
|
|
156
|
+
|
|
157
|
+
.. autoclass:: regstack.backends.protocols.LoginAttemptRepoProtocol
|
|
158
|
+
:members:
|
|
159
|
+
|
|
160
|
+
.. autoclass:: regstack.backends.protocols.MfaCodeRepoProtocol
|
|
161
|
+
:members:
|
|
162
|
+
|
|
163
|
+
.. autoclass:: regstack.backends.protocols.MfaVerifyOutcome
|
|
164
|
+
:members:
|
|
165
|
+
|
|
166
|
+
.. autoclass:: regstack.backends.protocols.MfaVerifyResult
|
|
167
|
+
:members:
|
|
168
|
+
|
|
169
|
+
.. autoexception:: regstack.backends.protocols.UserAlreadyExistsError
|
|
170
|
+
|
|
171
|
+
.. autoexception:: regstack.backends.protocols.PendingAlreadyExistsError
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Models
|
|
175
|
+
|
|
176
|
+
The persisted data shapes. `BaseUser` is the canonical user document;
|
|
177
|
+
`UserCreate` validates registration input; `UserPublic` is what the
|
|
178
|
+
API returns (omits the password hash). The other models drive
|
|
179
|
+
verification, lockout, and SMS MFA.
|
|
180
|
+
|
|
181
|
+
```{eval-rst}
|
|
182
|
+
.. autoclass:: regstack.models.user.BaseUser
|
|
183
|
+
:members:
|
|
184
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
185
|
+
|
|
186
|
+
.. autoclass:: regstack.models.user.UserCreate
|
|
187
|
+
:members:
|
|
188
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
189
|
+
|
|
190
|
+
.. autoclass:: regstack.models.user.UserPublic
|
|
191
|
+
:members:
|
|
192
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
193
|
+
|
|
194
|
+
.. autoclass:: regstack.models.pending_registration.PendingRegistration
|
|
195
|
+
:members:
|
|
196
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
197
|
+
|
|
198
|
+
.. autoclass:: regstack.models.login_attempt.LoginAttempt
|
|
199
|
+
:members:
|
|
200
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
201
|
+
|
|
202
|
+
.. autoclass:: regstack.models.mfa_code.MfaCode
|
|
203
|
+
:members:
|
|
204
|
+
:exclude-members: model_config, model_fields, model_computed_fields
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Email + SMS
|
|
208
|
+
|
|
209
|
+
Pluggable transports for the verification / reset / change-email
|
|
210
|
+
emails and the SMS MFA codes. Implement `EmailService` or `SmsService`
|
|
211
|
+
to plug in a provider that isn't bundled (Postmark, SendGrid,
|
|
212
|
+
MessageBird, …) and pass the instance to `regstack.set_email_backend`
|
|
213
|
+
/ `set_sms_backend`.
|
|
214
|
+
|
|
215
|
+
### Email
|
|
216
|
+
|
|
217
|
+
```{eval-rst}
|
|
218
|
+
.. autoclass:: regstack.email.base.EmailMessage
|
|
219
|
+
:members:
|
|
220
|
+
|
|
221
|
+
.. autoclass:: regstack.email.base.EmailService
|
|
222
|
+
:members:
|
|
223
|
+
|
|
224
|
+
.. autoclass:: regstack.email.console.ConsoleEmailService
|
|
225
|
+
:members:
|
|
226
|
+
|
|
227
|
+
.. autoclass:: regstack.email.composer.MailComposer
|
|
228
|
+
:members:
|
|
229
|
+
|
|
230
|
+
.. autofunction:: regstack.email.factory.build_email_service
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### SMS
|
|
234
|
+
|
|
235
|
+
```{eval-rst}
|
|
236
|
+
.. autoclass:: regstack.sms.base.SmsMessage
|
|
237
|
+
:members:
|
|
238
|
+
|
|
239
|
+
.. autoclass:: regstack.sms.base.SmsService
|
|
240
|
+
:members:
|
|
241
|
+
|
|
242
|
+
.. autofunction:: regstack.sms.base.is_valid_e164
|
|
243
|
+
|
|
244
|
+
.. autoclass:: regstack.sms.null.NullSmsService
|
|
245
|
+
:members:
|
|
246
|
+
|
|
247
|
+
.. autofunction:: regstack.sms.factory.build_sms_service
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Hooks
|
|
251
|
+
|
|
252
|
+
The event bus regstack uses to fire side-effect notifications
|
|
253
|
+
(`user_registered`, `password_changed`, etc.) without coupling auth
|
|
254
|
+
code to host concerns like CRMs or analytics. See the
|
|
255
|
+
[Embedding guide](embedding.md#subscribing-to-events) for examples.
|
|
256
|
+
|
|
257
|
+
```{eval-rst}
|
|
258
|
+
.. autoclass:: regstack.hooks.events.HookRegistry
|
|
259
|
+
:members:
|
|
260
|
+
|
|
261
|
+
.. autodata:: regstack.hooks.events.KNOWN_EVENTS
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Routers
|
|
265
|
+
|
|
266
|
+
Hosts normally access these via the `regstack.router` and
|
|
267
|
+
`regstack.ui_router` properties; the builder functions are public for
|
|
268
|
+
hosts that want to compose differently.
|
|
269
|
+
|
|
270
|
+
```{eval-rst}
|
|
271
|
+
.. autofunction:: regstack.routers.build_router
|
|
272
|
+
|
|
273
|
+
.. autofunction:: regstack.ui.pages.build_ui_router
|
|
274
|
+
|
|
275
|
+
.. autofunction:: regstack.ui.pages.build_ui_environment
|
|
276
|
+
|
|
277
|
+
.. autofunction:: regstack.ui.pages.default_static_dir
|
|
278
|
+
```
|
|
@@ -3,6 +3,34 @@
|
|
|
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.3 — 2026-04-28
|
|
7
|
+
|
|
8
|
+
**Docs-only release.** API reference rewritten around the current
|
|
9
|
+
package layout, public surface gained proper Google-style docstrings.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- ``docs/api.md`` restructured around the post-multi-backend package
|
|
14
|
+
layout (``regstack.backends.{base,protocols,factory,mongo,sql}`` and
|
|
15
|
+
friends). Each section now opens with a one-paragraph orientation
|
|
16
|
+
before the autodoc directives. The pre-refactor
|
|
17
|
+
``regstack.db.repositories.*`` references that rendered empty are
|
|
18
|
+
gone.
|
|
19
|
+
- Added Google-style docstrings (purpose summary + Args / Returns /
|
|
20
|
+
Raises) to the most-touched public methods on ``RegStack``,
|
|
21
|
+
``JwtCodec``, ``PasswordHasher``, ``LockoutService``,
|
|
22
|
+
``AuthDependencies``, ``HookRegistry``, ``EmailService``,
|
|
23
|
+
``SmsService``, ``build_router``, ``build_ui_router``,
|
|
24
|
+
``build_ui_environment``, ``default_static_dir``, ``Clock`` /
|
|
25
|
+
``SystemClock`` / ``FrozenClock``.
|
|
26
|
+
- Dataclass field documentation moved to PEP 258 attribute docstrings
|
|
27
|
+
on ``TokenPayload``, ``LockoutDecision``, ``EmailMessage``,
|
|
28
|
+
``SmsMessage``, ``MfaVerifyResult`` — autodoc now renders each field
|
|
29
|
+
with its description without the "duplicate object description"
|
|
30
|
+
warnings the napoleon ``Attributes:`` block was triggering.
|
|
31
|
+
- ``MfaVerifyOutcome`` enum docstring reformatted as a bullet list
|
|
32
|
+
(the napoleon ``Members:`` block isn't a recognised section).
|
|
33
|
+
|
|
6
34
|
## 0.2.2 — 2026-04-28
|
|
7
35
|
|
|
8
36
|
**Docs-only release.**
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from fastapi.staticfiles import StaticFiles
|
|
7
|
+
|
|
8
|
+
from regstack.auth.clock import Clock, SystemClock
|
|
9
|
+
from regstack.auth.dependencies import AuthDependencies
|
|
10
|
+
from regstack.auth.jwt import JwtCodec
|
|
11
|
+
from regstack.auth.lockout import LockoutService
|
|
12
|
+
from regstack.auth.password import PasswordHasher
|
|
13
|
+
from regstack.backends.factory import build_backend
|
|
14
|
+
from regstack.config.schema import RegStackConfig
|
|
15
|
+
from regstack.email.base import EmailService
|
|
16
|
+
from regstack.email.composer import MailComposer
|
|
17
|
+
from regstack.email.factory import build_email_service
|
|
18
|
+
from regstack.hooks.events import HookRegistry
|
|
19
|
+
from regstack.models.user import BaseUser
|
|
20
|
+
from regstack.routers import build_router
|
|
21
|
+
from regstack.sms.base import SmsService
|
|
22
|
+
from regstack.sms.factory import build_sms_service
|
|
23
|
+
from regstack.ui.pages import build_ui_environment, build_ui_router, default_static_dir
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Awaitable, Callable
|
|
27
|
+
|
|
28
|
+
from fastapi import APIRouter
|
|
29
|
+
from jinja2 import Environment
|
|
30
|
+
|
|
31
|
+
from regstack.backends.base import Backend
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RegStack:
|
|
35
|
+
"""Embeddable account-management façade.
|
|
36
|
+
|
|
37
|
+
One ``RegStack`` is constructed per FastAPI application. The host
|
|
38
|
+
then mounts the JSON router (and optionally the SSR router) and
|
|
39
|
+
regstack owns user accounts, authentication, password reset, email
|
|
40
|
+
verification, and (optionally) SMS two-factor.
|
|
41
|
+
|
|
42
|
+
The persistence story is owned by a
|
|
43
|
+
:class:`~regstack.backends.base.Backend` selected by
|
|
44
|
+
``config.database_url``'s URL scheme:
|
|
45
|
+
|
|
46
|
+
- ``mongodb://`` / ``mongodb+srv://`` → MongoDB
|
|
47
|
+
- ``sqlite+aiosqlite://`` → SQLite
|
|
48
|
+
- ``postgresql+asyncpg://`` → PostgreSQL
|
|
49
|
+
|
|
50
|
+
Hosts that need to share a connection pool with their own code can
|
|
51
|
+
pass an explicit ``backend=`` argument and the URL is ignored.
|
|
52
|
+
|
|
53
|
+
Typical embed::
|
|
54
|
+
|
|
55
|
+
config = RegStackConfig.load()
|
|
56
|
+
regstack = RegStack(config=config)
|
|
57
|
+
|
|
58
|
+
@asynccontextmanager
|
|
59
|
+
async def lifespan(app):
|
|
60
|
+
await regstack.install_schema()
|
|
61
|
+
yield
|
|
62
|
+
await regstack.aclose()
|
|
63
|
+
|
|
64
|
+
app = FastAPI(lifespan=lifespan)
|
|
65
|
+
app.include_router(regstack.router, prefix=config.api_prefix)
|
|
66
|
+
|
|
67
|
+
Notable instance attributes (all set during ``__init__``):
|
|
68
|
+
|
|
69
|
+
- ``config`` — the loaded :class:`~regstack.config.schema.RegStackConfig`.
|
|
70
|
+
- ``clock`` — the injected :class:`~regstack.auth.clock.Clock`
|
|
71
|
+
(``SystemClock`` in production, ``FrozenClock`` in tests).
|
|
72
|
+
- ``backend`` — the active backend (Mongo / SQLite / Postgres).
|
|
73
|
+
- ``users``, ``pending``, ``blacklist``, ``attempts``, ``mfa_codes``
|
|
74
|
+
— repositories conforming to the protocols in
|
|
75
|
+
:mod:`regstack.backends.protocols`.
|
|
76
|
+
- ``password_hasher`` — Argon2id wrapper.
|
|
77
|
+
- ``jwt`` — :class:`~regstack.auth.jwt.JwtCodec` for session tokens.
|
|
78
|
+
- ``lockout`` — :class:`~regstack.auth.lockout.LockoutService`.
|
|
79
|
+
- ``email``, ``sms`` — the active transports.
|
|
80
|
+
- ``mail`` — the :class:`~regstack.email.composer.MailComposer`.
|
|
81
|
+
- ``hooks`` — the :class:`~regstack.hooks.events.HookRegistry`
|
|
82
|
+
event bus.
|
|
83
|
+
- ``deps`` — :class:`~regstack.auth.dependencies.AuthDependencies`
|
|
84
|
+
factory.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
config: RegStackConfig,
|
|
91
|
+
backend: Backend | None = None,
|
|
92
|
+
clock: Clock | None = None,
|
|
93
|
+
email_service: EmailService | None = None,
|
|
94
|
+
mail_composer: MailComposer | None = None,
|
|
95
|
+
sms_service: SmsService | None = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Construct the façade and wire its collaborators.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
config: Loaded configuration (see
|
|
101
|
+
:func:`~regstack.config.schema.RegStackConfig.load`).
|
|
102
|
+
backend: Optional pre-built backend. When ``None``, the
|
|
103
|
+
backend is built from ``config.database_url`` via
|
|
104
|
+
:func:`~regstack.backends.factory.build_backend`. Pass an
|
|
105
|
+
explicit backend if you want to share a connection pool
|
|
106
|
+
with the host application.
|
|
107
|
+
clock: Optional clock. Defaults to
|
|
108
|
+
:class:`~regstack.auth.clock.SystemClock`. Tests pass a
|
|
109
|
+
``FrozenClock`` to make timing-sensitive assertions
|
|
110
|
+
deterministic.
|
|
111
|
+
email_service: Optional pre-built email backend. When
|
|
112
|
+
``None``, one is built from ``config.email`` via
|
|
113
|
+
:func:`~regstack.email.factory.build_email_service`.
|
|
114
|
+
mail_composer: Optional pre-built mail composer. When
|
|
115
|
+
``None``, one is built from ``config.email`` and
|
|
116
|
+
``config.app_name``.
|
|
117
|
+
sms_service: Optional pre-built SMS backend. When ``None``,
|
|
118
|
+
one is built from ``config.sms`` via
|
|
119
|
+
:func:`~regstack.sms.factory.build_sms_service`.
|
|
120
|
+
"""
|
|
121
|
+
self.config = config
|
|
122
|
+
self.clock: Clock = clock or SystemClock()
|
|
123
|
+
self.backend: Backend = backend or build_backend(config, clock=self.clock)
|
|
124
|
+
self.password_hasher = PasswordHasher()
|
|
125
|
+
self.jwt = JwtCodec(config, self.clock)
|
|
126
|
+
|
|
127
|
+
# Repos come straight off the backend so they're always in sync
|
|
128
|
+
# with whatever implementation is configured.
|
|
129
|
+
self.users = self.backend.users
|
|
130
|
+
self.pending = self.backend.pending
|
|
131
|
+
self.blacklist = self.backend.blacklist
|
|
132
|
+
self.attempts = self.backend.attempts
|
|
133
|
+
self.mfa_codes = self.backend.mfa_codes
|
|
134
|
+
|
|
135
|
+
self.lockout = LockoutService(attempts=self.attempts, config=config, clock=self.clock)
|
|
136
|
+
self.email: EmailService = email_service or build_email_service(config.email)
|
|
137
|
+
self.sms: SmsService = sms_service or build_sms_service(config.sms)
|
|
138
|
+
self.mail = mail_composer or MailComposer(
|
|
139
|
+
email_config=config.email,
|
|
140
|
+
app_name=config.app_name,
|
|
141
|
+
)
|
|
142
|
+
self.hooks = HookRegistry()
|
|
143
|
+
self.deps = AuthDependencies(jwt=self.jwt, users=self.users, blacklist=self.blacklist)
|
|
144
|
+
self._template_dirs: list[Path] = list(config.extra_template_dirs)
|
|
145
|
+
self._ui_env: Environment | None = None
|
|
146
|
+
self._router: APIRouter | None = None
|
|
147
|
+
self._ui_router: APIRouter | None = None
|
|
148
|
+
self._static_files: StaticFiles | None = None
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def router(self) -> APIRouter:
|
|
152
|
+
"""The composite JSON ``APIRouter``.
|
|
153
|
+
|
|
154
|
+
Mount with
|
|
155
|
+
``app.include_router(regstack.router, prefix=config.api_prefix)``.
|
|
156
|
+
Includes ``register``, ``verify``, ``login``, ``logout``,
|
|
157
|
+
``account`` always; conditionally adds ``password``
|
|
158
|
+
(forgot/reset), ``phone`` + MFA, and ``admin`` based on
|
|
159
|
+
``config.enable_*`` flags.
|
|
160
|
+
|
|
161
|
+
Built lazily on first access.
|
|
162
|
+
"""
|
|
163
|
+
if self._router is None:
|
|
164
|
+
self._router = build_router(self)
|
|
165
|
+
return self._router
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def ui_env(self) -> Environment:
|
|
169
|
+
"""The Jinja2 environment that renders the SSR pages.
|
|
170
|
+
|
|
171
|
+
Built lazily on first access; rebuilt automatically after every
|
|
172
|
+
:meth:`add_template_dir` call so host overrides take effect.
|
|
173
|
+
"""
|
|
174
|
+
if self._ui_env is None:
|
|
175
|
+
self._ui_env = build_ui_environment(self._template_dirs)
|
|
176
|
+
return self._ui_env
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def ui_router(self) -> APIRouter:
|
|
180
|
+
"""The SSR ``APIRouter`` for the bundled HTML pages.
|
|
181
|
+
|
|
182
|
+
Mount with ``app.include_router(regstack.ui_router,
|
|
183
|
+
prefix=config.ui_prefix)``. Only meaningful when
|
|
184
|
+
``config.enable_ui_router=True`` — building it on a host that
|
|
185
|
+
won't mount it is harmless but pointless.
|
|
186
|
+
"""
|
|
187
|
+
if self._ui_router is None:
|
|
188
|
+
self._ui_router = build_ui_router(self)
|
|
189
|
+
return self._ui_router
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def static_files(self) -> StaticFiles:
|
|
193
|
+
"""Bundled CSS / JS as a Starlette ``StaticFiles`` app.
|
|
194
|
+
|
|
195
|
+
Mount with
|
|
196
|
+
``app.mount(config.static_prefix, regstack.static_files)``.
|
|
197
|
+
Serves ``core.css``, the default ``theme.css``, and
|
|
198
|
+
``regstack.js`` — the assets the SSR pages link to.
|
|
199
|
+
"""
|
|
200
|
+
if self._static_files is None:
|
|
201
|
+
self._static_files = StaticFiles(directory=str(default_static_dir()))
|
|
202
|
+
return self._static_files
|
|
203
|
+
|
|
204
|
+
# --- Lifecycle -------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
async def install_schema(self) -> None:
|
|
207
|
+
"""Bring the database schema to head — idempotent.
|
|
208
|
+
|
|
209
|
+
On Mongo this means ensuring every required index exists. On
|
|
210
|
+
SQL backends it runs Alembic migrations to head, which creates
|
|
211
|
+
tables on a fresh database and applies any new revisions on an
|
|
212
|
+
existing one. Safe to call on every application boot.
|
|
213
|
+
"""
|
|
214
|
+
await self.backend.install_schema()
|
|
215
|
+
|
|
216
|
+
async def install_indexes(self) -> None:
|
|
217
|
+
"""Backwards-compatible alias for :meth:`install_schema`.
|
|
218
|
+
|
|
219
|
+
Kept for the 0.1.x ``install_indexes()`` name. New callers
|
|
220
|
+
should use :meth:`install_schema`.
|
|
221
|
+
"""
|
|
222
|
+
await self.install_schema()
|
|
223
|
+
|
|
224
|
+
async def aclose(self) -> None:
|
|
225
|
+
"""Tear down the backend's connection pool.
|
|
226
|
+
|
|
227
|
+
Call from your FastAPI lifespan teardown so background
|
|
228
|
+
connections are closed cleanly when the application shuts down.
|
|
229
|
+
"""
|
|
230
|
+
await self.backend.aclose()
|
|
231
|
+
|
|
232
|
+
async def bootstrap_admin(self, email: str, password: str) -> BaseUser:
|
|
233
|
+
"""Create or promote a verified superuser. Idempotent.
|
|
234
|
+
|
|
235
|
+
If a user with this email already exists, they are promoted to
|
|
236
|
+
``is_superuser=True`` if they weren't already (their password is
|
|
237
|
+
not changed). Otherwise a new active, verified, superuser
|
|
238
|
+
account is created with the given password.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
email: The admin's email address. Must be valid for the
|
|
242
|
+
user model's ``email`` validator.
|
|
243
|
+
password: The plaintext password to hash and store on a
|
|
244
|
+
newly-created admin. Ignored when promoting an existing
|
|
245
|
+
user.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The persisted (and now-superuser) :class:`~regstack.models.user.BaseUser`.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
UserAlreadyExistsError: If the create path races against
|
|
252
|
+
another writer for the same email.
|
|
253
|
+
"""
|
|
254
|
+
existing = await self.users.get_by_email(email)
|
|
255
|
+
if existing is not None:
|
|
256
|
+
if not existing.is_superuser:
|
|
257
|
+
assert existing.id is not None
|
|
258
|
+
await self.users.set_superuser(existing.id, is_superuser=True)
|
|
259
|
+
existing.is_superuser = True
|
|
260
|
+
return existing
|
|
261
|
+
user = BaseUser(
|
|
262
|
+
email=email,
|
|
263
|
+
hashed_password=self.password_hasher.hash(password),
|
|
264
|
+
is_active=True,
|
|
265
|
+
is_verified=True,
|
|
266
|
+
is_superuser=True,
|
|
267
|
+
)
|
|
268
|
+
return await self.users.create(user)
|
|
269
|
+
|
|
270
|
+
# --- Extension surface ------------------------------------------------
|
|
271
|
+
|
|
272
|
+
def set_email_backend(self, service: EmailService) -> None:
|
|
273
|
+
"""Replace the active email backend at runtime.
|
|
274
|
+
|
|
275
|
+
Useful for hosts that want a backend not in the bundled set
|
|
276
|
+
(Postmark, SendGrid, MessageBird, …). See
|
|
277
|
+
:class:`~regstack.email.base.EmailService` for the contract.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
service: An :class:`EmailService` implementation.
|
|
281
|
+
"""
|
|
282
|
+
self.email = service
|
|
283
|
+
|
|
284
|
+
def set_sms_backend(self, service: SmsService) -> None:
|
|
285
|
+
"""Replace the active SMS backend at runtime.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
service: A :class:`~regstack.sms.base.SmsService` implementation.
|
|
289
|
+
"""
|
|
290
|
+
self.sms = service
|
|
291
|
+
|
|
292
|
+
def add_template_dir(self, path: str | Path) -> None:
|
|
293
|
+
"""Prepend a host template directory to the override chain.
|
|
294
|
+
|
|
295
|
+
Host templates win over regstack defaults via Jinja2's
|
|
296
|
+
``ChoiceLoader`` for **both** the email composer and the SSR UI
|
|
297
|
+
pages. To override the verification email, drop a
|
|
298
|
+
``verification.html`` file in the directory; to override the
|
|
299
|
+
login page, drop ``auth/login.html``.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
path: Filesystem directory to search before regstack's
|
|
303
|
+
bundled templates. Must exist when templates are
|
|
304
|
+
rendered.
|
|
305
|
+
"""
|
|
306
|
+
path_obj = Path(path)
|
|
307
|
+
self.mail.add_template_dir(path_obj)
|
|
308
|
+
if path_obj not in self._template_dirs:
|
|
309
|
+
self._template_dirs.insert(0, path_obj)
|
|
310
|
+
# Force the UI environment to rebuild on next access so the new
|
|
311
|
+
# directory takes effect even if the env was already touched.
|
|
312
|
+
self._ui_env = None
|
|
313
|
+
|
|
314
|
+
def on(self, event: str, handler: Callable[..., Awaitable[None] | None]) -> None:
|
|
315
|
+
"""Register an event handler. Sync and async handlers both work.
|
|
316
|
+
|
|
317
|
+
Forwards to :meth:`HookRegistry.on
|
|
318
|
+
<regstack.hooks.events.HookRegistry.on>`. Handlers fire
|
|
319
|
+
concurrently when an event happens; exceptions are logged but
|
|
320
|
+
never break the primary auth flow. See
|
|
321
|
+
:data:`~regstack.hooks.events.KNOWN_EVENTS` for the set of
|
|
322
|
+
events regstack itself fires.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
event: The event name (e.g. ``"user_registered"``,
|
|
326
|
+
``"password_changed"``).
|
|
327
|
+
handler: A callable invoked with the event's keyword
|
|
328
|
+
arguments. Can be sync or async.
|
|
329
|
+
"""
|
|
330
|
+
self.hooks.on(event, handler)
|