docs: reorder & testing

This commit is contained in:
Ilia Denisov
2026-05-07 00:58:53 +03:00
committed by GitHub
parent f446c6a2ac
commit 604fe40bcf
148 changed files with 9150 additions and 2757 deletions
+128 -29
View File
@@ -332,15 +332,14 @@ func (s *Store) LoadSession(ctx context.Context, deviceSessionID uuid.UUID) (Ses
return modelToSession(row), nil
}
// RevokeSession transitions an active row to status='revoked' and
// returns the row as it stands after the update. The boolean reports
// whether the UPDATE actually changed a row — false means the row was
// already revoked or did not exist; the auth Service then falls back to
// LoadSession for idempotent-revoke responses.
func (s *Store) RevokeSession(ctx context.Context, deviceSessionID uuid.UUID) (Session, bool, error) {
// TouchSessionLastSeen sets `last_seen_at` to at on the row keyed by
// deviceSessionID. The UPDATE is gated by `status='active'` so a
// revoked or absent row reports ErrSessionNotFound. Returns the post-
// update row so the cache can be refreshed without a second read.
func (s *Store) TouchSessionLastSeen(ctx context.Context, deviceSessionID uuid.UUID, at time.Time) (Session, error) {
stmt := table.DeviceSessions.
UPDATE(table.DeviceSessions.Status, table.DeviceSessions.RevokedAt).
SET(postgres.String(SessionStatusRevoked), postgres.NOW()).
UPDATE(table.DeviceSessions.LastSeenAt).
SET(postgres.TimestampzT(at)).
WHERE(
table.DeviceSessions.DeviceSessionID.EQ(postgres.UUID(deviceSessionID)).
AND(table.DeviceSessions.Status.EQ(postgres.String(SessionStatusActive))),
@@ -350,39 +349,139 @@ func (s *Store) RevokeSession(ctx context.Context, deviceSessionID uuid.UUID) (S
var row model.DeviceSessions
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return Session{}, false, nil
return Session{}, ErrSessionNotFound
}
return Session{}, fmt.Errorf("auth store: touch last_seen %s: %w", deviceSessionID, err)
}
return modelToSession(row), nil
}
// RevokeSession transitions an active row to status='revoked' and
// inserts the matching audit row into session_revocations atomically
// inside one transaction. The boolean reports whether the UPDATE
// actually changed a row — false means the row was already revoked or
// did not exist, in which case no audit row is written and the auth
// Service falls back to LoadSession for the idempotent-revoke
// response.
func (s *Store) RevokeSession(ctx context.Context, deviceSessionID uuid.UUID, rc RevokeContext, at time.Time) (Session, bool, error) {
var (
revoked Session
ok bool
)
err := withTx(ctx, s.db, func(tx *sql.Tx) error {
updateStmt := table.DeviceSessions.
UPDATE(table.DeviceSessions.Status, table.DeviceSessions.RevokedAt).
SET(postgres.String(SessionStatusRevoked), postgres.TimestampzT(at)).
WHERE(
table.DeviceSessions.DeviceSessionID.EQ(postgres.UUID(deviceSessionID)).
AND(table.DeviceSessions.Status.EQ(postgres.String(SessionStatusActive))),
).
RETURNING(sessionColumns())
var row model.DeviceSessions
if err := updateStmt.QueryContext(ctx, tx, &row); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return nil
}
return err
}
revoked = modelToSession(row)
ok = true
return insertRevocationTx(ctx, tx, deviceSessionID, revoked.UserID, rc, at)
})
if err != nil {
return Session{}, false, fmt.Errorf("auth store: revoke session %s: %w", deviceSessionID, err)
}
return modelToSession(row), true, nil
return revoked, ok, nil
}
// RevokeAllForUser transitions every active row for userID to
// status='revoked' and returns the rows as they stand after the update.
// An empty slice with a nil error is returned when the user owned no
// active sessions; the caller must treat that as a successful idempotent
// revoke (the API surface returns revoked_count=0 in that case).
func (s *Store) RevokeAllForUser(ctx context.Context, userID uuid.UUID) ([]Session, error) {
stmt := table.DeviceSessions.
UPDATE(table.DeviceSessions.Status, table.DeviceSessions.RevokedAt).
SET(postgres.String(SessionStatusRevoked), postgres.NOW()).
WHERE(
table.DeviceSessions.UserID.EQ(postgres.UUID(userID)).
AND(table.DeviceSessions.Status.EQ(postgres.String(SessionStatusActive))),
).
RETURNING(sessionColumns())
// status='revoked', writes one session_revocations row per revoked
// session, and returns the rows as they stand after the update. The
// UPDATE and the audit inserts run inside one transaction. An empty
// slice with a nil error is returned when the user owned no active
// sessions; the caller treats that as a successful idempotent revoke
// (the API surface returns revoked_count=0).
func (s *Store) RevokeAllForUser(ctx context.Context, userID uuid.UUID, rc RevokeContext, at time.Time) ([]Session, error) {
var out []Session
err := withTx(ctx, s.db, func(tx *sql.Tx) error {
updateStmt := table.DeviceSessions.
UPDATE(table.DeviceSessions.Status, table.DeviceSessions.RevokedAt).
SET(postgres.String(SessionStatusRevoked), postgres.TimestampzT(at)).
WHERE(
table.DeviceSessions.UserID.EQ(postgres.UUID(userID)).
AND(table.DeviceSessions.Status.EQ(postgres.String(SessionStatusActive))),
).
RETURNING(sessionColumns())
var rows []model.DeviceSessions
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
var rows []model.DeviceSessions
if err := updateStmt.QueryContext(ctx, tx, &rows); err != nil {
return err
}
out = make([]Session, 0, len(rows))
for _, row := range rows {
sess := modelToSession(row)
out = append(out, sess)
if err := insertRevocationTx(ctx, tx, sess.DeviceSessionID, sess.UserID, rc, at); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("auth store: revoke all for user %s: %w", userID, err)
}
out := make([]Session, 0, len(rows))
for _, row := range rows {
out = append(out, modelToSession(row))
}
return out, nil
}
// insertRevocationTx writes a single audit row inside an existing
// transaction. Callers are expected to mint a fresh revocation_id per
// row; collisions are not retried because revocation_id is a uuid.New
// in the only call sites.
func insertRevocationTx(ctx context.Context, tx *sql.Tx, deviceSessionID, userID uuid.UUID, rc RevokeContext, at time.Time) error {
actorUserID, actorUsername, err := revokeContextToColumns(rc)
if err != nil {
return err
}
stmt := table.SessionRevocations.INSERT(
table.SessionRevocations.RevocationID,
table.SessionRevocations.DeviceSessionID,
table.SessionRevocations.UserID,
table.SessionRevocations.ActorKind,
table.SessionRevocations.ActorUserID,
table.SessionRevocations.ActorUsername,
table.SessionRevocations.Reason,
table.SessionRevocations.RevokedAt,
).VALUES(uuid.New(), deviceSessionID, userID, string(rc.ActorKind), actorUserID, actorUsername, rc.Reason, at)
if _, err := stmt.ExecContext(ctx, tx); err != nil {
return fmt.Errorf("insert session_revocations: %w", err)
}
return nil
}
// revokeContextToColumns splits RevokeContext.ActorID into the
// (actor_user_id, actor_username) pair persisted by session_revocations.
// User-driven kinds parse ActorID as a UUID; admin-driven kinds keep it
// as the operator username. Empty ActorID lands as NULL/NULL.
func revokeContextToColumns(rc RevokeContext) (any, any, error) {
if rc.ActorID == "" {
return nil, nil, nil
}
switch rc.ActorKind {
case ActorKindUserSelf, ActorKindSoftDeleteUser:
uid, err := uuid.Parse(rc.ActorID)
if err != nil {
return nil, nil, fmt.Errorf("auth store: actor_id %q is not a uuid: %w", rc.ActorID, err)
}
return uid, nil, nil
case ActorKindAdminSanction, ActorKindSoftDeleteAdmin:
return nil, rc.ActorID, nil
default:
return nil, nil, nil
}
}
// modelToChallenge projects a generated model row into the public
// Challenge struct. Pointer fields are copied so callers cannot mutate
// the underlying scan buffer.