A fork of the Cocoon PDS but being made more distributed.
11

Configure Feed

Select the types of activity you want to include in your feed.

update email, reset password

Hailey (Mar 29, 2025, 6:02 PM -0700) 9a610af4 3aee1775

+209 -17
+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)