+4
-4
appview/dns/cloudflare.go
+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
+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
+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 {