this repo has no description
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/atcrypto" 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/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:"omitempty"` 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 ctx := e.Request().Context() 40 41 var request ComAtprotoServerCreateAccountRequest 42 43 if err := e.Bind(&request); err != nil { 44 s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) 45 return helpers.ServerError(e, nil) 46 } 47 48 request.Handle = strings.ToLower(request.Handle) 49 50 if err := e.Validate(request); err != nil { 51 s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err) 52 53 var verr ValidationError 54 if errors.As(err, &verr) { 55 if verr.Field == "Email" { 56 // TODO: what is this supposed to be? `InvalidEmail` isn't listed in doc 57 return helpers.InputError(e, to.StringPtr("InvalidEmail")) 58 } 59 60 if verr.Field == "Handle" { 61 return helpers.InputError(e, to.StringPtr("InvalidHandle")) 62 } 63 64 if verr.Field == "Password" { 65 return helpers.InputError(e, to.StringPtr("InvalidPassword")) 66 } 67 68 if verr.Field == "InviteCode" { 69 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 70 } 71 } 72 } 73 74 var signupDid string 75 if request.Did != nil { 76 signupDid = *request.Did 77 78 token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1)) 79 if token == "" { 80 return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did")) 81 } 82 authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount") 83 84 if err != nil { 85 s.logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err) 86 return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token")) 87 } 88 89 if authDid != signupDid { 90 return helpers.ForbiddenError(e, to.StringPtr("auth did did not match signup did")) 91 } 92 } 93 94 // see if the handle is already taken 95 actor, err := s.getActorByHandle(ctx, request.Handle) 96 if err != nil && err != gorm.ErrRecordNotFound { 97 s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err) 98 return helpers.ServerError(e, nil) 99 } 100 if err == nil && actor.Did != signupDid { 101 return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 102 } 103 104 if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != signupDid { 105 return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 106 } 107 108 var ic models.InviteCode 109 if s.config.RequireInvite { 110 if strings.TrimSpace(request.InviteCode) == "" { 111 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 112 } 113 114 if err := s.db.Raw(ctx, "SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 115 if err == gorm.ErrRecordNotFound { 116 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 117 } 118 s.logger.Error("error getting invite code from db", "error", err) 119 return helpers.ServerError(e, nil) 120 } 121 122 if ic.RemainingUseCount < 1 { 123 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 124 } 125 } 126 127 // see if the email is already taken 128 existingRepo, err := s.getRepoByEmail(ctx, request.Email) 129 if err != nil && err != gorm.ErrRecordNotFound { 130 s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err) 131 return helpers.ServerError(e, nil) 132 } 133 if err == nil && existingRepo.Did != signupDid { 134 return helpers.InputError(e, to.StringPtr("EmailNotAvailable")) 135 } 136 137 // TODO: unsupported domains 138 139 var k *atcrypto.PrivateKeyK256 140 141 if signupDid != "" { 142 reservedKey, err := s.getReservedKey(ctx, signupDid) 143 if err != nil { 144 s.logger.Error("error looking up reserved key", "error", err) 145 } 146 if reservedKey != nil { 147 k, err = atcrypto.ParsePrivateBytesK256(reservedKey.PrivateKey) 148 if err != nil { 149 s.logger.Error("error parsing reserved key", "error", err) 150 k = nil 151 } else { 152 defer func() { 153 if delErr := s.deleteReservedKey(ctx, reservedKey.KeyDid, reservedKey.Did); delErr != nil { 154 s.logger.Error("error deleting reserved key", "error", delErr) 155 } 156 }() 157 } 158 } 159 } 160 161 if k == nil { 162 k, err = atcrypto.GeneratePrivateKeyK256() 163 if err != nil { 164 s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 165 return helpers.ServerError(e, nil) 166 } 167 } 168 169 if signupDid == "" { 170 did, op, err := s.plcClient.CreateDID(k, "", request.Handle) 171 if err != nil { 172 s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 173 return helpers.ServerError(e, nil) 174 } 175 176 if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil { 177 s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err) 178 return helpers.ServerError(e, nil) 179 } 180 signupDid = did 181 } 182 183 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) 184 if err != nil { 185 s.logger.Error("error hashing password", "error", err) 186 return helpers.ServerError(e, nil) 187 } 188 189 urepo := models.Repo{ 190 Did: signupDid, 191 CreatedAt: time.Now(), 192 Email: request.Email, 193 EmailVerificationCode: to.StringPtr(fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))), 194 Password: string(hashed), 195 SigningKey: k.Bytes(), 196 } 197 198 if actor == nil { 199 actor = &models.Actor{ 200 Did: signupDid, 201 Handle: request.Handle, 202 } 203 204 if err := s.db.Create(ctx, &urepo, nil).Error; err != nil { 205 s.logger.Error("error inserting new repo", "error", err) 206 return helpers.ServerError(e, nil) 207 } 208 209 if err := s.db.Create(ctx, &actor, nil).Error; err != nil { 210 s.logger.Error("error inserting new actor", "error", err) 211 return helpers.ServerError(e, nil) 212 } 213 } else { 214 if err := s.db.Save(ctx, &actor, nil).Error; err != nil { 215 s.logger.Error("error inserting new actor", "error", err) 216 return helpers.ServerError(e, nil) 217 } 218 } 219 220 if request.Did == nil || *request.Did == "" { 221 bs := s.getBlockstore(signupDid) 222 r := repo.NewRepo(context.TODO(), signupDid, bs) 223 224 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 225 if err != nil { 226 s.logger.Error("error committing", "error", err) 227 return helpers.ServerError(e, nil) 228 } 229 230 if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil { 231 s.logger.Error("error updating repo after commit", "error", err) 232 return helpers.ServerError(e, nil) 233 } 234 235 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 236 RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ 237 Did: urepo.Did, 238 Handle: to.StringPtr(request.Handle), 239 Seq: time.Now().UnixMicro(), // TODO: no 240 Time: time.Now().Format(util.ISO8601), 241 }, 242 }) 243 } 244 245 if s.config.RequireInvite { 246 if err := s.db.Raw(ctx, "UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 247 s.logger.Error("error decrementing use count", "error", err) 248 return helpers.ServerError(e, nil) 249 } 250 } 251 252 sess, err := s.createSession(ctx, &urepo) 253 if err != nil { 254 s.logger.Error("error creating new session", "error", err) 255 return helpers.ServerError(e, nil) 256 } 257 258 go func() { 259 if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil { 260 s.logger.Error("error sending email verification email", "error", err) 261 } 262 if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil { 263 s.logger.Error("error sending welcome email", "error", err) 264 } 265 }() 266 267 return e.JSON(200, ComAtprotoServerCreateAccountResponse{ 268 AccessJwt: sess.AccessToken, 269 RefreshJwt: sess.RefreshToken, 270 Handle: request.Handle, 271 Did: signupDid, 272 }) 273}