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 var req ComAtprotoServerDeleteAccountRequest
24 if err := e.Bind(&req); err != nil {
25 s.logger.Error("error binding", "error", err)
26 return helpers.ServerError(e, nil)
27 }
28
29 if err := e.Validate(&req); err != nil {
30 s.logger.Error("error validating", "error", err)
31 return helpers.ServerError(e, nil)
32 }
33
34 urepo, err := s.getRepoActorByDid(req.Did)
35 if err != nil {
36 s.logger.Error("error getting repo", "error", err)
37 return echo.NewHTTPError(400, "account not found")
38 }
39
40 if err := bcrypt.CompareHashAndPassword([]byte(urepo.Repo.Password), []byte(req.Password)); err != nil {
41 s.logger.Error("password mismatch", "error", err)
42 return echo.NewHTTPError(401, "Invalid did or password")
43 }
44
45 if urepo.Repo.AccountDeleteCode == nil || urepo.Repo.AccountDeleteCodeExpiresAt == nil {
46 s.logger.Error("no deletion token found for account")
47 return echo.NewHTTPError(400, map[string]interface{}{
48 "error": "InvalidToken",
49 "message": "Token is invalid",
50 })
51 }
52
53 if *urepo.Repo.AccountDeleteCode != req.Token {
54 s.logger.Error("deletion token mismatch")
55 return echo.NewHTTPError(400, map[string]interface{}{
56 "error": "InvalidToken",
57 "message": "Token is invalid",
58 })
59 }
60
61 if time.Now().UTC().After(*urepo.Repo.AccountDeleteCodeExpiresAt) {
62 s.logger.Error("deletion token expired")
63 return echo.NewHTTPError(400, map[string]interface{}{
64 "error": "ExpiredToken",
65 "message": "Token is expired",
66 })
67 }
68
69 tx := s.db.BeginDangerously()
70 if tx.Error != nil {
71 s.logger.Error("error starting transaction", "error", tx.Error)
72 return helpers.ServerError(e, nil)
73 }
74
75 if err := tx.Exec("DELETE FROM blocks WHERE did = ?", nil, req.Did).Error; err != nil {
76 tx.Rollback()
77 s.logger.Error("error deleting blocks", "error", err)
78 return helpers.ServerError(e, nil)
79 }
80
81 if err := tx.Exec("DELETE FROM records WHERE did = ?", nil, req.Did).Error; err != nil {
82 tx.Rollback()
83 s.logger.Error("error deleting records", "error", err)
84 return helpers.ServerError(e, nil)
85 }
86
87 if err := tx.Exec("DELETE FROM blobs WHERE did = ?", nil, req.Did).Error; err != nil {
88 tx.Rollback()
89 s.logger.Error("error deleting blobs", "error", err)
90 return helpers.ServerError(e, nil)
91 }
92
93 if err := tx.Exec("DELETE FROM tokens WHERE did = ?", nil, req.Did).Error; err != nil {
94 tx.Rollback()
95 s.logger.Error("error deleting tokens", "error", err)
96 return helpers.ServerError(e, nil)
97 }
98
99 if err := tx.Exec("DELETE FROM refresh_tokens WHERE did = ?", nil, req.Did).Error; err != nil {
100 tx.Rollback()
101 s.logger.Error("error deleting refresh tokens", "error", err)
102 return helpers.ServerError(e, nil)
103 }
104
105 if err := tx.Exec("DELETE FROM reserved_keys WHERE did = ?", nil, req.Did).Error; err != nil {
106 tx.Rollback()
107 s.logger.Error("error deleting reserved keys", "error", err)
108 return helpers.ServerError(e, nil)
109 }
110
111 if err := tx.Exec("DELETE FROM invite_codes WHERE did = ?", nil, req.Did).Error; err != nil {
112 tx.Rollback()
113 s.logger.Error("error deleting invite codes", "error", err)
114 return helpers.ServerError(e, nil)
115 }
116
117 if err := tx.Exec("DELETE FROM actors WHERE did = ?", nil, req.Did).Error; err != nil {
118 tx.Rollback()
119 s.logger.Error("error deleting actor", "error", err)
120 return helpers.ServerError(e, nil)
121 }
122
123 if err := tx.Exec("DELETE FROM repos WHERE did = ?", nil, req.Did).Error; err != nil {
124 tx.Rollback()
125 s.logger.Error("error deleting repo", "error", err)
126 return helpers.ServerError(e, nil)
127 }
128
129 if err := tx.Commit().Error; err != nil {
130 s.logger.Error("error committing transaction", "error", err)
131 return helpers.ServerError(e, nil)
132 }
133
134 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
135 RepoAccount: &atproto.SyncSubscribeRepos_Account{
136 Active: false,
137 Did: req.Did,
138 Status: to.StringPtr("deleted"),
139 Seq: time.Now().UnixMicro(),
140 Time: time.Now().Format(util.ISO8601),
141 },
142 })
143
144 return e.NoContent(200)
145}