Metadata-Version: 2.4
Name: hawkapi-users
Version: 0.1.0
Summary: User lifecycle for HawkAPI — register, login, password reset, email verification, on top of hawkapi-auth + hawkapi-sqlalchemy + hawkapi-mail
Project-URL: Homepage, https://pypi.org/project/hawkapi-users/
Project-URL: Repository, https://github.com/ashimov/hawkapi-users
Project-URL: Issues, https://github.com/ashimov/hawkapi-users/issues
Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
License: MIT License
        
        Copyright (c) 2026 HawkAPI Contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: authentication,email-verification,hawkapi,password-reset,users
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: hawkapi-auth>=0.2.0
Requires-Dist: hawkapi-mail>=0.2.0
Requires-Dist: hawkapi-sqlalchemy>=0.2.0
Requires-Dist: hawkapi>=0.1.7
Requires-Dist: jinja2>=3.1
Requires-Dist: sqlalchemy[asyncio]>=2.0
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
Requires-Dist: pyright>=1.1; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# hawkapi-users

Full user lifecycle for [HawkAPI](https://github.com/ashimov/HawkAPI). Register, login, email verification, password reset — all on top of three existing plugins:

- [`hawkapi-auth`](https://pypi.org/project/hawkapi-auth/) — argon2id passwords, JWT token issuer
- [`hawkapi-sqlalchemy`](https://pypi.org/project/hawkapi-sqlalchemy/) — async SQLAlchemy sessions
- [`hawkapi-mail`](https://pypi.org/project/hawkapi-mail/) — transactional email backends + templates

No `fastapi-users` dependency; pure stdlib + the three plugins above.

## Install

```bash
pip install hawkapi-users
```

## Quickstart

```python
from hawkapi import HawkAPI
from hawkapi_sqlalchemy import Base, init_database
from hawkapi_mail import init_mail, SMTPBackend, SMTPConfig
from hawkapi_users import (
    SQLAlchemyBaseUserTable,
    UserManager, UserTokens, UserTokenConfig,
    init_users, send_verification_email, send_password_reset_email,
)


class User(SQLAlchemyBaseUserTable):
    __tablename__ = "users"


app = HawkAPI()
init_database(app, url="postgresql+asyncpg://...")
mail = init_mail(
    app,
    backend=SMTPBackend(SMTPConfig(host="smtp.example.com", port=587, start_tls=True)),
    default_sender="noreply@example.com",
)

tokens = UserTokens(UserTokenConfig(secret="…stable secret, ≥32 chars…"))
manager = UserManager(model=User, tokens=tokens)


async def on_request_verify(user, token):
    await send_verification_email(
        mail, to=user.email, token=token,
        verify_url_template="https://app.example.com/verify/{token}",
    )


async def on_forgot_password(user, token):
    await send_password_reset_email(
        mail, to=user.email, token=token,
        reset_url_template="https://app.example.com/password-reset/{token}",
    )


manager.on_after_request_verify = on_request_verify
manager.on_after_forgot_password = on_forgot_password

init_users(app, manager=manager)
```

The plugin mounts under `/users` (configurable):

| Route                                    | Description                                  |
|------------------------------------------|----------------------------------------------|
| `POST /users/register`                   | Create account, returns public user record   |
| `POST /users/login`                      | Verify credentials, returns public user      |
| `POST /users/verify/request`             | Send a verification email (always 202)       |
| `POST /users/verify/{token}`             | Apply a verification token                   |
| `POST /users/password-reset/request`     | Send a reset email (always 202)              |
| `POST /users/password-reset/{token}`     | Apply a new password                         |

## Security defaults

- **Argon2id** password hashing (via `hawkapi-auth`'s pinned parameters).
- **Token type binding** — verify tokens cannot be replayed as reset tokens (the `type` claim is enforced).
- **password_version** binding — each token carries the user's current `password_version`. A successful reset bumps the counter, invalidating every outstanding verify/reset token in one DB write.
- **Account enumeration prevention** — `/verify/request` and `/password-reset/request` always return 202 regardless of whether the email exists.
- **Timing-safe lookup** — `authenticate()` runs argon2id against a dummy hash when the user does not exist, equalizing wall-clock time across the hit/miss paths.
- **Email normalization** — `email` is lower-cased and stripped server-side; uniqueness is enforced after normalization.

## Known accepted risks

These are real tradeoffs the plugin does NOT mitigate. Operators must layer additional protection where it matters.

- **Email enumeration via `/register` (409 response)** — registering with an already-used email returns `409 Conflict`. An attacker can confirm whether any email has an account. Mitigation: rate-limit `/register` per source IP and require CAPTCHA for unauthenticated traffic. The anti-enumeration guarantee applies only to `/verify/request` and `/password-reset/request` (always 202).
- **Inactive-account 403 confirms credentials** — `/login` returns `401` for bad credentials and `403` for a disabled account. A `403` confirms the email+password combination is correct. Treat `403` as a credential-leak event in audit pipelines.
- **No rate limiting** — `/login`, `/register`, `/verify/request`, `/password-reset/request` have no built-in throttling. Pair with `hawkapi-ratelimit` plus per-account lockout.
- **No session issued by `/login`** — the route validates credentials and returns the public user record. Issuing a JWT, setting a cookie, or starting a server-side session is the operator's job (`manager.on_after_login`).
- **Token-in-URL leakage** — verify/reset tokens travel in URL paths and may appear in Referer headers, browser history, and email-server access logs. Use HTTPS + short TTLs (default 1 hour) and prefer SPA flows that strip the token from the URL after consumption.

## Hooks

```python
manager.on_after_register          # async (user) -> None
manager.on_after_login             # async (user) -> None
manager.on_after_request_verify    # async (user, token) -> None
manager.on_after_verify            # async (user) -> None
manager.on_after_forgot_password   # async (user, token) -> None
manager.on_after_reset_password    # async (user) -> None
```

Use these to mint a session cookie, write an audit log, send the email, etc.

## Custom email templates

```python
from hawkapi_mail import TemplateRenderer
from hawkapi_users import send_verification_email

renderer = TemplateRenderer(directory="my/email/templates")

await send_verification_email(
    mail,
    to=user.email,
    token=token,
    template="my_verify.html",
    text_template="my_verify.txt",
    renderer=renderer,
)
```

The built-in templates live in `hawkapi_users/templates/` (verify.html/.txt, password_reset.html/.txt) and are used by default.

## Development

```bash
git clone https://github.com/ashimov/hawkapi-users.git
cd hawkapi-users
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run pyright src/
```

## License

MIT.
