diff --git a/appview/dns/cloudflare.go b/appview/dns/cloudflare.go
--- a/appview/dns/cloudflare.go
+++ b/appview/dns/cloudflare.go
@@ -30,8 +30,8 @@
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 @@
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
--- a/appview/signup/requests.go
+++ b/appview/signup/requests.go
@@ -102,3 +102,21 @@
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
--- a/appview/signup/signup.go
+++ b/appview/signup/signup.go
@@ -2,6 +2,7 @@
import (
"bufio"
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -216,56 +217,112 @@
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
- }
-
- s.pages.Notice(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now
- login
- with %s.tngl.sh.`, username))
-
- go func() {
- err := db.DeleteInflightSignup(s.db, email)
- if err != nil {
- s.l.Error("failed to delete inflight signup", "error", err)
- }
- }()
- 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)
+ }
+ }
+
+ // 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)
+ }
+ }
+
+ // 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)
+ }
+ }
+ }
+ }()
+
+ // 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 {