Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Postgres ¶
type Postgres struct {
// contains filtered or unexported fields
}
func (*Postgres) Create ¶
Create inserts a complete session into the table. The caller is responsible for filling in every field, including a unique ID and a last_used_at value (typically set to created_at on initial issue).
Single-active-per-identity is enforced by soft-revoking any existing active row for the same (identity_type, identity_key) in the same transaction. The reason recorded on the old row is:
- "replaced" if it was still within its refresh window
- "expired" if it had already aged past it
revoked_at follows the reason: "replaced" rows get the current time (the session was actively killed now), "expired" rows get their own expires_at (the session was provably unused after that point — any later validate would have failed and any later refresh would have moved expires_at forward, so a row that aged out to refresh_until without being refreshed cannot have been used past expires_at). Note: rows that never get touched by Create/EnforceActiveIPCap can still sit with revoked_at IS NULL past their refresh_until — for those, the row's expires_at / lifetime_ends_at are the truth.
func (*Postgres) EnforceActiveIPCap ¶
func (p *Postgres) EnforceActiveIPCap( ctx context.Context, identityType domain.AuthSessionIdentityType, ipHash string, maxActive int, now time.Time, ) error
EnforceActiveIPCap soft-revokes sessions for a given (identity_type, ip_hash). Two things happen in a single UPDATE:
- any not-yet-revoked rows past their refresh_until get revoked_reason = 'expired' and revoked_at = expires_at (the session was provably unused after that point).
- if the number of still-active rows (revoked_at IS NULL AND refresh_until > now) exceeds maxActive-1, the oldest excess gets revoked_reason = 'evicted_by_ip_cap' and revoked_at = now (the session is being actively killed now to make room).
CASE expressions pick reason and timestamp at write time from each row's own refresh_until so each touched row gets the accurate "why" and "when." Idempotent: if there's nothing to revoke, this is a no-op.
func (*Postgres) Update ¶
func (p *Postgres) Update( ctx context.Context, id string, update func(domain.AuthSession) (domain.AuthSession, error), ) (domain.AuthSession, error)
Update loads the row by id, calls update on it, and writes the result back inside a single transaction. update sees the live row and returns the desired new state. The row is SELECT-FOR-UPDATE locked between load and write so concurrent updates don't trample each other.
Only the mutable fields (ip_hash, expires_at, refresh_until, last_used_at) are written back; everything else (id, identity, time of creation, revocation state) is immutable from this method's perspective.
Returns ErrAuthSessionNotFound if the id doesn't exist, ErrAuthSessionRevoked if the row exists but has been revoked, update's error if it returns one (the row is not modified), or any tx/DB error.