1package server
2
3import (
4 "context"
5 "time"
6
7 "github.com/Azure/go-autorest/autorest/to"
8 "github.com/bluesky-social/indigo/api/atproto"
9 "github.com/bluesky-social/indigo/events"
10 "github.com/bluesky-social/indigo/util"
11 "github.com/haileyok/cocoon/internal/helpers"
12 "github.com/labstack/echo/v4"
13 "golang.org/x/crypto/bcrypt"
14)
15
16type ComAtprotoServerDeleteAccountRequest struct {
17 Did string `json:"did" validate:"required"`
18 Password string `json:"password" validate:"required"`
19 Token string `json:"token" validate:"required"`
20}
21
22func (s *Server) handleServerDeleteAccount(e echo.Context) error {
23 ctx := e.Request().Context()
24 logger := s.logger.With("name", "handleServerDeleteAccount")
25
26 var req ComAtprotoServerDeleteAccountRequest
27 if err := e.Bind(&req); err != nil {
28 logger.Error("error binding", "error", err)
29 return helpers.ServerError(e, nil)
30 }
31
32 if err := e.Validate(&req); err != nil {
33 logger.Error("error validating", "error", err)
34 return helpers.ServerError(e, nil)
35 }
36
37 urepo, err := s.getRepoActorByDid(ctx, req.Did)
38 if err != nil {
39 logger.Error("error getting repo", "error", err)
40 return echo.NewHTTPError(400, "account not found")
41 }
42
43 if err := bcrypt.CompareHashAndPassword([]byte(urepo.Repo.Password), []byte(req.Password)); err != nil {
44 logger.Error("password mismatch", "error", err)
45 return echo.NewHTTPError(401, "Invalid did or password")
46 }
47
48 if urepo.Repo.AccountDeleteCode == nil || urepo.Repo.AccountDeleteCodeExpiresAt == nil {
49 logger.Error("no deletion token found for account")
50 return echo.NewHTTPError(400, map[string]interface{}{
51 "error": "InvalidToken",
52 "message": "Token is invalid",
53 })
54 }
55
56 if *urepo.Repo.AccountDeleteCode != req.Token {
57 logger.Error("deletion token mismatch")
58 return echo.NewHTTPError(400, map[string]interface{}{
59 "error": "InvalidToken",
60 "message": "Token is invalid",
61 })
62 }
63
64 if time.Now().UTC().After(*urepo.Repo.AccountDeleteCodeExpiresAt) {
65 logger.Error("deletion token expired")
66 return echo.NewHTTPError(400, map[string]interface{}{
67 "error": "ExpiredToken",
68 "message": "Token is expired",
69 })
70 }
71
72 tx := s.db.BeginDangerously(ctx)
73 if tx.Error != nil {
74 logger.Error("error starting transaction", "error", tx.Error)
75 return helpers.ServerError(e, nil)
76 }
77
78 status := "error"
79 func() {
80 if status == "error" {
81 if err := tx.Rollback().Error; err != nil {
82 logger.Error("error rolling back after delete failure", "err", err)
83 }
84 }
85 }()
86
87 if err := tx.Exec("DELETE FROM blocks WHERE did = ?", nil, req.Did).Error; err != nil {
88 logger.Error("error deleting blocks", "error", err)
89 return helpers.ServerError(e, nil)
90 }
91
92 if err := tx.Exec("DELETE FROM records WHERE did = ?", nil, req.Did).Error; err != nil {
93 logger.Error("error deleting records", "error", err)
94 return helpers.ServerError(e, nil)
95 }
96
97 if err := tx.Exec("DELETE FROM blobs WHERE did = ?", nil, req.Did).Error; err != nil {
98 logger.Error("error deleting blobs", "error", err)
99 return helpers.ServerError(e, nil)
100 }
101
102 if err := tx.Exec("DELETE FROM tokens WHERE did = ?", nil, req.Did).Error; err != nil {
103 logger.Error("error deleting tokens", "error", err)
104 return helpers.ServerError(e, nil)
105 }
106
107 if err := tx.Exec("DELETE FROM refresh_tokens WHERE did = ?", nil, req.Did).Error; err != nil {
108 logger.Error("error deleting refresh tokens", "error", err)
109 return helpers.ServerError(e, nil)
110 }
111
112 if err := tx.Exec("DELETE FROM reserved_keys WHERE did = ?", nil, req.Did).Error; err != nil {
113 logger.Error("error deleting reserved keys", "error", err)
114 return helpers.ServerError(e, nil)
115 }
116
117 if err := tx.Exec("DELETE FROM invite_codes WHERE did = ?", nil, req.Did).Error; err != nil {
118 logger.Error("error deleting invite codes", "error", err)
119 return helpers.ServerError(e, nil)
120 }
121
122 if err := tx.Exec("DELETE FROM actors WHERE did = ?", nil, req.Did).Error; err != nil {
123 logger.Error("error deleting actor", "error", err)
124 return helpers.ServerError(e, nil)
125 }
126
127 if err := tx.Exec("DELETE FROM repos WHERE did = ?", nil, req.Did).Error; err != nil {
128 logger.Error("error deleting repo", "error", err)
129 return helpers.ServerError(e, nil)
130 }
131
132 status = "ok"
133
134 if err := tx.Commit().Error; err != nil {
135 logger.Error("error committing transaction", "error", err)
136 return helpers.ServerError(e, nil)
137 }
138
139 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
140 RepoAccount: &atproto.SyncSubscribeRepos_Account{
141 Active: false,
142 Did: req.Did,
143 Status: to.StringPtr("deleted"),
144 Seq: time.Now().UnixMicro(),
145 Time: time.Now().Format(util.ISO8601),
146 },
147 })
148
149 return e.NoContent(200)
150}