Monorepo for Tangled tangled.org

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

anirudh.fi 0cc6260d 8f7177a8

verified
Changed files
+115 -40
appview
+4 -4
appview/dns/cloudflare.go
··· 30 return &Cloudflare{api: api, zone: c.Cloudflare.ZoneId}, nil 31 } 32 33 - func (cf *Cloudflare) CreateDNSRecord(ctx context.Context, record Record) error { 34 - _, err := cf.api.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(cf.zone), cloudflare.CreateDNSRecordParams{ 35 Type: record.Type, 36 Name: record.Name, 37 Content: record.Content, ··· 39 Proxied: &record.Proxied, 40 }) 41 if err != nil { 42 - return fmt.Errorf("failed to create DNS record: %w", err) 43 } 44 - return nil 45 } 46 47 func (cf *Cloudflare) DeleteDNSRecord(ctx context.Context, recordID string) error {
··· 30 return &Cloudflare{api: api, zone: c.Cloudflare.ZoneId}, nil 31 } 32 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 Type: record.Type, 36 Name: record.Name, 37 Content: record.Content, ··· 39 Proxied: &record.Proxied, 40 }) 41 if err != nil { 42 + return "", fmt.Errorf("failed to create DNS record: %w", err) 43 } 44 + return result.ID, nil 45 } 46 47 func (cf *Cloudflare) DeleteDNSRecord(ctx context.Context, recordID string) error {
+18
appview/signup/requests.go
··· 102 103 return result.DID, nil 104 }
··· 102 103 return result.DID, nil 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 3 import ( 4 "bufio" 5 "encoding/json" 6 "errors" 7 "fmt" ··· 216 return 217 } 218 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 if s.cf == nil { 227 s.l.Error("cloudflare client is nil", "error", "Cloudflare integration is not enabled in configuration") 228 s.pages.Notice(w, "signup-error", "Account signup is currently disabled. DNS record creation is not available. Please contact support.") 229 return 230 } 231 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 - }) 239 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.") 242 return 243 } 244 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 - } 256 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)) 260 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) 265 } 266 - }() 267 - return 268 } 269 } 270 271 type turnstileResponse struct {
··· 2 3 import ( 4 "bufio" 5 + "context" 6 "encoding/json" 7 "errors" 8 "fmt" ··· 217 return 218 } 219 220 if s.cf == nil { 221 s.l.Error("cloudflare client is nil", "error", "Cloudflare integration is not enabled in configuration") 222 s.pages.Notice(w, "signup-error", "Account signup is currently disabled. DNS record creation is not available. Please contact support.") 223 return 224 } 225 226 + // Execute signup transactionally with rollback capability 227 + err = s.executeSignupTransaction(r.Context(), username, password, email, code, w) 228 if err != nil { 229 + // Error already logged and notice already sent 230 return 231 } 232 + } 233 + } 234 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 240 241 + success := false 242 + defer func() { 243 + if !success { 244 + s.l.Info("rolling back signup transaction", "username", username, "did", did) 245 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 + } 253 } 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 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 326 } 327 328 type turnstileResponse struct {