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