package store import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/vasyakrg/dns-autoresolver/internal/diff" "github.com/vasyakrg/dns-autoresolver/internal/service" "github.com/vasyakrg/dns-autoresolver/internal/store/db" ) // LoadDomain joins domains+provider_accounts+templates to build the // service.DomainRef needed to check/apply a domain's DNS records. Scoped by // projectID so a domain belonging to another tenant's project can never be // loaded, even if its domainID is guessed/leaked (closes IDOR). func (s *Store) LoadDomain(ctx context.Context, projectID, domainID uuid.UUID) (service.DomainRef, error) { row, err := s.q.LoadDomainFull(ctx, db.LoadDomainFullParams{ID: domainID, ProjectID: projectID}) if err != nil { return service.DomainRef{}, err } if row.Doc == nil { return service.DomainRef{}, fmt.Errorf("store: domain %s has no template", domainID) } return service.DomainRef{ ZoneID: row.ZoneID, Provider: row.Provider, SecretEnc: row.SecretEnc, Template: *row.Doc, }, nil } // LoadZone returns just the provider-access half of a domain (provider name, // encrypted secret, zone id), WITHOUT requiring an attached template — so a // zone's live records can be read for viewing/snapshot even when no template // is set. Scoped by projectID (same IDOR closure as LoadDomain). func (s *Store) LoadZone(ctx context.Context, projectID, domainID uuid.UUID) (service.ZoneRef, error) { row, err := s.q.LoadDomainFull(ctx, db.LoadDomainFullParams{ID: domainID, ProjectID: projectID}) if err != nil { return service.ZoneRef{}, err } return service.ZoneRef{ZoneID: row.ZoneID, Provider: row.Provider, SecretEnc: row.SecretEnc}, nil } // SaveCheckRun persists a summary of the changeset (counts of updates/prunes) // as a check_runs row. func (s *Store) SaveCheckRun(ctx context.Context, domainID uuid.UUID, cs diff.Changeset) error { summary := map[string]int{ "updates": len(cs.Updates()), "prunes": len(cs.Prunes()), } raw, err := json.Marshal(summary) if err != nil { return err } _, err = s.q.CreateCheckRun(ctx, db.CreateCheckRunParams{ ID: uuid.New(), DomainID: domainID, Result: raw, }) return err } // CheckRun is a provider-neutral summary of a past check/apply run, returned // by ListCheckRuns for the domain history endpoint (Фаза 3). type CheckRun struct { ID uuid.UUID DomainID uuid.UUID Result json.RawMessage CreatedAt time.Time } func checkRunFromDB(c db.CheckRun) CheckRun { return CheckRun{ ID: c.ID, DomainID: c.DomainID, Result: json.RawMessage(c.Result), CreatedAt: c.CreatedAt.Time, } } // ListCheckRuns returns the most recent check_runs rows for a domain (newest // first, capped at 50). Not scoped by project itself — callers must verify // the domain belongs to the caller's project first (e.g. via GetDomain) // since check_runs only references domain_id. func (s *Store) ListCheckRuns(ctx context.Context, domainID uuid.UUID) ([]CheckRun, error) { rows, err := s.q.ListCheckRuns(ctx, domainID) if err != nil { return nil, err } out := make([]CheckRun, 0, len(rows)) for _, r := range rows { out = append(out, checkRunFromDB(r)) } return out, nil } // compile-time interface checks var _ service.Loader = (*Store)(nil) var _ service.Recorder = (*Store)(nil)