package api_impl import ( "database/sql" "encoding/json" "errors" "fmt" "log/slog" "net/http" "time" "atlas9.dev/c/core" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/api" "atlas9.dev/c/demo/lib/domains" "atlas9.dev/c/demo/tasks" ) type DomainImpl struct { DB *sql.DB Store dbi.Factory[domains.Store] Tasks dbi.Factory[tasks.Producer] Audit dbi.Factory[iam.AuditStore] } func (d *DomainImpl) ServeMux(mux *http.ServeMux) { mux.HandleFunc(api.Path_Domain_Create, d.Create) mux.HandleFunc(api.Path_Domain_Get, d.Get) mux.HandleFunc(api.Path_Domain_List, d.List) mux.HandleFunc(api.Path_Domain_Delete, d.Delete) mux.HandleFunc(api.Path_Domain_Verify, d.Verify) } func (d *DomainImpl) Create(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req api.Domain_CreateReq if read(w, r, &req) { return } dom := domains.NewDomain(req.Tenant, req.Domain) err := dbi.ReadWrite(ctx, d.DB, func(tx dbi.DBI) error { err := d.Store(tx).Create(ctx, &dom) if err != nil { return err } payload, err := json.Marshal(api.Domain_VerifyReq{ ID: dom.ID, Tenant: req.Tenant, }) if err != nil { return err } if err := d.Tasks(tx).Push(ctx, api.Path_Domain_Verify, payload); err != nil { return err } return audit(ctx, d.Audit(tx), iam.AuditEntry{ Tenant: req.Tenant, Action: "Domain_Create", Resource: dom.ID.String(), Detail: dom.Domain, }) }) write(ctx, w, err, api.Domain_CreateRes{Domain: dom}) } func (d *DomainImpl) Get(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req api.Domain_GetReq if read(w, r, &req) { return } var res api.Domain_GetRes err := dbi.ReadOnly(ctx, d.DB, func(tx dbi.DBI) error { return d.Store(tx).Get(ctx, req.Tenant, req.ID, &res.Domain) }) write(ctx, w, err, res) } func (d *DomainImpl) List(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req api.Domain_ListReq if read(w, r, &req) { return } var res api.Domain_ListRes err := dbi.ReadOnly(ctx, d.DB, func(tx dbi.DBI) error { return d.Store(tx).List(ctx, req.Tenant, req.Page, &res.Page) }) if err != nil { writeErr(ctx, w, err) return } write(ctx, w, nil, res) } func (d *DomainImpl) Delete(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req api.Domain_DeleteReq if read(w, r, &req) { return } err := dbi.ReadWrite(ctx, d.DB, func(tx dbi.DBI) error { err := d.Store(tx).Delete(ctx, req.Tenant, req.ID) if err != nil { return err } return audit(ctx, d.Audit(tx), iam.AuditEntry{ Tenant: req.Tenant, Action: "Domain_Delete", Resource: req.ID.String(), }) }) write(ctx, w, err, api.Domain_DeleteRes{}) } func (d *DomainImpl) Verify(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req api.Domain_VerifyReq if read(w, r, &req) { return } var dom domains.Domain err := dbi.ReadOnly(ctx, d.DB, func(tx dbi.DBI) error { return d.Store(tx).Get(ctx, req.Tenant, req.ID, &dom) }) if errors.Is(err, core.ErrNotFound) { // The domain was deleted while verification was pending: an // exceptional circumstance that retrying cannot fix, so the task // fails permanently. slog.InfoContext(ctx, "domain deleted before verification", "domain", req.ID) write(ctx, w, nil, api.Domain_VerifyRes{ Task: tasks.Result{Outcome: tasks.OutcomeFailed, Error: "domain deleted before verification"}, }) return } if writeErr(ctx, w, wrap("reading domain", err)) { return } found, err := domains.VerifyDomain(dom.Domain, req.ID.String()) if err != nil { slog.WarnContext(ctx, "domain verification error", "domain", req.ID, "err", err) write(ctx, w, nil, api.Domain_VerifyRes{ Task: tasks.Result{Outcome: tasks.OutcomeRetry, Error: wrap("verifying domain", err).Error()}, }) return } if !found { // The owner may simply not have published the DNS record yet. write(ctx, w, nil, api.Domain_VerifyRes{ Task: tasks.Result{Outcome: tasks.OutcomeRetry, Error: "verification record not found"}, }) return } err = dbi.ReadWrite(ctx, d.DB, func(tx dbi.DBI) error { if err := d.Store(tx).SetVerifiedAt(ctx, req.Tenant, req.ID, time.Now()); err != nil { return err } return audit(ctx, d.Audit(tx), iam.AuditEntry{ Tenant: req.Tenant, Action: "Domain_Verify", Resource: req.ID.String(), Detail: dom.Domain, }) }) if errors.Is(err, core.ErrNotFound) { // Deleted between the DNS check and the write; same permanent // failure as above. slog.InfoContext(ctx, "domain deleted before verification", "domain", req.ID) write(ctx, w, nil, api.Domain_VerifyRes{ Task: tasks.Result{Outcome: tasks.OutcomeFailed, Error: "domain deleted before verification"}, }) return } write(ctx, w, err, api.Domain_VerifyRes{ Verified: true, Task: tasks.Result{Outcome: tasks.OutcomeCompleted}, }) } func wrap(msg string, err error) error { if err == nil { return nil } return fmt.Errorf("%s: %w", msg, err) }