An atproto PDS written in Go
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}