Metadata-Version: 2.4
Name: fastapi-auth-admin
Version: 0.2.0
Summary: Standalone FastAPI JWT auth with admin-approved registration, password management, and extensible token claims.
Project-URL: Homepage, https://github.com/tedlaz/fastapi-auth-admin
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: aiosqlite
Requires-Dist: bcrypt
Requires-Dist: fastapi
Requires-Dist: pydantic[email]
Requires-Dist: pyjwt
Requires-Dist: python-multipart
Requires-Dist: sqlalchemy>=2.0
Description-Content-Type: text/markdown

# fastapi-auth-admin

Standalone JWT authentication package for FastAPI applications with admin-approved registration, password management, and extensible token claims (e.g. multi-tenant `company_id`).

## Installation

```bash
pip install fastapi-auth-admin
```

Or with uv:

```bash
uv add fastapi-auth-admin
```

All dependencies (`fastapi`, `sqlalchemy`, `aiosqlite`, `bcrypt`, `pyjwt`, `pydantic[email]`, `python-multipart`) are installed automatically.

## Features

- One-call setup — `setup_auth(app)` does everything
- Fully async — built on SQLAlchemy async + aiosqlite
- Email/password registration and login
- Admin-approved user registration workflow
- JWT tokens with extensible arbitrary claims
- Password change (authenticated, requires current password)
- Async SQLAlchemy user repository (dedicated `users.db` database)
- Auto-seeded admin user on first run
- Swagger UI `/docs` compatible (OAuth2 password flow)

## Quick start

Two touch points — that's it:

**1. Entry point** — one call to set up auth:

```python
from fastapi import FastAPI
from fastapi_auth import setup_auth

app = FastAPI()
setup_auth(app)
```

This single call:
- Configures JWT security
- Registers an async startup handler that initializes the `users.db` SQLite database and seeds the default admin user
- Registers all `/auth/*` endpoints on your app

**2. Endpoints** — add `RequireLogin` where needed:

```python
from fastapi_auth import RequireLogin

@router.get("/orders")
async def list_orders(user: RequireLogin):
    ...
```

Run with:

```bash
uvicorn myapp:app --reload
```

### setup_auth options

```python
setup_auth(
    app,
    secret_key="your-secret-key-min-32-bytes-long!",  # JWT secret (random per process unless set; use a fixed key in production)
    algorithm="HS256",                                  # JWT algorithm (default)
    expire_minutes=30,                                  # token TTL (default)
    db_url="sqlite+aiosqlite:///./users.db",            # async database URL (default)
)
```

Any async SQLAlchemy-compatible URL works:

```python
setup_auth(app, db_url="sqlite+aiosqlite:///./data/auth.db")           # custom SQLite path
setup_auth(app, db_url="postgresql+asyncpg://user:pass@host/mydb")     # PostgreSQL (requires asyncpg)
```

## Endpoints

| Method | Path                    | Auth   | Description                      |
|--------|-------------------------|--------|----------------------------------|
| POST   | `/auth/register`        | public | Register a new user (pending)    |
| POST   | `/auth/login`           | public | Login, returns JWT (OAuth2 form) |
| GET    | `/auth/me`              | login  | Current user info + token claims |
| POST   | `/auth/change-password` | login  | Change own password              |
| GET    | `/auth/pending`         | admin  | List unapproved users            |
| POST   | `/auth/approve`         | admin  | Approve a user by email          |
| POST   | `/auth/reject`          | admin  | Reject a user by email           |

## Default admin

On first startup the adapter auto-creates an admin user:

- **Email:** `admin@admin.com`
- **Password:** `changeme`

Change the password immediately after first login via `/auth/change-password`.

## Protecting endpoints

Add `RequireLogin` as a parameter to any endpoint to require a valid JWT:

```python
from fastapi import APIRouter
from fastapi_auth import RequireLogin

router = APIRouter()

@router.get("/dashboard")
async def dashboard(user: RequireLogin):
    print(user.sub)                        # user email
    print(user.extra.get("is_admin"))      # True/False
```

If you don't need user info in the function body, it still enforces authentication just by being declared as a parameter.

## Multi-tenant / custom token claims

The JWT token supports arbitrary extra claims. Call the `login` use case directly with `extra_claims`:

```python
from fastapi_auth.use_cases import login

token = await login(
    email="user@example.com",
    password="secret",
    repo=repo,
    extra_claims={"company_id": 42, "role": "manager"},
)
```

These claims are available in any protected endpoint:

```python
@router.get("/tenant-data")
async def tenant_data(user: RequireLogin):
    company_id = user.extra.get("company_id")
    role = user.extra.get("role")
```

## Custom user repository

Implement the `UserRepositoryPort` protocol to use any async storage backend:

```python
from fastapi_auth import UserRepositoryPort, User, set_get_user_repo

class MyUserRepository:
    async def find_by_email(self, email: str) -> User | None: ...
    async def create(self, user: User) -> None: ...
    async def update(self, user: User) -> None: ...
    async def list_pending(self) -> list[User]: ...
    async def ensure_admin(self, email: str, hashed_password: str) -> None: ...

async def get_user_repo():
    yield MyUserRepository()

set_get_user_repo(get_user_repo)
```

## Testing

The test suite uses `pytest-asyncio` and `httpx`. Each test gets an isolated in-memory database:

```bash
uv sync --group dev
uv run pytest
```

To add the dev dependencies to your own project:

```bash
uv add --group dev pytest pytest-asyncio httpx
```

Configure `pytest-asyncio` in `pyproject.toml`:

```toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```

Use `httpx.AsyncClient` with `ASGITransport` to test endpoints without a running server:

```python
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from fastapi_auth import auth_router, configure_security, set_get_user_repo
from fastapi_auth.sqlalchemy_adapter import get_user_repo, init_db

@pytest.fixture
async def client():
    configure_security(secret_key="a-secret-key-long-enough-for-hs256")
    await init_db("sqlite+aiosqlite:///:memory:")
    set_get_user_repo(get_user_repo)
    app = FastAPI()
    app.include_router(auth_router)
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
        yield ac
```

## Package structure

```
fastapi_auth/
  __init__.py            # setup_auth() + public API exports
  models.py              # User, TokenPayload dataclasses
  ports.py               # UserRepositoryPort protocol (async)
  errors.py              # AuthError, InvalidCredentials, NotAuthorized, ...
  security.py            # bcrypt password hashing, JWT encode/decode, configure()
  use_cases.py           # async: register, login, approve, reject, change_password, list_pending
  dependencies.py        # FastAPI deps: get_current_user, RequireLogin
  router.py              # APIRouter with all auth endpoints (async handlers)
  sqlalchemy_adapter.py  # Async SQLAlchemy + aiosqlite user repository (default, recommended)
  json_adapter.py        # JSON file user repository (dev/prototyping only)
```

## Workflow

1. Admin logs in with default credentials
2. Users register via `POST /auth/register` (created as unapproved)
3. Admin views pending users via `GET /auth/pending`
4. Admin approves (`POST /auth/approve`) or rejects (`POST /auth/reject`)
5. Approved users can log in and receive a JWT
6. Users change their own password via `POST /auth/change-password`
