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 ¶
- type CredentialRepository
- type HouseholdRepository
- type MFARepository
- func (r *MFARepository) BeginEnrollment(ctx context.Context, memberID domain.MemberID, householdID domain.HouseholdID, ...) error
- func (r *MFARepository) ConfirmEnrollmentWithCodes(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID, ...) error
- func (r *MFARepository) DeleteEnrollment(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID) error
- func (r *MFARepository) GetEnrollment(ctx context.Context, memberID domain.MemberID) (*domain.MFAEnrollment, error)
- func (r *MFARepository) ListUnusedRecoveryCodes(ctx context.Context, memberID domain.MemberID) ([]domain.RecoveryCode, error)
- func (r *MFARepository) MarkRecoveryCodeUsed(ctx context.Context, codeID domain.RecoveryCodeID) error
- func (r *MFARepository) RecordLoginStep(ctx context.Context, memberID domain.MemberID, step int64) error
- type MemberRepository
- func (r *MemberRepository) CreateMember(ctx context.Context, m *domain.Member) error
- func (r *MemberRepository) GetMember(ctx context.Context, id domain.MemberID) (*domain.Member, error)
- func (r *MemberRepository) ListMembers(ctx context.Context, householdID domain.HouseholdID) ([]*domain.Member, error)
- func (r *MemberRepository) SetMemberActive(ctx context.Context, householdID domain.HouseholdID, id domain.MemberID, ...) error
- func (r *MemberRepository) UpdateMemberProfile(ctx context.Context, householdID domain.HouseholdID, id domain.MemberID, ...) error
- type WebAuthnCredentialRepository
- func (r *WebAuthnCredentialRepository) Create(ctx context.Context, householdID domain.HouseholdID, ...) error
- func (r *WebAuthnCredentialRepository) Delete(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID, ...) error
- func (r *WebAuthnCredentialRepository) FindByUserHandle(ctx context.Context, handle []byte) (domain.MemberID, []domain.WebAuthnCredential, error)
- func (r *WebAuthnCredentialRepository) ListByMember(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID) ([]domain.WebAuthnCredential, error)
- func (r *WebAuthnCredentialRepository) Rename(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID, ...) error
- func (r *WebAuthnCredentialRepository) UpdateAfterAssertion(ctx context.Context, credentialID []byte, signCount uint32, usedAt time.Time) error
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 ¶
CreateHousehold inserts a household and populates its timestamps.
func (*HouseholdRepository) GetHousehold ¶
func (r *HouseholdRepository) GetHousehold(ctx context.Context, id domain.HouseholdID) (*domain.Household, error)
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 ¶
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
func (r *WebAuthnCredentialRepository) Create(ctx context.Context, householdID domain.HouseholdID, cred *domain.WebAuthnCredential) error
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
func (r *WebAuthnCredentialRepository) Delete(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID, id domain.WebAuthnCredentialID) error
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
func (r *WebAuthnCredentialRepository) FindByUserHandle(ctx context.Context, handle []byte) (domain.MemberID, []domain.WebAuthnCredential, error)
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
func (r *WebAuthnCredentialRepository) ListByMember(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID) ([]domain.WebAuthnCredential, error)
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
func (r *WebAuthnCredentialRepository) Rename(ctx context.Context, householdID domain.HouseholdID, memberID domain.MemberID, id domain.WebAuthnCredentialID, nickname string) error
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.