236 lines
8.4 KiB
Markdown
236 lines
8.4 KiB
Markdown
# SQLite Repository Implementation — Design & Session State
|
|
|
|
> **For Claude:** This document captures the full project state and in-progress design decisions
|
|
> for the SQLite repository implementation. Resume from "Next Steps" section.
|
|
|
|
## Project State (as of 2026-02-12)
|
|
|
|
### Git
|
|
|
|
- **Branch:** `main` (10 commits, no feature branches)
|
|
- **HEAD:** `9d7a67b fix: add collision retry for userid generation`
|
|
- **Working tree:** Clean (untracked: `.idea/`, `README.md`, `docs/`)
|
|
- **All 17 tests passing**, all quality checks green
|
|
|
|
### What Exists
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `src/fastapi_oidc_op/config.py` | `Settings(BaseSettings)` with `env_prefix="OIDC_OP_"`, `StorageBackend` enum (sqlite/mongodb), `sqlite_path` default `data/oidc_op.db` |
|
|
| `src/fastapi_oidc_op/models.py` | `User`, `WebAuthnCredential`, `PasswordCredential`, `MagicLink`, `CredentialType` enum. Datetimes use `datetime.UTC`. `MagicLink.expires_at` is required (no default). |
|
|
| `src/fastapi_oidc_op/store/protocols.py` | `@runtime_checkable` Protocol interfaces: `UserRepository`, `CredentialRepository`, `MagicLinkRepository` |
|
|
| `src/fastapi_oidc_op/userid.py` | `generate_userid()` (pure, proquint 32-bit), `generate_unique_userid(user_repo, max_retries=10)` (async, collision retry) |
|
|
| `src/fastapi_oidc_op/app.py` | `create_app(settings)` factory. `/health` endpoint. Settings on `app.state`. Docs only in debug mode. |
|
|
| `tests/conftest.py` | `settings` fixture, async `client` fixture via httpx `ASGITransport` |
|
|
| `scripts/check.sh` | Quality gate: ruff format, ruff check, ty check, pytest |
|
|
| `pyproject.toml` | Python 3.13, hatchling build, src layout. Deps: fastapi, idpyoidc, pydantic-settings, fido2, argon2-cffi, motor, aiosqlite, proquint, httpx. Dev: ruff, ty, pytest, pytest-asyncio. |
|
|
|
|
### Package Structure
|
|
|
|
```
|
|
src/fastapi_oidc_op/
|
|
├── __init__.py
|
|
├── app.py
|
|
├── config.py
|
|
├── models.py
|
|
├── userid.py
|
|
├── oidc/__init__.py
|
|
├── authn/__init__.py
|
|
├── manage/__init__.py
|
|
├── invite/__init__.py
|
|
└── store/
|
|
├── __init__.py
|
|
├── protocols.py
|
|
├── mongodb/__init__.py
|
|
└── sqlite/__init__.py
|
|
```
|
|
|
|
### Existing Protocol Signatures (must implement exactly)
|
|
|
|
```python
|
|
class UserRepository(Protocol):
|
|
async def create(self, user: User) -> User
|
|
async def get_by_userid(self, userid: str) -> User | None
|
|
async def get_by_username(self, username: str) -> User | None
|
|
async def update(self, user: User) -> User
|
|
async def list_users(self, offset: int = 0, limit: int = 100) -> list[User]
|
|
async def delete(self, userid: str) -> bool
|
|
|
|
class CredentialRepository(Protocol):
|
|
async def create_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential
|
|
async def create_password(self, credential: PasswordCredential) -> PasswordCredential
|
|
async def get_webauthn_by_user(self, user_id: str) -> list[WebAuthnCredential]
|
|
async def get_webauthn_by_credential_id(self, credential_id: bytes) -> WebAuthnCredential | None
|
|
async def get_password_by_user(self, user_id: str) -> PasswordCredential | None
|
|
async def update_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential
|
|
async def delete_webauthn(self, user_id: str, credential_id: bytes) -> bool
|
|
async def delete_password(self, user_id: str) -> bool
|
|
|
|
class MagicLinkRepository(Protocol):
|
|
async def create(self, link: MagicLink) -> MagicLink
|
|
async def get_by_token(self, token: str) -> MagicLink | None
|
|
async def mark_used(self, token: str) -> bool
|
|
async def delete_expired(self) -> int
|
|
```
|
|
|
|
### Design Document
|
|
|
|
Full design doc at `docs/plans/2026-02-11-oidc-op-design.md`. Key points:
|
|
- SQLite uses `aiosqlite` with raw SQL
|
|
- Storage backend selected at startup via config
|
|
- SQLite is the default test backend (no external deps)
|
|
|
|
### Known Quirks
|
|
|
|
- **ty 0.0.16:** `python-version` goes under `[tool.ty.environment]`, no `src` field at top level
|
|
- **ruff 0.15:** version command is `ruff version` not `ruff --version`
|
|
- **User.userid** (no underscore) vs **credential.user_id** (underscore) — intentional, matches design doc
|
|
|
|
---
|
|
|
|
## Design Decisions (confirmed by user)
|
|
|
|
### 1. Schema Initialization: SQL Migration Files
|
|
|
|
- Directory: `src/fastapi_oidc_op/store/sqlite/migrations/`
|
|
- Numbered SQL files: `001_initial.sql`, `002_...`, etc.
|
|
- Tracker table `_migrations` records applied files
|
|
- Migration runner at startup: read files, skip applied, execute in order, record
|
|
|
|
### 2. Connection Strategy: Single Shared Connection
|
|
|
|
- One `aiosqlite` connection created at startup, shared by all repositories
|
|
- WAL mode enabled for concurrent reads
|
|
- Foreign keys enabled (`PRAGMA foreign_keys = ON`)
|
|
- Connection closed on shutdown
|
|
|
|
### 3. Dependency Injection: Lifespan + app.state
|
|
|
|
- App lifespan (`@asynccontextmanager`) creates DB connection and repositories
|
|
- Stored on `app.state` (e.g. `app.state.user_repo`)
|
|
- FastAPI `Depends()` retrieves from `request.app.state`
|
|
|
|
### 4. Groups Storage: Separate Join Table
|
|
|
|
- `user_groups(userid TEXT, group_name TEXT)` with composite PK
|
|
- `ON DELETE CASCADE` from users table
|
|
- Repository handles join/split when reading/writing User models
|
|
|
|
### 5. Bytes Columns: BLOB
|
|
|
|
- `credential_id` and `public_key` stored as BLOB
|
|
- aiosqlite handles bytes natively
|
|
|
|
### 6. Datetimes: ISO 8601 TEXT
|
|
|
|
- All datetime columns stored as ISO 8601 text strings
|
|
- Converted to/from `datetime` objects in the repository layer
|
|
|
|
### 7. Booleans: INTEGER 0/1
|
|
|
|
- SQLite has no native bool; use INTEGER with 0/1
|
|
|
|
---
|
|
|
|
## Schema (approved — Section 1)
|
|
|
|
```sql
|
|
-- _migrations tracker
|
|
CREATE TABLE IF NOT EXISTS _migrations (
|
|
id INTEGER PRIMARY KEY,
|
|
filename TEXT NOT NULL UNIQUE,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- 001_initial.sql
|
|
CREATE TABLE users (
|
|
userid TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL UNIQUE,
|
|
preferred_username TEXT,
|
|
given_name TEXT,
|
|
family_name TEXT,
|
|
nickname TEXT,
|
|
email TEXT,
|
|
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
phone_number TEXT,
|
|
phone_number_verified INTEGER NOT NULL DEFAULT 0,
|
|
picture TEXT,
|
|
locale TEXT,
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE user_groups (
|
|
userid TEXT NOT NULL REFERENCES users(userid) ON DELETE CASCADE,
|
|
group_name TEXT NOT NULL,
|
|
PRIMARY KEY (userid, group_name)
|
|
);
|
|
|
|
CREATE TABLE webauthn_credentials (
|
|
user_id TEXT NOT NULL REFERENCES users(userid) ON DELETE CASCADE,
|
|
credential_id BLOB NOT NULL,
|
|
public_key BLOB NOT NULL,
|
|
sign_count INTEGER NOT NULL DEFAULT 0,
|
|
device_name TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (user_id, credential_id)
|
|
);
|
|
|
|
CREATE TABLE password_credentials (
|
|
user_id TEXT PRIMARY KEY REFERENCES users(userid) ON DELETE CASCADE,
|
|
password_hash TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE magic_links (
|
|
token TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
used INTEGER NOT NULL DEFAULT 0,
|
|
created_by TEXT,
|
|
note TEXT
|
|
);
|
|
```
|
|
|
|
**Status:** Schema approved. Implementation complete.
|
|
|
|
---
|
|
|
|
## Implementation Status
|
|
|
|
All SQLite repository tasks are **complete**. See `docs/plans/2026-02-13-sqlite-repositories-plan.md` for the full implementation plan.
|
|
|
|
### Completed (commits `bfa5b2e` through `a45604f`)
|
|
|
|
1. SQL migration file (`001_initial.sql`)
|
|
2. Migration runner (`run_migrations()` with `_migrations` tracker table)
|
|
3. `DuplicateError` domain exception
|
|
4. `SQLiteUserRepository` with tests
|
|
5. `SQLiteCredentialRepository` with tests
|
|
6. `SQLiteMagicLinkRepository` with tests
|
|
7. Lifespan integration and dependency injection (`dependencies.py`)
|
|
|
|
### Quality Gate
|
|
|
|
All 86 tests passing. `./scripts/check.sh` all green (ruff format, ruff check, ty check, pytest).
|
|
|
|
### Roadmap
|
|
|
|
Per the design doc, the roadmap is:
|
|
1. ~~Scaffolding~~ (done)
|
|
2. ~~SQLite repositories~~ (done)
|
|
3. ~~Authentication services~~ (done) — PasswordService, WebAuthnService, MagicLinkService
|
|
4. **Authentication routes** (login/register endpoints + templates) — next phase
|
|
5. OIDC provider integration (idpyoidc)
|
|
6. Management UI (OIDC RP that dogfoods the OP)
|
|
|
|
---
|
|
|
|
## Process Notes
|
|
|
|
- User prefers to be asked design questions before implementation begins
|
|
- Use **brainstorming** skill: one question at a time, present design in 200-300 word sections, validate each
|
|
- Use **subagent-driven development** for implementation
|
|
- Plans live in `docs/plans/`
|
|
- Quality gate: `./scripts/check.sh` (ruff format, ruff check, ty check, pytest)
|