store

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package store defines the persistence ports that authit's service packages depend on. authit assumes no specific database: a host application supplies a concrete implementation of the interfaces in this package (Postgres, SQLite, in-memory, ...). The memstore package ships a reference in-memory implementation of every interface here, suitable for tests and small apps.

authit ships no DDL: your schema, naming and migrations are yours. It does ship a reference one. schema.sql in the repository root is a complete, non-binding table set for every interface here, annotated with the places where the column set is not guessable from the struct definitions -- and sqlbstore/example_test.go applies it and runs the real flows over it, so it is checked rather than merely asserted. Start there rather than reverse-engineering the columns one type at a time.

Index

Constants

This section is empty.

Variables

View Source
var ErrConflict = errors.New("authit/store: conflict")

ErrConflict is returned when a create would violate a uniqueness constraint (e.g. an email or slug that already exists).

View Source
var ErrNotFound = errors.New("authit/store: not found")

ErrNotFound is returned by lookup methods when no matching record exists.

Functions

This section is empty.

Types

type DeviceAuthorization

type DeviceAuthorization struct {
	ID              string
	DeviceCodeHash  string
	UserCode        string
	ClientID        string
	Scope           string
	Status          DeviceAuthorizationStatus
	UserID          *string // set once Status is Approved
	ExpiresAt       time.Time
	IntervalSeconds int
	LastPolledAt    *time.Time
	CreatedAt       time.Time
}

DeviceAuthorization is one in-flight RFC 8628 device-authorization-grant request. UserCode is stored as plaintext (not hashed): it is short and low-entropy by design — the security property comes from rate-limiting guesses at the approval endpoint (the host application's job), not from the code being a secret. DeviceCode, by contrast, IS a secret (the CLI's poll credential) and only its hash is persisted.

type DeviceAuthorizationStatus

type DeviceAuthorizationStatus string

DeviceAuthorizationStatus is the lifecycle state of a DeviceAuthorization.

const (
	DeviceAuthorizationPending  DeviceAuthorizationStatus = "pending"
	DeviceAuthorizationApproved DeviceAuthorizationStatus = "approved"
	DeviceAuthorizationDenied   DeviceAuthorizationStatus = "denied"
)

type DeviceAuthorizationStore

type DeviceAuthorizationStore interface {
	CreateDeviceAuthorization(ctx context.Context, d *DeviceAuthorization) error
	GetDeviceAuthorizationByDeviceCodeHash(ctx context.Context, hash string) (*DeviceAuthorization, error)
	GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*DeviceAuthorization, error)
	UpdateDeviceAuthorization(ctx context.Context, d *DeviceAuthorization) error
	DeleteDeviceAuthorization(ctx context.Context, id string) error
}

DeviceAuthorizationStore persists DeviceAuthorization records.

type EmailVerificationStore

type EmailVerificationStore interface {
	CreateEmailVerificationToken(ctx context.Context, t *EmailVerificationToken) error
	GetEmailVerificationTokenByHash(ctx context.Context, hash string) (*EmailVerificationToken, error)
	MarkEmailVerificationTokenUsed(ctx context.Context, id string) error
	DeleteUserEmailVerificationTokens(ctx context.Context, userID string) error
}

EmailVerificationStore persists email verification tokens.

type EmailVerificationToken

type EmailVerificationToken struct {
	ID        string
	UserID    string
	TokenHash string
	ExpiresAt time.Time
	UsedAt    *time.Time
	CreatedAt time.Time
}

EmailVerificationToken is a single-use, time-limited token e-mailed to a user to prove ownership of their address. Only its hash is persisted.

type FailedLoginAttempt

type FailedLoginAttempt struct {
	ID        string
	Email     string
	IPAddress string
	CreatedAt time.Time
}

FailedLoginAttempt records one bad login attempt, keyed by email so lockout can be checked before a matching user is even confirmed to exist (this avoids leaking account existence through timing/behavior).

type Invitation

type Invitation struct {
	ID          string
	TeamID      string
	Email       string
	TokenHash   string
	Role        Role
	Status      InvitationStatus
	InvitedByID string
	ExpiresAt   time.Time
	AcceptedAt  *time.Time
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

Invitation represents an offer for an email address to join a Team with a given Role. Only the token's hash is persisted; the raw token is handed back to the caller once, at creation or resend.

type InvitationStatus

type InvitationStatus string

InvitationStatus is the lifecycle state of an Invitation. There is deliberately no "expired" status: expiry is derived from ExpiresAt at read time rather than written back.

const (
	InvitationPending  InvitationStatus = "pending"
	InvitationAccepted InvitationStatus = "accepted"
	InvitationRevoked  InvitationStatus = "revoked"
)

type InvitationStore

type InvitationStore interface {
	CreateInvitation(ctx context.Context, i *Invitation) error
	GetInvitation(ctx context.Context, id string) (*Invitation, error)
	GetInvitationByTokenHash(ctx context.Context, hash string) (*Invitation, error)
	ListInvitationsByTeam(ctx context.Context, teamID string) ([]*Invitation, error)
	UpdateInvitation(ctx context.Context, i *Invitation) error
}

InvitationStore persists Invitation records.

type LockoutStore

type LockoutStore interface {
	RecordFailedLoginAttempt(ctx context.Context, a *FailedLoginAttempt) error
	CountRecentFailedLoginAttempts(ctx context.Context, email string, since time.Time) (int, error)
	ClearFailedLoginAttempts(ctx context.Context, email string) error
	LockAccount(ctx context.Context, userID string) error
	IsAccountLocked(ctx context.Context, userID string) (bool, error)
	UnlockAccount(ctx context.Context, userID string) error
}

LockoutStore tracks failed logins and account lockout state.

It needs TWO tables, which is not visible from the types above. One holds FailedLoginAttempt rows (keyed by email, per that type's doc). The second holds the set of currently-locked accounts, and has no canonical authit type at all: LockAccount, IsAccountLocked and UnlockAccount are insert/exists/delete over a set of user ids, not CRUD over a struct, so its shape is entirely yours. All authit requires is that its user-id column is UNIQUE, so locking an already-locked account is idempotent rather than an error.

Implementing only the attempts table compiles cleanly and fails at runtime. See schema.sql (`account_locks`).

type Member

type Member struct {
	ID          string
	TeamID      string
	UserID      *string
	Role        Role
	DisplayName string
	Email       string
	IsActive    bool
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

Member is the join between a User and a Team, carrying the role that governs authorization within that team. UserID is nullable so a team can track a member (e.g. pending an invitation, or a login-less contact) before or without a linked User.

type MemberStore

type MemberStore interface {
	CreateMember(ctx context.Context, m *Member) error
	GetMember(ctx context.Context, id string) (*Member, error)
	GetMemberByUserAndTeam(ctx context.Context, userID, teamID string) (*Member, error)
	ListMembersByTeam(ctx context.Context, teamID string) ([]*Member, error)
	ListMembershipsByUser(ctx context.Context, userID string) ([]*Member, error)
	UpdateMember(ctx context.Context, m *Member) error
	DeleteMember(ctx context.Context, id string) error
}

MemberStore persists Member records.

type PasswordResetStore

type PasswordResetStore interface {
	CreatePasswordResetToken(ctx context.Context, t *PasswordResetToken) error
	GetPasswordResetTokenByHash(ctx context.Context, hash string) (*PasswordResetToken, error)
	MarkPasswordResetTokenUsed(ctx context.Context, id string) error
	DeleteUserPasswordResetTokens(ctx context.Context, userID string) error
}

PasswordResetStore persists password reset tokens.

type PasswordResetToken

type PasswordResetToken struct {
	ID        string
	UserID    string
	TokenHash string
	ExpiresAt time.Time
	UsedAt    *time.Time
	CreatedAt time.Time
}

PasswordResetToken is a single-use, time-limited token e-mailed to a user who requested a password reset. Only its hash is persisted.

type PendingTwoFactorSession

type PendingTwoFactorSession struct {
	ID        string
	UserID    string
	TokenHash string
	ExpiresAt time.Time
	CreatedAt time.Time
}

PendingTwoFactorSession is the short-lived token issued after a correct password when the account has TOTP enabled; it must be exchanged for a real session by presenting a valid TOTP or backup code.

type PendingTwoFactorStore

type PendingTwoFactorStore interface {
	CreatePendingTwoFactorSession(ctx context.Context, s *PendingTwoFactorSession) error
	GetPendingTwoFactorSessionByHash(ctx context.Context, hash string) (*PendingTwoFactorSession, error)
	DeletePendingTwoFactorSession(ctx context.Context, id string) error
}

PendingTwoFactorStore persists pending 2FA sessions.

type PersonalAccessToken

type PersonalAccessToken struct {
	ID         string
	UserID     string
	Name       string
	TokenHash  string
	Scopes     []string
	ExpiresAt  *time.Time
	LastUsedAt *time.Time
	RevokedAt  *time.Time
	CreatedAt  time.Time
}

PersonalAccessToken is a long-lived, named, scoped bearer credential a user creates for themselves — for a CLI, a script, or any caller that isn't going through an interactive login. Unlike RefreshToken, it is not paired with a short-lived access token: the raw value itself is the bearer credential, verified on every request via its hash.

type PersonalAccessTokenStore

type PersonalAccessTokenStore interface {
	CreatePersonalAccessToken(ctx context.Context, t *PersonalAccessToken) error
	GetPersonalAccessToken(ctx context.Context, id string) (*PersonalAccessToken, error)
	GetPersonalAccessTokenByHash(ctx context.Context, hash string) (*PersonalAccessToken, error)
	ListPersonalAccessTokensByUser(ctx context.Context, userID string) ([]*PersonalAccessToken, error)
	UpdatePersonalAccessToken(ctx context.Context, t *PersonalAccessToken) error
}

PersonalAccessTokenStore persists PersonalAccessToken records.

type RefreshToken

type RefreshToken struct {
	ID        string
	UserID    string
	TokenHash string
	ExpiresAt time.Time
	RevokedAt *time.Time
	UserAgent string
	IPAddress string
	CreatedAt time.Time
}

RefreshToken is a server-side session record. The raw token is only ever returned to the caller once, at issuance; only TokenHash is persisted.

type RefreshTokenStore

type RefreshTokenStore interface {
	CreateRefreshToken(ctx context.Context, t *RefreshToken) error
	GetRefreshTokenByHash(ctx context.Context, hash string) (*RefreshToken, error)
	RevokeRefreshToken(ctx context.Context, id string) error
	RevokeAllUserRefreshTokens(ctx context.Context, userID string) error
	ListActiveRefreshTokens(ctx context.Context, userID string) ([]*RefreshToken, error)
}

RefreshTokenStore persists refresh tokens, which double as the list of a user's active sessions.

type Role

type Role string

Role is a member's permission level within a team. The three-tier default below is a starting point, not a closed set — host applications may store and check any string value they like; authit's team package only assigns special meaning to RoleOwner (last-owner protections).

Roles are per-team by design, and that is a real limit rather than a gap waiting to be filled. A Role only exists attached to a Member, and a Member only exists attached to a Team, so there is no way to express a principal whose identity spans teams — a platform-level auditor, a consultant, a support engineer, a coach working across many client organizations. Such an identity belongs in your own model, joined to authit by user id, not squeezed in here: the workarounds (a synthetic team everyone joins, or a membership row per team) both break as soon as the principal needs to reach a team it holds no membership in.

The split that survives: authit answers "who is this", your model answers "what may they do". See the team package's doc comment.

const (
	RoleOwner  Role = "owner"
	RoleAdmin  Role = "admin"
	RoleMember Role = "member"
)

type Superuser

type Superuser struct {
	ID           string
	Email        string
	PasswordHash string
	DisplayName  string
	IsActive     bool
	CreatedBy    *string
	LastLoginAt  *time.Time
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

Superuser is an operator identity, deliberately unrelated to Team/Member roles: it has no organization and no role field. The only way to create one is through the superuser package's API (never exposed over a public registration endpoint), so a compromised user-facing flow can never mint one.

type SuperuserRefreshToken

type SuperuserRefreshToken struct {
	ID          string
	SuperuserID string
	TokenHash   string
	ExpiresAt   time.Time
	RevokedAt   *time.Time
	UserAgent   string
	IPAddress   string
	CreatedAt   time.Time
}

SuperuserRefreshToken is the admin-plane equivalent of RefreshToken, kept in its own store/table so a leaked user-session store dump can't be replayed as an admin session.

type SuperuserRefreshTokenStore

type SuperuserRefreshTokenStore interface {
	CreateSuperuserRefreshToken(ctx context.Context, t *SuperuserRefreshToken) error
	GetSuperuserRefreshTokenByHash(ctx context.Context, hash string) (*SuperuserRefreshToken, error)
	RevokeSuperuserRefreshToken(ctx context.Context, id string) error
	RevokeAllSuperuserRefreshTokens(ctx context.Context, superuserID string) error
}

SuperuserRefreshTokenStore persists admin-plane refresh tokens.

type SuperuserStore

type SuperuserStore interface {
	CreateSuperuser(ctx context.Context, s *Superuser) error
	GetSuperuserByID(ctx context.Context, id string) (*Superuser, error)
	GetSuperuserByEmail(ctx context.Context, email string) (*Superuser, error)
	ListSuperusers(ctx context.Context) ([]*Superuser, error)
	UpdateSuperuser(ctx context.Context, s *Superuser) error
	CountSuperusers(ctx context.Context) (int, error)
}

SuperuserStore persists Superuser records.

type TOTPSettings

type TOTPSettings struct {
	ID                 string
	UserID             string
	SecretEncrypted    []byte
	Enabled            bool
	VerifiedAt         *time.Time
	RecoveryCodeHashes []string
	RecoveryCodesUsed  int
	CreatedAt          time.Time
	UpdatedAt          time.Time
}

TOTPSettings holds a user's TOTP secret (encrypted at rest by the caller before it reaches the store) and backup codes (hashed).

The field names are worth reading before writing the table: the obvious guesses (`confirmed`, `backup_codes`) are not what this type has. Enabled is the on/off flag, VerifiedAt records when enrollment was confirmed, and the backup codes are RecoveryCodeHashes plus a RecoveryCodesUsed counter.

RecoveryCodeHashes is a []string with no single obvious storage: a Postgres text[], a join table and a JSON column are all defensible, and the choice is yours -- see schema.sql, which uses text[].

type TOTPStore

type TOTPStore interface {
	CreateTOTPSettings(ctx context.Context, t *TOTPSettings) error
	GetTOTPSettingsByUserID(ctx context.Context, userID string) (*TOTPSettings, error)
	UpdateTOTPSettings(ctx context.Context, t *TOTPSettings) error
	DeleteTOTPSettings(ctx context.Context, userID string) error
}

TOTPStore persists TOTP enrollment state.

type Team

type Team struct {
	ID        string
	Name      string
	Slug      string
	OwnerID   string
	CreatedAt time.Time
	UpdatedAt time.Time
}

Team is an organization/tenant that users belong to via Member records.

type TeamStore

type TeamStore interface {
	CreateTeam(ctx context.Context, t *Team) error
	GetTeam(ctx context.Context, id string) (*Team, error)
	GetTeamBySlug(ctx context.Context, slug string) (*Team, error)
	UpdateTeam(ctx context.Context, t *Team) error
	DeleteTeam(ctx context.Context, id string) error
}

TeamStore persists Team records.

type User

type User struct {
	ID              string
	Email           string
	PasswordHash    string
	EmailVerified   bool
	EmailVerifiedAt *time.Time
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

User is a login-capable identity. Host applications are free to store additional profile fields elsewhere and join on ID; authit only needs the fields below to authenticate.

type UserStore

type UserStore interface {
	CreateUser(ctx context.Context, u *User) error
	GetUserByID(ctx context.Context, id string) (*User, error)
	GetUserByEmail(ctx context.Context, email string) (*User, error)
	UpdateUser(ctx context.Context, u *User) error
}

UserStore persists User records.

Jump to

Keyboard shortcuts

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