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