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") }