1b367c4bda
Manual domain checks (Recheck button / diff page load) never wrote domains.last_check_status - only the scheduler did, leaving a newly-templated domain stuck at "unknown" until the next scheduled run. Extract status derivation into internal/service (single source of truth): StatusUnknown/InSync/Drift/Error constants and DeriveStatus(diff.Changeset). The scheduler now aliases these constants instead of duplicating them. handleCheck persists the derived status (or StatusError on failure) via TenantStore.SetDomainStatus after every manual check - status/history only, no notification, which remains the scheduler's job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
168 lines
6.6 KiB
Go
168 lines
6.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/vasyakrg/dns-autoresolver/internal/diff"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/model"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/provider"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/service"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/store"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/store/dto"
|
|
)
|
|
|
|
// CheckApplier is the service surface the API depends on.
|
|
type CheckApplier interface {
|
|
Check(ctx context.Context, projectID, domainID uuid.UUID) (diff.Changeset, error)
|
|
Apply(ctx context.Context, projectID, domainID uuid.UUID, req service.ApplyRequest) (diff.Changeset, error)
|
|
// ZoneRecords reads a zone's current records straight from the provider,
|
|
// with no diff and no template required — backs read-only zone viewing
|
|
// and the template-from-zone snapshot.
|
|
ZoneRecords(ctx context.Context, projectID, domainID uuid.UUID) ([]model.Record, error)
|
|
}
|
|
|
|
// TenantStore is the narrow persistence surface the CRUD handlers depend on.
|
|
// *store.Store satisfies it directly via its thin wrapper methods (see
|
|
// internal/store/tenant.go); tests can supply their own mock.
|
|
type TenantStore interface {
|
|
CreateAccount(ctx context.Context, projectID uuid.UUID, provider, secretEnc, comment string) (store.Account, error)
|
|
ListAccounts(ctx context.Context, projectID uuid.UUID) ([]store.Account, error)
|
|
GetAccount(ctx context.Context, id, projectID uuid.UUID) (store.Account, error)
|
|
DeleteAccount(ctx context.Context, id, projectID uuid.UUID) error
|
|
|
|
CreateTemplate(ctx context.Context, projectID uuid.UUID, name string, doc dto.TemplateDoc) (store.Template, error)
|
|
ListTemplates(ctx context.Context, projectID uuid.UUID) ([]store.Template, error)
|
|
UpdateTemplate(ctx context.Context, id, projectID uuid.UUID, name string, doc dto.TemplateDoc) (store.Template, error)
|
|
DeleteTemplate(ctx context.Context, id, projectID uuid.UUID) error
|
|
GetTemplate(ctx context.Context, id, projectID uuid.UUID) (store.Template, error)
|
|
|
|
CreateDomain(ctx context.Context, projectID, accountID uuid.UUID, zoneName, zoneID string, templateID *uuid.UUID) (store.Domain, error)
|
|
ListDomains(ctx context.Context, projectID uuid.UUID) ([]store.Domain, error)
|
|
// GetDomain is used by the template-from-zone snapshot to read the
|
|
// domain's zone name (for the generated template's name) before creating it.
|
|
GetDomain(ctx context.Context, id, projectID uuid.UUID) (store.Domain, error)
|
|
DeleteDomain(ctx context.Context, id, projectID uuid.UUID) error
|
|
ImportDomains(ctx context.Context, projectID, accountID uuid.UUID, zones []provider.Zone) ([]store.Domain, error)
|
|
SetDomainTemplate(ctx context.Context, domainID, projectID uuid.UUID, templateID *uuid.UUID) (store.Domain, error)
|
|
// SetDomainStatus persists the outcome of a manual check (handleCheck) so
|
|
// the domain's badge reflects reality immediately, instead of staying
|
|
// "unknown" until the scheduler's next tick.
|
|
SetDomainStatus(ctx context.Context, domainID uuid.UUID, status string) error
|
|
}
|
|
|
|
// Cipher encrypts/decrypts provider account secrets. *crypto.Cipher satisfies it.
|
|
type Cipher interface {
|
|
Encrypt(plaintext []byte) (string, error)
|
|
Decrypt(enc string) ([]byte, error)
|
|
}
|
|
|
|
// ProviderRegistry resolves a provider.Provider by name. *registry.Registry satisfies it.
|
|
type ProviderRegistry interface {
|
|
ByName(name string) (provider.Provider, error)
|
|
}
|
|
|
|
// AuthStore is the persistence surface the auth handlers depend on.
|
|
// *store.Store satisfies it directly (see internal/store/store.go); tests
|
|
// can supply their own mock.
|
|
type AuthStore interface {
|
|
RegisterUser(ctx context.Context, email, passwordHash string) (store.User, store.Project, error)
|
|
GetUserByEmail(ctx context.Context, email string) (store.User, error)
|
|
GetUserByID(ctx context.Context, userID uuid.UUID) (store.User, error)
|
|
GetUserProject(ctx context.Context, userID uuid.UUID) (store.Project, error)
|
|
// GetProjectOwned looks up projectID and returns it only if it's owned by
|
|
// userID — RequireProjectAccess uses this to reject foreign/nonexistent
|
|
// projects with 404 before any handler runs.
|
|
GetProjectOwned(ctx context.Context, projectID, userID uuid.UUID) (store.Project, error)
|
|
}
|
|
|
|
// SessionManager creates/validates/destroys login sessions. *auth.Sessions
|
|
// satisfies it directly (see internal/auth/session.go).
|
|
type SessionManager interface {
|
|
Create(ctx context.Context, userID uuid.UUID) (string, time.Time, error)
|
|
Validate(ctx context.Context, token string) (uuid.UUID, error)
|
|
Destroy(ctx context.Context, token string) error
|
|
}
|
|
|
|
// API holds handler dependencies.
|
|
type API struct {
|
|
Svc CheckApplier
|
|
Store TenantStore
|
|
Cipher Cipher
|
|
Reg ProviderRegistry
|
|
Auth AuthStore
|
|
Sessions SessionManager
|
|
Schedule ScheduleStore
|
|
Dispatch TestSender
|
|
}
|
|
|
|
func NewRouter(a *API) http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
r.Route("/api/v1/auth", func(r chi.Router) {
|
|
r.Post("/register", a.handleRegister)
|
|
r.Post("/login", a.handleLogin)
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(a.RequireAuth)
|
|
r.Post("/logout", a.handleLogout)
|
|
r.Get("/me", a.handleMe)
|
|
})
|
|
})
|
|
|
|
r.Route("/api/v1/projects/{pid}", func(r chi.Router) {
|
|
r.Use(a.RequireAuth)
|
|
r.Use(a.RequireProjectAccess)
|
|
|
|
r.Route("/domains", func(r chi.Router) {
|
|
r.Post("/", a.handleCreateDomain)
|
|
r.Get("/", a.handleListDomains)
|
|
r.Route("/{did}", func(r chi.Router) {
|
|
r.Get("/check", a.handleCheck)
|
|
r.Post("/apply", a.handleApply)
|
|
r.Patch("/", a.handleSetDomainTemplate)
|
|
r.Delete("/", a.handleDeleteDomain)
|
|
r.Get("/history", a.handleDomainHistory)
|
|
r.Get("/records", a.handleZoneRecords)
|
|
r.Post("/template-from-zone", a.handleTemplateFromZone)
|
|
})
|
|
})
|
|
|
|
r.Get("/schedule", a.handleGetSchedule)
|
|
r.Put("/schedule", a.handlePutSchedule)
|
|
r.Route("/channels", func(r chi.Router) {
|
|
r.Post("/", a.handleCreateChannel)
|
|
r.Get("/", a.handleListChannels)
|
|
r.Route("/{cid}", func(r chi.Router) {
|
|
r.Delete("/", a.handleDeleteChannel)
|
|
r.Post("/test", a.handleTestChannel)
|
|
})
|
|
})
|
|
|
|
r.Route("/accounts", func(r chi.Router) {
|
|
r.Post("/", a.handleCreateAccount)
|
|
r.Get("/", a.handleListAccounts)
|
|
r.Route("/{aid}", func(r chi.Router) {
|
|
r.Delete("/", a.handleDeleteAccount)
|
|
r.Post("/import", a.handleImportZones)
|
|
})
|
|
})
|
|
|
|
r.Route("/templates", func(r chi.Router) {
|
|
r.Post("/", a.handleCreateTemplate)
|
|
r.Get("/", a.handleListTemplates)
|
|
r.Route("/{tid}", func(r chi.Router) {
|
|
r.Put("/", a.handleUpdateTemplate)
|
|
r.Delete("/", a.handleDeleteTemplate)
|
|
})
|
|
})
|
|
})
|
|
return r
|
|
}
|