1package server
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/Azure/go-autorest/autorest/to"
11 "github.com/bluesky-social/indigo/api/atproto"
12 "github.com/bluesky-social/indigo/atproto/crypto"
13 "github.com/bluesky-social/indigo/events"
14 "github.com/bluesky-social/indigo/repo"
15 "github.com/bluesky-social/indigo/util"
16 "github.com/haileyok/cocoon/blockstore"
17 "github.com/haileyok/cocoon/internal/helpers"
18 "github.com/haileyok/cocoon/models"
19 "github.com/labstack/echo/v4"
20 "golang.org/x/crypto/bcrypt"
21 "gorm.io/gorm"
22)
23
24type ComAtprotoServerCreateAccountRequest struct {
25 Email string `json:"email" validate:"required,email"`
26 Handle string `json:"handle" validate:"required,atproto-handle"`
27 Did *string `json:"did" validate:"atproto-did"`
28 Password string `json:"password" validate:"required"`
29 InviteCode string `json:"inviteCode" validate:"required"`
30}
31
32type ComAtprotoServerCreateAccountResponse struct {
33 AccessJwt string `json:"accessJwt"`
34 RefreshJwt string `json:"refreshJwt"`
35 Handle string `json:"handle"`
36 Did string `json:"did"`
37}
38
39func (s *Server) handleCreateAccount(e echo.Context) error {
40 var request ComAtprotoServerCreateAccountRequest
41
42 if err := e.Bind(&request); err != nil {
43 s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
44 return helpers.ServerError(e, nil)
45 }
46
47 request.Handle = strings.ToLower(request.Handle)
48
49 if err := e.Validate(request); err != nil {
50 s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
51
52 var verr ValidationError
53 if errors.As(err, &verr) {
54 if verr.Field == "Email" {
55 // TODO: what is this supposed to be? `InvalidEmail` isn't listed in doc
56 return helpers.InputError(e, to.StringPtr("InvalidEmail"))
57 }
58
59 if verr.Field == "Handle" {
60 return helpers.InputError(e, to.StringPtr("InvalidHandle"))
61 }
62
63 if verr.Field == "Password" {
64 return helpers.InputError(e, to.StringPtr("InvalidPassword"))
65 }
66
67 if verr.Field == "InviteCode" {
68 return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
69 }
70 }
71 }
72
73 // see if the handle is already taken
74 _, err := s.getActorByHandle(request.Handle)
75 if err != nil && err != gorm.ErrRecordNotFound {
76 s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
77 return helpers.ServerError(e, nil)
78 }
79 if err == nil {
80 return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
81 }
82
83 if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != "" {
84 return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
85 }
86
87 var ic models.InviteCode
88 if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", request.InviteCode).Scan(&ic).Error; err != nil {
89 if err == gorm.ErrRecordNotFound {
90 return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
91 }
92 s.logger.Error("error getting invite code from db", "error", err)
93 return helpers.ServerError(e, nil)
94 }
95
96 if ic.RemainingUseCount < 1 {
97 return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
98 }
99
100 // see if the email is already taken
101 _, err = s.getRepoByEmail(request.Email)
102 if err != nil && err != gorm.ErrRecordNotFound {
103 s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
104 return helpers.ServerError(e, nil)
105 }
106 if err == nil {
107 return helpers.InputError(e, to.StringPtr("EmailNotAvailable"))
108 }
109
110 // TODO: unsupported domains
111
112 // TODO: did stuff
113
114 k, err := crypto.GeneratePrivateKeyK256()
115 if err != nil {
116 s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
117 return helpers.ServerError(e, nil)
118 }
119
120 did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
121 if err != nil {
122 s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
123 return helpers.ServerError(e, nil)
124 }
125
126 if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
127 s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
128 return helpers.ServerError(e, nil)
129 }
130
131 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
132 if err != nil {
133 s.logger.Error("error hashing password", "error", err)
134 return helpers.ServerError(e, nil)
135 }
136
137 urepo := models.Repo{
138 Did: did,
139 CreatedAt: time.Now(),
140 Email: request.Email,
141 EmailVerificationCode: to.StringPtr(fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))),
142 Password: string(hashed),
143 SigningKey: k.Bytes(),
144 }
145
146 actor := models.Actor{
147 Did: did,
148 Handle: request.Handle,
149 }
150
151 if err := s.db.Create(&urepo).Error; err != nil {
152 s.logger.Error("error inserting new repo", "error", err)
153 return helpers.ServerError(e, nil)
154 }
155
156 bs := blockstore.New(did, s.db)
157 r := repo.NewRepo(context.TODO(), did, bs)
158
159 root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
160 if err != nil {
161 s.logger.Error("error committing", "error", err)
162 return helpers.ServerError(e, nil)
163 }
164
165 if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil {
166 s.logger.Error("error updating repo after commit", "error", err)
167 return helpers.ServerError(e, nil)
168 }
169
170 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
171 RepoHandle: &atproto.SyncSubscribeRepos_Handle{
172 Did: urepo.Did,
173 Handle: request.Handle,
174 Seq: time.Now().UnixMicro(), // TODO: no
175 Time: time.Now().Format(util.ISO8601),
176 },
177 })
178
179 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
180 RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
181 Did: urepo.Did,
182 Handle: to.StringPtr(request.Handle),
183 Seq: time.Now().UnixMicro(), // TODO: no
184 Time: time.Now().Format(util.ISO8601),
185 },
186 })
187
188 if err := s.db.Create(&actor).Error; err != nil {
189 s.logger.Error("error inserting new actor", "error", err)
190 return helpers.ServerError(e, nil)
191 }
192
193 if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", request.InviteCode).Scan(&ic).Error; err != nil {
194 s.logger.Error("error decrementing use count", "error", err)
195 return helpers.ServerError(e, nil)
196 }
197
198 sess, err := s.createSession(&urepo)
199 if err != nil {
200 s.logger.Error("error creating new session", "error", err)
201 return helpers.ServerError(e, nil)
202 }
203
204 go func() {
205 if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
206 s.logger.Error("error sending email verification email", "error", err)
207 }
208 if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
209 s.logger.Error("error sending welcome email", "error", err)
210 }
211 }()
212
213 return e.JSON(200, ComAtprotoServerCreateAccountResponse{
214 AccessJwt: sess.AccessToken,
215 RefreshJwt: sess.RefreshToken,
216 Handle: request.Handle,
217 Did: did,
218 })
219}