adapter

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package adapter contains the identity bounded context's outbound adapters: pgx-backed implementations of the ports declared in identity/domain, run against the schema identity/migrate owns.

Every query schema-qualifies identity. explicitly rather than relying on search_path resolution, because callers connect with their own app's search path (see identity/migrate's package doc).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CredentialRepository

type CredentialRepository struct {
	// contains filtered or unexported fields
}

CredentialRepository is the pgx-backed implementation of domain.CredentialRepository, backed by identity.member's own email and password_hash columns (see identity/migrate's baseline migration, not a separate credential table).

func NewCredentialRepository

func NewCredentialRepository(dbtx db.TX) *CredentialRepository

NewCredentialRepository constructs the repository with an injected query executor. See HouseholdRepository's constructor doc for why the executor is a db.TX rather than a concrete pool type.

func (*CredentialRepository) FindByEmail

func (r *CredentialRepository) FindByEmail(ctx context.Context, email string) (*domain.Credential, error)

FindByEmail returns the Credential for the given email address, or domain.ErrInvalidCredentials when no active member with that email and a non-null password_hash exists (preventing user enumeration). A deactivated member (identity.member.active = false) is excluded: login is one of the readers identity/migrate's package doc names for that flag.

func (*CredentialRepository) SetCredential

func (r *CredentialRepository) SetCredential(ctx context.Context, memberID domain.MemberID, email, passwordHash string) error

SetCredential stores (or replaces) the email and password hash on the member row identified by memberID. Returns domain.ErrMemberNotFound when the member does not exist, and domain.ErrEmailAlreadyInUse when the email is already assigned to another member.

type HouseholdRepository

type HouseholdRepository struct {
	// contains filtered or unexported fields
}

HouseholdRepository is the pgx-backed implementation of domain.HouseholdRepository. UUIDs are passed and scanned as text, matching the identity schema's existing adapter convention (no pgx UUID codec registration).

func NewHouseholdRepository

func NewHouseholdRepository(dbtx db.TX) *HouseholdRepository

NewHouseholdRepository constructs the repository with an injected query executor. The executor is a db.TX, satisfied by both *pgxpool.Pool (the default composition) and pgx.Tx (so a caller can run this repository inside its own transaction); the same methods work against either.

func (*HouseholdRepository) CreateHousehold

func (r *HouseholdRepository) CreateHousehold(ctx context.Context, h *domain.Household) error

CreateHousehold inserts a household and populates its timestamps.

func (*HouseholdRepository) GetHousehold

GetHousehold returns the household, or domain.ErrHouseholdNotFound.

type MFARepository added in v0.3.0

type MFARepository struct {
	// contains filtered or unexported fields
}

MFARepository is the pgx-backed domain.MFARepository. UUIDs are passed and scanned as text, mirroring CredentialRepository's and MemberRepository's convention (no pgx UUID codec registration).

func NewMFARepository added in v0.3.0

func NewMFARepository(dbtx db.TX) *MFARepository

NewMFARepository constructs the repository with an injected query executor (a db.TX, satisfied by both *pgxpool.Pool and pgx.Tx). Panics if dbtx cannot also begin a transaction (mfaTxBeginner) — both beginEnrollmentOnce and ConfirmEnrollmentWithCodes need one, and failing here at construction is far more convenient than the first time a member enrolls or confirms.

func (*MFARepository) BeginEnrollment added in v0.3.0

func (r *MFARepository) BeginEnrollment(ctx context.Context, memberID domain.MemberID, householdID domain.HouseholdID, secretEnc []byte) error

BeginEnrollment upserts an unconfirmed enrollment for memberID, retrying beginEnrollmentOnce a single time if the first attempt loses a concurrent first-time-enrollment race (see that method's own doc for why SELECT ... FOR UPDATE cannot protect against it) — the retry re-runs against the now-existing row, which the FOR UPDATE lock path resolves normally.

Returns domain.ErrMFAAlreadyEnrolled when the existing row is already CONFIRMED, and domain.ErrMemberNotFound both for a genuinely unknown member/household (FK violation on insert) and when an existing row belongs to a DIFFERENT household than householdID — a defense-in-depth tenant guard: this method must never overwrite another household's pending secret, and reports both cases identically so neither leaks which one occurred.

func (*MFARepository) ConfirmEnrollmentWithCodes added in v0.3.0

func (r *MFARepository) ConfirmEnrollmentWithCodes(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID, recoveryCodeHashes []string) error

ConfirmEnrollmentWithCodes atomically confirms memberID's enrollment and replaces their recovery codes with one fresh row per hash, in a single transaction that locks the row (SELECT ... FOR UPDATE) before confirming — see the port doc for why this must be one atomic operation: it is what makes two concurrent callers racing to confirm the SAME still-unconfirmed enrollment resolve to exactly one winner, with the loser's hashes never persisted at all.

func (*MFARepository) DeleteEnrollment added in v0.3.0

func (r *MFARepository) DeleteEnrollment(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID) error

DeleteEnrollment removes memberID's enrollment (confirmed or not), cascading its recovery codes, scoped to householdID as a defense-in-depth tenant check. Returns domain.ErrMFANotEnrolled when no row exists in that household.

func (*MFARepository) GetEnrollment added in v0.3.0

func (r *MFARepository) GetEnrollment(ctx context.Context, memberID domain.MemberID) (*domain.MFAEnrollment, error)

GetEnrollment returns memberID's enrollment (confirmed or not), or domain.ErrMFANotEnrolled when no row exists.

func (*MFARepository) ListUnusedRecoveryCodes added in v0.3.0

func (r *MFARepository) ListUnusedRecoveryCodes(ctx context.Context, memberID domain.MemberID) ([]domain.RecoveryCode, error)

ListUnusedRecoveryCodes returns every not-yet-used recovery code for memberID, oldest first — ties on created_at (every code in one batch shares the same transaction-local now() value) broken deterministically by id, mirroring WebAuthnCredentialRepository.ListByMember's own tiebreaker.

func (*MFARepository) MarkRecoveryCodeUsed added in v0.3.0

func (r *MFARepository) MarkRecoveryCodeUsed(ctx context.Context, codeID domain.RecoveryCodeID) error

MarkRecoveryCodeUsed sets used_at = now() on codeID.

func (*MFARepository) RecordLoginStep added in v0.3.0

func (r *MFARepository) RecordLoginStep(ctx context.Context, memberID domain.MemberID, step int64) error

RecordLoginStep durably persists step as memberID's last-accepted login TOTP step, guarded by a single conditional UPDATE so the check-and-set is atomic against a concurrent racing call: the WHERE clause only matches a row whose last_totp_step is still NULL or strictly less than step, closing the same race BeginEnrollment's own doc discusses (two concurrent login attempts must never both "win" and accept the same or an out-of-order step).

type MemberRepository

type MemberRepository struct {
	// contains filtered or unexported fields
}

MemberRepository is the pgx-backed implementation of domain.MemberRepository.

func NewMemberRepository

func NewMemberRepository(dbtx db.TX) *MemberRepository

NewMemberRepository constructs the repository with an injected query executor. See HouseholdRepository's constructor doc for why the executor is a db.TX rather than a concrete pool type. Panics if dbtx cannot also begin a transaction (memberTxBeginner) — UpdateMemberProfile and SetMemberActive both need one, mirroring MFARepository's mfaTxBeginner requirement (mfa_postgres.go) for the same reason: failing here at construction is far more convenient than the first time a caller renames or deactivates a member.

func (*MemberRepository) CreateMember

func (r *MemberRepository) CreateMember(ctx context.Context, m *domain.Member) error

CreateMember inserts a member, returning domain.ErrDuplicateMember when the display name collides (case-insensitively) within the household, and domain.ErrHouseholdNotFound when m.HouseholdID does not exist.

func (*MemberRepository) GetMember

func (r *MemberRepository) GetMember(ctx context.Context, id domain.MemberID) (*domain.Member, error)

GetMember returns the member, or domain.ErrMemberNotFound.

func (*MemberRepository) ListMembers

func (r *MemberRepository) ListMembers(ctx context.Context, householdID domain.HouseholdID) ([]*domain.Member, error)

ListMembers returns the household's members ordered by creation. An unknown household yields an empty slice, not an error.

func (*MemberRepository) SetMemberActive added in v0.3.0

func (r *MemberRepository) SetMemberActive(ctx context.Context, householdID domain.HouseholdID, id domain.MemberID, active bool) error

SetMemberActive deactivates or reactivates a member, subject to the last-active-owner guard documented on domain.MemberWriter. Reactivation (active true) is never refused: the guard fragment's leading "$3 = false" clause is false in that case, so the surrounding NOT(...) is unconditionally true and the row updates.

func (*MemberRepository) UpdateMemberProfile added in v0.3.0

func (r *MemberRepository) UpdateMemberProfile(ctx context.Context, householdID domain.HouseholdID, id domain.MemberID, displayName string, role domain.Role) error

UpdateMemberProfile renames a member and/or changes their role, subject to the last-active-owner guard documented on domain.MemberWriter.

type WebAuthnCredentialRepository added in v0.3.0

type WebAuthnCredentialRepository struct {
	// contains filtered or unexported fields
}

WebAuthnCredentialRepository is the pgx-backed domain.WebAuthnCredentialRepository. UUIDs are passed and scanned as text, mirroring MFARepository's and CredentialRepository's convention (no pgx UUID codec registration) — except AAGUID, which pgx's native uuid.UUID support scans directly since it is never used as a lookup key, only stored/returned opaquely.

func NewWebAuthnCredentialRepository added in v0.3.0

func NewWebAuthnCredentialRepository(dbtx db.TX) *WebAuthnCredentialRepository

NewWebAuthnCredentialRepository constructs the repository with an injected query executor (a db.TX, satisfied by both *pgxpool.Pool and pgx.Tx).

func (*WebAuthnCredentialRepository) Create added in v0.3.0

Create persists a newly registered credential. Returns domain.ErrHouseholdNotFound when householdID does not exist, domain.ErrMemberNotFound when cred.MemberID does not belong to householdID (FK violations), and domain.ErrWebAuthnCredentialExists when cred.CredentialID is already registered (unique violation).

func (*WebAuthnCredentialRepository) Delete added in v0.3.0

Delete removes the credential identified by id, scoped to memberID and householdID. Returns domain.ErrWebAuthnCredentialNotFound when no row matches all three.

func (*WebAuthnCredentialRepository) FindByUserHandle added in v0.3.0

FindByUserHandle resolves handle to its owning member and every one of that member's registered credentials — the usernameless login lookup, driven by member_credential_user_handle_idx. Returns domain.ErrMemberNotFound when handle matches no row.

func (*WebAuthnCredentialRepository) ListByMember added in v0.3.0

ListByMember returns every credential registered by memberID within householdID, oldest first — ties on created_at (two credentials registered within the same clock tick) broken deterministically by id, its own tie-break secondary key. A member with none, or a memberID belonging to a different household than householdID, returns an empty slice, never an error.

func (*WebAuthnCredentialRepository) Rename added in v0.3.0

Rename updates the nickname on the credential identified by id, scoped to memberID and householdID. Returns domain.ErrWebAuthnCredentialNotFound when no row matches all three.

func (*WebAuthnCredentialRepository) UpdateAfterAssertion added in v0.3.0

func (r *WebAuthnCredentialRepository) UpdateAfterAssertion(ctx context.Context, credentialID []byte, signCount uint32, usedAt time.Time) error

UpdateAfterAssertion persists the authenticator's new signature counter and last-used timestamp for the credential identified by its raw WebAuthn credential id (credential_id, globally unique — not this row's own WebAuthnCredentialID) — but ONLY when usedAt is not older than whatever last_used_at is already on file (NULL, on a credential's first assertion, always qualifies). This is a monotonic guard against a race between two concurrently successful assertions on the SAME credential, mirroring MFARepository.RecordLoginStep's own last_totp_step guard, though the caller contract differs (see below).

Returns domain.ErrWebAuthnCredentialNotFound only when credentialID matches no row AT ALL. When the row exists but the guard skipped the write (a newer assertion already won the race), this returns nil, NOT an error: the assertion that reaches this method has ALREADY been cryptographically verified by WebAuthnService before this call — losing the bookkeeping race here must never fail an otherwise-valid login or step-up, only skip a write that would have regressed stored state backward in time.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL