forked from hailey.at/cocoon
An atproto PDS written in Go

update email, reset password

+2 -2
README.md
··· 32 32 33 33 - [ ] com.atproto.server.activateAccount 34 34 - [ ] com.atproto.server.checkAccountStatus 35 - - [ ] com.atproto.server.confirmEmail 35 + - [x] com.atproto.server.confirmEmail 36 36 - [x] com.atproto.server.createAccount 37 37 - [ ] com.atproto.server.deactivateAccount 38 38 - [ ] com.atproto.server.deleteAccount ··· 43 43 - [ ] com.atproto.server.listAppPasswords 44 44 - [x] com.atproto.server.refreshSession 45 45 - [ ] com.atproto.server.requestAccountDelete 46 - - [ ] com.atproto.server.requestEmailConfirmation 46 + - [x] com.atproto.server.requestEmailConfirmation 47 47 - [ ] com.atproto.server.requestEmailUpdate 48 48 - [ ] com.atproto.server.requestPasswordReset 49 49 - [ ] com.atproto.server.reserveSigningKey
+15 -10
models/models.go
··· 8 8 ) 9 9 10 10 type Repo struct { 11 - Did string `gorm:"primaryKey"` 12 - CreatedAt time.Time 13 - Email string `gorm:"uniqueIndex"` 14 - EmailConfirmedAt *time.Time 15 - EmailVerificationCode *string 16 - Password string 17 - SigningKey []byte 18 - Rev string 19 - Root []byte 20 - Preferences []byte 11 + Did string `gorm:"primaryKey"` 12 + CreatedAt time.Time 13 + Email string `gorm:"uniqueIndex"` 14 + EmailConfirmedAt *time.Time 15 + EmailVerificationCode *string 16 + EmailVerificationCodeExpiresAt *time.Time 17 + EmailUpdateCode *string 18 + EmailUpdateCodeExpiresAt *time.Time 19 + PasswordResetCode *string 20 + PasswordResetCodeExpiresAt *time.Time 21 + Password string 22 + SigningKey []byte 23 + Rev string 24 + Root []byte 25 + Preferences []byte 21 26 } 22 27 23 28 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+6 -2
server/handle_server_confirm_email.go
··· 27 27 return helpers.InputError(e, nil) 28 28 } 29 29 30 - if urepo.EmailVerificationCode == nil { 30 + if urepo.EmailVerificationCode == nil || urepo.EmailVerificationCodeExpiresAt == nil { 31 31 return helpers.InputError(e, to.StringPtr("ExpiredToken")) 32 32 } 33 33 ··· 35 35 return helpers.InputError(e, to.StringPtr("InvalidToken")) 36 36 } 37 37 38 + if time.Now().UTC().After(*urepo.EmailVerificationCodeExpiresAt) { 39 + return helpers.InputError(e, to.StringPtr("ExpiredToken")) 40 + } 41 + 38 42 now := time.Now().UTC() 39 43 40 - if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_confirmed_at = ? WHERE did = ?", now, urepo.Repo.Did).Error; err != nil { 44 + if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", now, urepo.Repo.Did).Error; err != nil { 41 45 s.logger.Error("error updating user", "error", err) 42 46 return helpers.ServerError(e, nil) 43 47 }
+3 -1
server/handle_server_request_email_confirmation.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "time" 5 6 6 7 "github.com/Azure/go-autorest/autorest/to" 7 8 "github.com/haileyok/cocoon/internal/helpers" ··· 17 18 } 18 19 19 20 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6)) 21 + eat := time.Now().Add(10 * time.Minute).UTC() 20 22 21 - if err := s.db.Exec("UPDATE repos SET email_verification_code = ? WHERE did = ?", code, urepo.Repo.Did).Error; err != nil { 23 + if err := s.db.Exec("UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", code, eat, urepo.Repo.Did).Error; err != nil { 22 24 s.logger.Error("error updating user", "error", err) 23 25 return helpers.ServerError(e, nil) 24 26 }
+29
server/handle_server_request_email_update.go
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error { 13 + urepo := e.Get("repo").(*models.RepoActor) 14 + 15 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6)) 16 + eat := time.Now().Add(10 * time.Minute).UTC() 17 + 18 + if err := s.db.Exec("UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", code, eat, urepo.Repo.Did).Error; err != nil { 19 + s.logger.Error("error updating repo", "error", err) 20 + return helpers.ServerError(e, nil) 21 + } 22 + 23 + if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil { 24 + s.logger.Error("error sending email", "error", err) 25 + return helpers.ServerError(e, nil) 26 + } 27 + 28 + return e.NoContent(200) 29 + }
+29
server/handle_server_request_password_reset.go
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (s *Server) handleServerRequestPasswordReset(e echo.Context) error { 13 + urepo := e.Get("repo").(*models.RepoActor) 14 + 15 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6)) 16 + eat := time.Now().Add(10 * time.Minute).UTC() 17 + 18 + if err := s.db.Exec("UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", code, eat, urepo.Repo.Did).Error; err != nil { 19 + s.logger.Error("error updating repo", "error", err) 20 + return helpers.ServerError(e, nil) 21 + } 22 + 23 + if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil { 24 + s.logger.Error("error sending email", "error", err) 25 + return helpers.ServerError(e, nil) 26 + } 27 + 28 + return e.NoContent(200) 29 + }
+55
server/handle_server_reset_password.go
··· 1 + package server 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/Azure/go-autorest/autorest/to" 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/labstack/echo/v4" 10 + "golang.org/x/crypto/bcrypt" 11 + ) 12 + 13 + type ComAtprotoServerResetPasswordRequest struct { 14 + Token string `json:"token" validate:"required"` 15 + Password string `json:"password" validate:"required"` 16 + } 17 + 18 + func (s *Server) handleServerResetPassword(e echo.Context) error { 19 + urepo := e.Get("repo").(*models.RepoActor) 20 + 21 + var req ComAtprotoServerResetPasswordRequest 22 + if err := e.Bind(&req); err != nil { 23 + s.logger.Error("error binding", "error", err) 24 + return helpers.ServerError(e, nil) 25 + } 26 + 27 + if err := e.Validate(req); err != nil { 28 + return helpers.InputError(e, nil) 29 + } 30 + 31 + if urepo.PasswordResetCode == nil || urepo.PasswordResetCodeExpiresAt == nil { 32 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 33 + } 34 + 35 + if *urepo.PasswordResetCode != req.Token { 36 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 37 + } 38 + 39 + if time.Now().UTC().After(*urepo.PasswordResetCodeExpiresAt) { 40 + return helpers.InputError(e, to.StringPtr("ExpiredToken")) 41 + } 42 + 43 + hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10) 44 + if err != nil { 45 + s.logger.Error("error creating hash", "error", err) 46 + return helpers.ServerError(e, nil) 47 + } 48 + 49 + if err := s.db.Exec("UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", hash, urepo.Repo.Did).Error; err != nil { 50 + s.logger.Error("error updating repo", "error", err) 51 + return helpers.ServerError(e, nil) 52 + } 53 + 54 + return e.NoContent(200) 55 + }
+49
server/handle_server_update_email.go
··· 1 + package server 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/Azure/go-autorest/autorest/to" 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + type ComAtprotoServerUpdateEmailRequest struct { 13 + Email string `json:"email" validate:"required"` 14 + EmailAuthFactor bool `json:"emailAuthFactor"` 15 + Token string `json:"token" validate:"required"` 16 + } 17 + 18 + func (s *Server) handleServerUpdateEmail(e echo.Context) error { 19 + urepo := e.Get("repo").(*models.RepoActor) 20 + 21 + var req ComAtprotoServerUpdateEmailRequest 22 + if err := e.Bind(&req); err != nil { 23 + s.logger.Error("error binding", "error", err) 24 + return helpers.ServerError(e, nil) 25 + } 26 + 27 + if err := e.Validate(req); err != nil { 28 + return helpers.InputError(e, nil) 29 + } 30 + 31 + if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil { 32 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 33 + } 34 + 35 + if *urepo.EmailUpdateCode != req.Token { 36 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 37 + } 38 + 39 + if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) { 40 + return helpers.InputError(e, to.StringPtr("ExpiredToken")) 41 + } 42 + 43 + if err := s.db.Exec("UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email = ? WHERE did = ?", req.Email, urepo.Repo.Did).Error; err != nil { 44 + s.logger.Error("error updating repo", "error", err) 45 + return helpers.ServerError(e, nil) 46 + } 47 + 48 + return e.NoContent(200) 49 + }
+17 -2
server/mail.go
··· 23 23 24 24 s.mail.To(email) 25 25 s.mail.Subject("Password reset for " + s.config.Hostname) 26 - s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your password reset code is %s.", handle, code)) 26 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your password reset code is %s. This code will expire in ten minutes.", handle, code)) 27 + 28 + if err := s.mail.Send(); err != nil { 29 + return err 30 + } 31 + 32 + return nil 33 + } 34 + 35 + func (s *Server) sendEmailUpdate(email, handle, code string) error { 36 + s.mailLk.Lock() 37 + defer s.mailLk.Unlock() 38 + 39 + s.mail.To(email) 40 + s.mail.Subject("Email update for " + s.config.Hostname) 41 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email update code is %s. This code will expire in ten minutes.", handle, code)) 27 42 28 43 if err := s.mail.Send(); err != nil { 29 44 return err ··· 38 53 39 54 s.mail.To(email) 40 55 s.mail.Subject("Email verification for " + s.config.Hostname) 41 - s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email verification code is %s", handle, code)) 56 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email verification code is %s. This code will expire in ten minutes.", handle, code)) 42 57 43 58 if err := s.mail.Send(); err != nil { 44 59 return err
+4
server/server.go
··· 383 383 s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleSessionMiddleware) 384 384 s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleSessionMiddleware) 385 385 s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleSessionMiddleware) 386 + s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset, s.handleSessionMiddleware) 387 + s.echo.POST("/xrpc/com.atproto.server.requestEmailUpdate", s.handleServerRequestEmailUpdate, s.handleSessionMiddleware) 388 + s.echo.POST("/xrpc/com.atproto.server.resetPassword", s.handleServerResetPassword, s.handleSessionMiddleware) 389 + s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleSessionMiddleware) 386 390 387 391 // repo 388 392 s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleSessionMiddleware)