Implement owner setup and browser authentication sessions
Some checks failed
CI / Backend (push) Failing after 1s
CI / Frontend (push) Successful in 13s
CI / Contracts and repository policy (push) Failing after 3s
CI / Container (push) Has been skipped

This commit is contained in:
Elijah 2026-07-16 19:45:40 -07:00
parent 077cf7601a
commit 715423ab8e
21 changed files with 2185 additions and 14 deletions

View file

@ -0,0 +1,214 @@
package postgres
import (
"context"
"errors"
"fmt"
"time"
dbgen "drive.local/drivev2/internal/adapters/postgres/generated"
"drive.local/drivev2/internal/application"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) WithAuthTransaction(ctx context.Context, operation func(application.AuthTransaction) error) (returnErr error) {
transaction, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
if err != nil {
return fmt.Errorf("begin authentication transaction: %w", err)
}
defer func() {
if err := transaction.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
returnErr = errors.Join(returnErr, fmt.Errorf("rollback authentication transaction: %w", err))
}
}()
transactionStore := &Store{queries: s.queries.WithTx(transaction)}
if err := operation(transactionStore); err != nil {
return err
}
if err := transaction.Commit(ctx); err != nil {
return fmt.Errorf("commit authentication transaction: %w", err)
}
return nil
}
func (s *Store) CreateOwner(ctx context.Context, record application.CreateOwnerRecord) (string, error) {
id, err := s.queries.CreateOwner(ctx, dbgen.CreateOwnerParams{
Username: record.Username, UsernameKey: record.UsernameKey, DisplayName: record.DisplayName, PasswordHash: record.PasswordHash,
})
if ownerAlreadyExists(err) {
return "", application.ErrSetupAlreadyComplete
}
if err != nil {
return "", fmt.Errorf("create owner: %w", err)
}
return id, nil
}
func (s *Store) CreateOwnerRoot(ctx context.Context, ownerID string, createdAt time.Time) (string, error) {
ownerUUID, err := parseUUID(ownerID)
if err != nil {
return "", err
}
id, err := s.queries.CreateOwnerRootNode(ctx, dbgen.CreateOwnerRootNodeParams{
OwnerID: ownerUUID, CreatedAt: timestamp(createdAt),
})
if err != nil {
return "", fmt.Errorf("create owner root node: %w", err)
}
return id, nil
}
func (s *Store) SetOwnerRoot(ctx context.Context, ownerID, rootNodeID string, updatedAt time.Time) error {
ownerUUID, err := parseUUID(ownerID)
if err != nil {
return err
}
rootUUID, err := parseUUID(rootNodeID)
if err != nil {
return err
}
if err := s.queries.SetOwnerRootNode(ctx, dbgen.SetOwnerRootNodeParams{
RootNodeID: rootUUID, UpdatedAt: timestamp(updatedAt), OwnerID: ownerUUID,
}); err != nil {
return fmt.Errorf("set owner root node: %w", err)
}
return nil
}
func (s *Store) CreateRecoveryCode(ctx context.Context, ownerID string, codeHash []byte) error {
ownerUUID, err := parseUUID(ownerID)
if err != nil {
return err
}
if err := s.queries.CreateRecoveryCode(ctx, dbgen.CreateRecoveryCodeParams{OwnerID: ownerUUID, CodeHash: codeHash}); err != nil {
return fmt.Errorf("create recovery code: %w", err)
}
return nil
}
func (s *Store) CreateBrowserSession(ctx context.Context, record application.CreateSessionRecord) (string, error) {
ownerUUID, err := parseUUID(record.OwnerID)
if err != nil {
return "", err
}
id, err := s.queries.CreateBrowserSession(ctx, dbgen.CreateBrowserSessionParams{
OwnerID: ownerUUID, TokenHash: record.TokenHash, CsrfTokenHash: record.CSRFTokenHash,
RemoteAddress: record.RemoteAddress, UserAgent: record.UserAgent,
AuthenticatedAt: timestamp(record.AuthenticatedAt), ExpiresAt: timestamp(record.ExpiresAt),
})
if err != nil {
return "", fmt.Errorf("create browser session: %w", err)
}
return id, nil
}
func (s *Store) CreateInitialRootChange(ctx context.Context, record application.InitialRootChangeRecord) error {
ownerUUID, err := parseUUID(record.OwnerID)
if err != nil {
return err
}
rootUUID, err := parseUUID(record.RootNodeID)
if err != nil {
return err
}
if err := s.queries.CreateInitialRootChange(ctx, dbgen.CreateInitialRootChangeParams{
OwnerID: ownerUUID, RootNodeID: rootUUID, ResultingState: record.ResultingState,
MutationOrigin: record.MutationOrigin, CommittedAt: timestamp(record.CommittedAt),
}); err != nil {
return fmt.Errorf("create initial root change: %w", err)
}
return nil
}
func (s *Store) CreateAuthenticationAudit(ctx context.Context, record application.AuthenticationAuditRecord) error {
ownerUUID, err := parseUUID(record.OwnerID)
if err != nil {
return err
}
resourceUUID, err := parseUUID(record.ResourceID)
if err != nil {
return err
}
resourceType := record.ResourceType
if err := s.queries.CreateAuthenticationAuditEvent(ctx, dbgen.CreateAuthenticationAuditEventParams{
OwnerID: ownerUUID, Actor: record.Actor, Action: record.Action, ResourceType: &resourceType,
ResourceID: resourceUUID, Outcome: record.Outcome, Details: record.Details, OccurredAt: timestamp(record.OccurredAt),
}); err != nil {
return fmt.Errorf("create authentication audit event: %w", err)
}
return nil
}
func (s *Store) GetOwnerAuthenticationRecord(ctx context.Context) (application.OwnerAuthenticationRecord, error) {
record, err := s.queries.GetOwnerAuthenticationRecord(ctx)
if errors.Is(err, pgx.ErrNoRows) {
return application.OwnerAuthenticationRecord{}, application.ErrOwnerNotFound
}
if err != nil {
return application.OwnerAuthenticationRecord{}, fmt.Errorf("get owner authentication record: %w", err)
}
return application.OwnerAuthenticationRecord{
ID: record.ID, Username: record.Username, UsernameKey: record.UsernameKey,
DisplayName: record.DisplayName, PasswordHash: record.PasswordHash,
}, nil
}
func (s *Store) GetActiveBrowserSession(ctx context.Context, tokenHash []byte, now time.Time) (application.BrowserSession, error) {
record, err := s.queries.GetActiveBrowserSession(ctx, dbgen.GetActiveBrowserSessionParams{TokenHash: tokenHash, NowAt: timestamp(now)})
if errors.Is(err, pgx.ErrNoRows) {
return application.BrowserSession{}, application.ErrSessionNotFound
}
if err != nil {
return application.BrowserSession{}, fmt.Errorf("get active browser session: %w", err)
}
return application.BrowserSession{
ID: record.SessionID, OwnerID: record.OwnerID, Username: record.Username, DisplayName: record.DisplayName,
CSRFTokenHash: record.CsrfTokenHash, AuthenticatedAt: record.AuthenticatedAt.Time,
LastSeenAt: record.LastSeenAt.Time, ExpiresAt: record.ExpiresAt.Time,
}, nil
}
func (s *Store) TouchBrowserSession(ctx context.Context, sessionID string, seenAt time.Time) error {
sessionUUID, err := parseUUID(sessionID)
if err != nil {
return err
}
if err := s.queries.TouchBrowserSession(ctx, dbgen.TouchBrowserSessionParams{SeenAt: timestamp(seenAt), SessionID: sessionUUID}); err != nil {
return fmt.Errorf("touch browser session: %w", err)
}
return nil
}
func (s *Store) RevokeBrowserSession(ctx context.Context, sessionID string, revokedAt time.Time) (bool, error) {
sessionUUID, err := parseUUID(sessionID)
if err != nil {
return false, err
}
rows, err := s.queries.RevokeBrowserSession(ctx, dbgen.RevokeBrowserSessionParams{RevokedAt: timestamp(revokedAt), SessionID: sessionUUID})
if err != nil {
return false, fmt.Errorf("revoke browser session: %w", err)
}
return rows == 1, nil
}
func parseUUID(value string) (pgtype.UUID, error) {
var parsed pgtype.UUID
if err := parsed.Scan(value); err != nil {
return pgtype.UUID{}, fmt.Errorf("parse UUID: %w", err)
}
return parsed, nil
}
func timestamp(value time.Time) pgtype.Timestamptz {
return pgtype.Timestamptz{Time: value, Valid: true}
}
func ownerAlreadyExists(err error) bool {
var postgresError *pgconn.PgError
return errors.As(err, &postgresError) && postgresError.Code == pgerrcode.UniqueViolation &&
(postgresError.ConstraintName == "users_single_owner" || postgresError.ConstraintName == "users_username_key_unique")
}