diff --git a/src/fastapi_oidc_op/models.py b/src/fastapi_oidc_op/models.py new file mode 100644 index 0000000..732121f --- /dev/null +++ b/src/fastapi_oidc_op/models.py @@ -0,0 +1,62 @@ +from datetime import UTC, datetime, timedelta +from enum import StrEnum + +from pydantic import BaseModel, Field + + +def _utcnow() -> datetime: + return datetime.now(UTC) + + +def _default_expiry() -> datetime: + return datetime.now(UTC) + timedelta(hours=24) + + +class CredentialType(StrEnum): + WEBAUTHN = "webauthn" + PASSWORD = "password" + + +class User(BaseModel): + userid: str + username: str + preferred_username: str | None = None + given_name: str | None = None + family_name: str | None = None + nickname: str | None = None + email: str | None = None + email_verified: bool = False + phone_number: str | None = None + phone_number_verified: bool = False + picture: str | None = None + locale: str | None = None + active: bool = True + created_at: datetime = Field(default_factory=_utcnow) + updated_at: datetime = Field(default_factory=_utcnow) + groups: list[str] = Field(default_factory=list) + + +class WebAuthnCredential(BaseModel): + user_id: str + type: CredentialType = CredentialType.WEBAUTHN + credential_id: bytes + public_key: bytes + sign_count: int = 0 + device_name: str = "" + created_at: datetime = Field(default_factory=_utcnow) + + +class PasswordCredential(BaseModel): + user_id: str + type: CredentialType = CredentialType.PASSWORD + password_hash: str + created_at: datetime = Field(default_factory=_utcnow) + + +class MagicLink(BaseModel): + token: str + username: str + expires_at: datetime = Field(default_factory=_default_expiry) + used: bool = False + created_by: str | None = None + note: str | None = None diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..1562d6c --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,79 @@ +from datetime import UTC, datetime + +from fastapi_oidc_op.models import ( + CredentialType, + MagicLink, + PasswordCredential, + User, + WebAuthnCredential, +) + + +def test_user_creation() -> None: + user = User( + userid="lusab-bansen", + username="alice", + ) + assert user.userid == "lusab-bansen" + assert user.username == "alice" + assert user.preferred_username is None + assert user.email is None + assert user.active is True + assert user.groups == [] + assert user.created_at is not None + assert user.updated_at is not None + + +def test_user_with_all_fields() -> None: + user = User( + userid="lusab-bansen", + username="alice", + preferred_username="Alice W.", + given_name="Alice", + family_name="Wonderland", + nickname="ally", + email="alice@example.com", + email_verified=True, + phone_number="+1234567890", + phone_number_verified=False, + picture="https://example.com/alice.jpg", + locale="en-US", + active=True, + groups=["admin", "users"], + ) + assert user.given_name == "Alice" + assert user.groups == ["admin", "users"] + + +def test_webauthn_credential() -> None: + cred = WebAuthnCredential( + user_id="lusab-bansen", + credential_id=b"\x01\x02\x03", + public_key=b"\x04\x05\x06", + sign_count=0, + device_name="YubiKey 5", + ) + assert cred.type == CredentialType.WEBAUTHN + assert cred.credential_id == b"\x01\x02\x03" + assert cred.device_name == "YubiKey 5" + + +def test_password_credential() -> None: + cred = PasswordCredential( + user_id="lusab-bansen", + password_hash="$argon2id$v=19$m=65536,t=3,p=4$hash", + ) + assert cred.type == CredentialType.PASSWORD + assert cred.password_hash.startswith("$argon2") + + +def test_magic_link() -> None: + link = MagicLink( + token="abc123def456", + username="newuser", + ) + assert link.token == "abc123def456" + assert link.username == "newuser" + assert link.used is False + assert link.created_by is None + assert link.expires_at > datetime.now(UTC)