docs: update sqlite design doc to reflect completed implementation
This commit is contained in:
parent
a45604ff2f
commit
e543fe2229
1 changed files with 235 additions and 0 deletions
235
docs/plans/2026-02-12-sqlite-repositories-design.md
Normal file
235
docs/plans/2026-02-12-sqlite-repositories-design.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# 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 64 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** (WebAuthn + password) — next phase
|
||||
4. OIDC provider integration (idpyoidc)
|
||||
5. 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue