forked from tangled.org/core
Monorepo for Tangled

appview/{dns,signup}: make signup flow transactional

authored by anirudh.fi and committed by Tangled ce1f2b90 0bba9fb3

Changed files
+115 -40
appview
+4 -4
appview/dns/cloudflare.go
··· 30 30 return &Cloudflare{api: api, zone: c.Cloudflare.ZoneId}, nil 31 31 } 32 32 33 - func (cf *Cloudflare) CreateDNSRecord(ctx context.Context, record Record) error { 34 - _, err := cf.api.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(cf.zone), cloudflare.CreateDNSRecordParams{ 33 + func (cf *Cloudflare) CreateDNSRecord(ctx context.Context, record Record) (string, error) { 34 + result, err := cf.api.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(cf.zone), cloudflare.CreateDNSRecordParams{ 35 35 Type: record.Type, 36 36 Name: record.Name, 37 37 Content: record.Content, ··· 39 39 Proxied: &record.Proxied, 40 40 }) 41 41 if err != nil { 42 - return fmt.Errorf("failed to create DNS record: %w", err) 42 + return "", fmt.Errorf("failed to create DNS record: %w", err) 43 43 } 44 - return nil 44 + return result.ID, nil 45 45 } 46 46 47 47 func (cf *Cloudflare) DeleteDNSRecord(ctx context.Context, recordID string) error {
+18
appview/signup/requests.go
··· 102 102 103 103 return result.DID, nil 104 104 } 105 + 106 + func (s *Signup) deleteAccountRequest(did string) error { 107 + body := map[string]string{ 108 + "did": did, 109 + } 110 + 111 + resp, err := s.makePdsRequest("POST", "com.atproto.admin.deleteAccount", body, true) 112 + if err != nil { 113 + return err 114 + } 115 + defer resp.Body.Close() 116 + 117 + if resp.StatusCode != http.StatusOK { 118 + return s.handlePdsError(resp, "delete account") 119 + } 120 + 121 + return nil 122 + }
+93 -36
appview/signup/signup.go
··· 2 2 3 3 import ( 4 4 "bufio" 5 + "context" 5 6 "encoding/json" 6 7 "errors" 7 8 "fmt" ··· 216 217 return 217 218 } 218 219 219 - did, err := s.createAccountRequest(username, password, email, code) 220 - if err != nil { 221 - s.l.Error("failed to create account", "error", err) 222 - s.pages.Notice(w, "signup-error", err.Error()) 223 - return 224 - } 225 - 226 220 if s.cf == nil { 227 221 s.l.Error("cloudflare client is nil", "error", "Cloudflare integration is not enabled in configuration") 228 222 s.pages.Notice(w, "signup-error", "Account signup is currently disabled. DNS record creation is not available. Please contact support.") 229 223 return 230 224 } 231 225 232 - err = s.cf.CreateDNSRecord(r.Context(), dns.Record{ 233 - Type: "TXT", 234 - Name: "_atproto." + username, 235 - Content: fmt.Sprintf(`"did=%s"`, did), 236 - TTL: 6400, 237 - Proxied: false, 238 - }) 226 + // Execute signup transactionally with rollback capability 227 + err = s.executeSignupTransaction(r.Context(), username, password, email, code, w) 239 228 if err != nil { 240 - s.l.Error("failed to create DNS record", "error", err) 241 - s.pages.Notice(w, "signup-error", "Failed to create DNS record for your handle. Please contact support.") 229 + // Error already logged and notice already sent 242 230 return 243 231 } 232 + } 233 + } 244 234 245 - err = db.AddEmail(s.db, models.Email{ 246 - Did: did, 247 - Address: email, 248 - Verified: true, 249 - Primary: true, 250 - }) 251 - if err != nil { 252 - s.l.Error("failed to add email", "error", err) 253 - s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.") 254 - return 255 - } 235 + // executeSignupTransaction performs the signup process transactionally with rollback 236 + func (s *Signup) executeSignupTransaction(ctx context.Context, username, password, email, code string, w http.ResponseWriter) error { 237 + var recordID string 238 + var did string 239 + var emailAdded bool 256 240 257 - s.pages.Notice(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now 258 - <a class="underline text-black dark:text-white" href="/login">login</a> 259 - with <code>%s.tngl.sh</code>.`, username)) 241 + success := false 242 + defer func() { 243 + if !success { 244 + s.l.Info("rolling back signup transaction", "username", username, "did", did) 260 245 261 - go func() { 262 - err := db.DeleteInflightSignup(s.db, email) 263 - if err != nil { 264 - s.l.Error("failed to delete inflight signup", "error", err) 246 + // Rollback DNS record 247 + if recordID != "" { 248 + if err := s.cf.DeleteDNSRecord(ctx, recordID); err != nil { 249 + s.l.Error("failed to rollback DNS record", "error", err, "recordID", recordID) 250 + } else { 251 + s.l.Info("successfully rolled back DNS record", "recordID", recordID) 252 + } 265 253 } 266 - }() 267 - return 254 + 255 + // Rollback PDS account 256 + if did != "" { 257 + if err := s.deleteAccountRequest(did); err != nil { 258 + s.l.Error("failed to rollback PDS account", "error", err, "did", did) 259 + } else { 260 + s.l.Info("successfully rolled back PDS account", "did", did) 261 + } 262 + } 263 + 264 + // Rollback email from database 265 + if emailAdded { 266 + if err := db.DeleteEmail(s.db, did, email); err != nil { 267 + s.l.Error("failed to rollback email from database", "error", err, "email", email) 268 + } else { 269 + s.l.Info("successfully rolled back email from database", "email", email) 270 + } 271 + } 272 + } 273 + }() 274 + 275 + // step 1: create account in PDS 276 + did, err := s.createAccountRequest(username, password, email, code) 277 + if err != nil { 278 + s.l.Error("failed to create account", "error", err) 279 + s.pages.Notice(w, "signup-error", err.Error()) 280 + return err 268 281 } 282 + 283 + // step 2: create DNS record with actual DID 284 + recordID, err = s.cf.CreateDNSRecord(ctx, dns.Record{ 285 + Type: "TXT", 286 + Name: "_atproto." + username, 287 + Content: fmt.Sprintf(`"did=%s"`, did), 288 + TTL: 6400, 289 + Proxied: false, 290 + }) 291 + if err != nil { 292 + s.l.Error("failed to create DNS record", "error", err) 293 + s.pages.Notice(w, "signup-error", "Failed to create DNS record for your handle. Please contact support.") 294 + return err 295 + } 296 + 297 + // step 3: add email to database 298 + err = db.AddEmail(s.db, models.Email{ 299 + Did: did, 300 + Address: email, 301 + Verified: true, 302 + Primary: true, 303 + }) 304 + if err != nil { 305 + s.l.Error("failed to add email", "error", err) 306 + s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.") 307 + return err 308 + } 309 + emailAdded = true 310 + 311 + // if we get here, we've successfully created the account and added the email 312 + success = true 313 + 314 + s.pages.Notice(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now 315 + <a class="underline text-black dark:text-white" href="/login">login</a> 316 + with <code>%s.tngl.sh</code>.`, username)) 317 + 318 + // clean up inflight signup asynchronously 319 + go func() { 320 + if err := db.DeleteInflightSignup(s.db, email); err != nil { 321 + s.l.Error("failed to delete inflight signup", "error", err) 322 + } 323 + }() 324 + 325 + return nil 269 326 } 270 327 271 328 type turnstileResponse struct {