From 0cc6260d4d84856e60e48a0c3fa2885afd616520 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Mon, 13 Oct 2025 15:09:42 +0300 Subject: [PATCH] appview/{dns,signup}: make signup flow transactional Change-Id: muxlkvmnmlrnlykstzkqvsmmkptltwqw --- appview/dns/cloudflare.go | 8 +-- appview/signup/requests.go | 18 ++++++ appview/signup/signup.go | 129 ++++++++++++++++++++++++++----------- 3 files changed, 115 insertions(+), 40 deletions(-) diff --git a/appview/dns/cloudflare.go b/appview/dns/cloudflare.go index c85f0209..18517814 100644 --- a/appview/dns/cloudflare.go +++ b/appview/dns/cloudflare.go @@ -30,8 +30,8 @@ func NewCloudflare(c *config.Config) (*Cloudflare, error) { return &Cloudflare{api: api, zone: c.Cloudflare.ZoneId}, nil } -func (cf *Cloudflare) CreateDNSRecord(ctx context.Context, record Record) error { - _, err := cf.api.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(cf.zone), cloudflare.CreateDNSRecordParams{ +func (cf *Cloudflare) CreateDNSRecord(ctx context.Context, record Record) (string, error) { + result, err := cf.api.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(cf.zone), cloudflare.CreateDNSRecordParams{ Type: record.Type, Name: record.Name, Content: record.Content, @@ -39,9 +39,9 @@ func (cf *Cloudflare) CreateDNSRecord(ctx context.Context, record Record) error Proxied: &record.Proxied, }) if err != nil { - return fmt.Errorf("failed to create DNS record: %w", err) + return "", fmt.Errorf("failed to create DNS record: %w", err) } - return nil + return result.ID, nil } func (cf *Cloudflare) DeleteDNSRecord(ctx context.Context, recordID string) error { diff --git a/appview/signup/requests.go b/appview/signup/requests.go index 103049f1..c5c88935 100644 --- a/appview/signup/requests.go +++ b/appview/signup/requests.go @@ -102,3 +102,21 @@ func (s *Signup) createAccountRequest(username, password, email, code string) (s return result.DID, nil } + +func (s *Signup) deleteAccountRequest(did string) error { + body := map[string]string{ + "did": did, + } + + resp, err := s.makePdsRequest("POST", "com.atproto.admin.deleteAccount", body, true) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return s.handlePdsError(resp, "delete account") + } + + return nil +} diff --git a/appview/signup/signup.go b/appview/signup/signup.go index 6c79cd26..ed97ef91 100644 --- a/appview/signup/signup.go +++ b/appview/signup/signup.go @@ -2,6 +2,7 @@ package signup import ( "bufio" + "context" "encoding/json" "errors" "fmt" @@ -216,56 +217,112 @@ func (s *Signup) complete(w http.ResponseWriter, r *http.Request) { return } - did, err := s.createAccountRequest(username, password, email, code) - if err != nil { - s.l.Error("failed to create account", "error", err) - s.pages.Notice(w, "signup-error", err.Error()) - return - } - if s.cf == nil { s.l.Error("cloudflare client is nil", "error", "Cloudflare integration is not enabled in configuration") s.pages.Notice(w, "signup-error", "Account signup is currently disabled. DNS record creation is not available. Please contact support.") return } - err = s.cf.CreateDNSRecord(r.Context(), dns.Record{ - Type: "TXT", - Name: "_atproto." + username, - Content: fmt.Sprintf(`"did=%s"`, did), - TTL: 6400, - Proxied: false, - }) + // Execute signup transactionally with rollback capability + err = s.executeSignupTransaction(r.Context(), username, password, email, code, w) if err != nil { - s.l.Error("failed to create DNS record", "error", err) - s.pages.Notice(w, "signup-error", "Failed to create DNS record for your handle. Please contact support.") + // Error already logged and notice already sent return } + } +} - err = db.AddEmail(s.db, models.Email{ - Did: did, - Address: email, - Verified: true, - Primary: true, - }) - if err != nil { - s.l.Error("failed to add email", "error", err) - s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.") - return - } +// executeSignupTransaction performs the signup process transactionally with rollback +func (s *Signup) executeSignupTransaction(ctx context.Context, username, password, email, code string, w http.ResponseWriter) error { + var recordID string + var did string + var emailAdded bool + + success := false + defer func() { + if !success { + s.l.Info("rolling back signup transaction", "username", username, "did", did) + + // Rollback DNS record + if recordID != "" { + if err := s.cf.DeleteDNSRecord(ctx, recordID); err != nil { + s.l.Error("failed to rollback DNS record", "error", err, "recordID", recordID) + } else { + s.l.Info("successfully rolled back DNS record", "recordID", recordID) + } + } - s.pages.Notice(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now - login - with %s.tngl.sh.`, username)) + // Rollback PDS account + if did != "" { + if err := s.deleteAccountRequest(did); err != nil { + s.l.Error("failed to rollback PDS account", "error", err, "did", did) + } else { + s.l.Info("successfully rolled back PDS account", "did", did) + } + } - go func() { - err := db.DeleteInflightSignup(s.db, email) - if err != nil { - s.l.Error("failed to delete inflight signup", "error", err) + // Rollback email from database + if emailAdded { + if err := db.DeleteEmail(s.db, did, email); err != nil { + s.l.Error("failed to rollback email from database", "error", err, "email", email) + } else { + s.l.Info("successfully rolled back email from database", "email", email) + } } - }() - return + } + }() + + // step 1: create account in PDS + did, err := s.createAccountRequest(username, password, email, code) + if err != nil { + s.l.Error("failed to create account", "error", err) + s.pages.Notice(w, "signup-error", err.Error()) + return err + } + + // step 2: create DNS record with actual DID + recordID, err = s.cf.CreateDNSRecord(ctx, dns.Record{ + Type: "TXT", + Name: "_atproto." + username, + Content: fmt.Sprintf(`"did=%s"`, did), + TTL: 6400, + Proxied: false, + }) + if err != nil { + s.l.Error("failed to create DNS record", "error", err) + s.pages.Notice(w, "signup-error", "Failed to create DNS record for your handle. Please contact support.") + return err + } + + // step 3: add email to database + err = db.AddEmail(s.db, models.Email{ + Did: did, + Address: email, + Verified: true, + Primary: true, + }) + if err != nil { + s.l.Error("failed to add email", "error", err) + s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.") + return err } + emailAdded = true + + // if we get here, we've successfully created the account and added the email + success = true + + s.pages.Notice(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now + login + with %s.tngl.sh.`, username)) + + // clean up inflight signup asynchronously + go func() { + if err := db.DeleteInflightSignup(s.db, email); err != nil { + s.l.Error("failed to delete inflight signup", "error", err) + } + }() + + return nil } type turnstileResponse struct { -- 2.43.0