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.

email verification

Hailey (Mar 29, 2025, 5:20 PM -0700) 02dc7fbe d56b9154

+222 -18
+36
cmd/cocoon/main.go
··· 56 56 Required: true, 57 57 EnvVars: []string{"COCOON_RELAYS"}, 58 58 }, 59 + &cli.StringFlag{ 60 + Name: "smtp-user", 61 + Required: false, 62 + EnvVars: []string{"COCOON_SMTP_USER"}, 63 + }, 64 + &cli.StringFlag{ 65 + Name: "smtp-pass", 66 + Required: false, 67 + EnvVars: []string{"COCOON_SMTP_PASS"}, 68 + }, 69 + &cli.StringFlag{ 70 + Name: "smtp-host", 71 + Required: false, 72 + EnvVars: []string{"COCOON_SMTP_HOST"}, 73 + }, 74 + &cli.StringFlag{ 75 + Name: "smtp-port", 76 + Required: false, 77 + EnvVars: []string{"COCOON_SMTP_PORT"}, 78 + }, 79 + &cli.StringFlag{ 80 + Name: "smtp-email", 81 + Required: false, 82 + EnvVars: []string{"COCOON_SMTP_EMAIL"}, 83 + }, 84 + &cli.StringFlag{ 85 + Name: "smtp-name", 86 + Required: false, 87 + EnvVars: []string{"COCOON_SMTP_NAME"}, 88 + }, 59 89 }, 60 90 Commands: []*cli.Command{ 61 91 run, ··· 82 112 ContactEmail: cmd.String("contact-email"), 83 113 Version: Version, 84 114 Relays: cmd.StringSlice("relays"), 115 + SmtpUser: cmd.String("smtp-user"), 116 + SmtpPass: cmd.String("smtp-pass"), 117 + SmtpHost: cmd.String("smtp-host"), 118 + SmtpPort: cmd.String("smtp-port"), 119 + SmtpEmail: cmd.String("smtp-email"), 120 + SmtpName: cmd.String("smtp-name"), 85 121 }) 86 122 if err != nil { 87 123 fmt.Printf("error creating cocoon: %v", err)
+1
go.mod
··· 32 32 github.com/cespare/xxhash/v2 v2.2.0 // indirect 33 33 github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 34 34 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 35 + github.com/domodwyer/mailyak/v3 v3.6.2 // indirect 35 36 github.com/felixge/httpsnoop v1.0.4 // indirect 36 37 github.com/go-logr/logr v1.4.2 // indirect 37 38 github.com/go-logr/stdr v1.2.2 // indirect
+2
go.sum
··· 37 37 github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 38 38 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 39 39 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 40 + github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= 41 + github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= 40 42 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 41 43 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 42 44 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+10 -9
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 - Password string 16 - SigningKey []byte 17 - Rev string 18 - Root []byte 19 - Preferences []byte 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 20 21 } 21 22 22 23 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+42
server/handle_server_confirm_email.go
··· 1 + package server 2 + 3 + import ( 4 + "github.com/Azure/go-autorest/autorest/to" 5 + "github.com/haileyok/cocoon/internal/helpers" 6 + "github.com/haileyok/cocoon/models" 7 + "github.com/labstack/echo/v4" 8 + ) 9 + 10 + type ComAtprotoServerConfirmEmailRequest struct { 11 + Email string `json:"email" validate:"required"` 12 + Token string `json:"token" validate:"required"` 13 + } 14 + 15 + func (s *Server) handleServerConfirmEmail(e echo.Context) error { 16 + urepo := e.Get("repo").(*models.RepoActor) 17 + 18 + var req ComAtprotoServerConfirmEmailRequest 19 + if err := e.Bind(&req); err != nil { 20 + s.logger.Error("error binding", "error", err) 21 + return helpers.ServerError(e, nil) 22 + } 23 + 24 + if err := e.Validate(req); err != nil { 25 + return helpers.InputError(e, nil) 26 + } 27 + 28 + if urepo.EmailVerificationCode == nil { 29 + return helpers.InputError(e, to.StringPtr("ExpiredToken")) 30 + } 31 + 32 + if *urepo.EmailVerificationCode != req.Token { 33 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 34 + } 35 + 36 + if err := s.db.Exec("UPDATE repos SET email_verification_token = NULL, email_confirmed_at = NOW() WHERE did = ?", urepo.Repo.Did).Error; err != nil { 37 + s.logger.Error("error updating user", "error", err) 38 + return helpers.ServerError(e, nil) 39 + } 40 + 41 + return e.NoContent(200) 42 + }
+16 -5
server/handle_server_create_account.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 + "fmt" 6 7 "strings" 7 8 "time" 8 9 ··· 134 135 } 135 136 136 137 urepo := models.Repo{ 137 - Did: did, 138 - CreatedAt: time.Now(), 139 - Email: request.Email, 140 - Password: string(hashed), 141 - SigningKey: k.Bytes(), 138 + Did: did, 139 + CreatedAt: time.Now(), 140 + Email: request.Email, 141 + EmailVerificationCode: to.StringPtr(fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))), 142 + Password: string(hashed), 143 + SigningKey: k.Bytes(), 142 144 } 143 145 144 146 actor := models.Actor{ ··· 198 200 s.logger.Error("error creating new session", "error", err) 199 201 return helpers.ServerError(e, nil) 200 202 } 203 + 204 + go func() { 205 + if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil { 206 + s.logger.Error("error sending email verification email", "error", err) 207 + } 208 + if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil { 209 + s.logger.Error("error sending welcome email", "error", err) 210 + } 211 + }() 201 212 202 213 return e.JSON(200, ComAtprotoServerCreateAccountResponse{ 203 214 AccessJwt: sess.AccessToken,
+32
server/handle_server_request_email_confirmation.go
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 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 + func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error { 13 + urepo := e.Get("repo").(*models.RepoActor) 14 + 15 + if urepo.EmailConfirmedAt != nil { 16 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 17 + } 18 + 19 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6)) 20 + 21 + if err := s.db.Exec("UPDATE repos SET email_verification_code = ? WHERE did = ?", code, urepo.Repo.Did).Error; err != nil { 22 + s.logger.Error("error updating user", "error", err) 23 + return helpers.ServerError(e, nil) 24 + } 25 + 26 + if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil { 27 + s.logger.Error("error sending mail", "error", err) 28 + return helpers.ServerError(e, nil) 29 + } 30 + 31 + return e.NoContent(200) 32 + }
+48
server/mail.go
··· 1 + package server 2 + 3 + import "fmt" 4 + 5 + func (s *Server) sendWelcomeMail(email, handle string) error { 6 + s.mailLk.Lock() 7 + defer s.mailLk.Unlock() 8 + 9 + s.mail.To(email) 10 + s.mail.Subject("Welcome to " + s.config.Hostname) 11 + s.mail.Plain().Set(fmt.Sprintf("Welcome to %s! Your handle is %s.", email, handle)) 12 + 13 + if err := s.mail.Send(); err != nil { 14 + return err 15 + } 16 + 17 + return nil 18 + } 19 + 20 + func (s *Server) sendPasswordReset(email, handle, code string) error { 21 + s.mailLk.Lock() 22 + defer s.mailLk.Unlock() 23 + 24 + s.mail.To(email) 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)) 27 + 28 + if err := s.mail.Send(); err != nil { 29 + return err 30 + } 31 + 32 + return nil 33 + } 34 + 35 + func (s *Server) sendEmailVerification(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 verification for " + s.config.Hostname) 41 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email verification code is %s", handle, code)) 42 + 43 + if err := s.mail.Send(); err != nil { 44 + return err 45 + } 46 + 47 + return nil 48 + }
+35 -4
server/server.go
··· 7 7 "fmt" 8 8 "log/slog" 9 9 "net/http" 10 + "net/smtp" 10 11 "os" 11 12 "strings" 13 + "sync" 12 14 "time" 13 15 14 16 "github.com/Azure/go-autorest/autorest/to" ··· 17 19 "github.com/bluesky-social/indigo/events" 18 20 "github.com/bluesky-social/indigo/util" 19 21 "github.com/bluesky-social/indigo/xrpc" 22 + "github.com/domodwyer/mailyak/v3" 20 23 "github.com/go-playground/validator" 21 24 "github.com/golang-jwt/jwt/v4" 22 25 "github.com/haileyok/cocoon/identity" ··· 34 37 type Server struct { 35 38 http *http.Client 36 39 httpd *http.Server 40 + mail *mailyak.MailYak 41 + mailLk *sync.Mutex 37 42 echo *echo.Echo 38 43 db *gorm.DB 39 44 plcClient *plc.Client ··· 56 61 JwkPath string 57 62 ContactEmail string 58 63 Relays []string 64 + 65 + SmtpUser string 66 + SmtpPass string 67 + SmtpHost string 68 + SmtpPort string 69 + SmtpEmail string 70 + SmtpName string 59 71 } 60 72 61 73 type config struct { ··· 65 77 ContactEmail string 66 78 EnforcePeering bool 67 79 Relays []string 80 + SmtpEmail string 81 + SmtpName string 68 82 } 69 83 70 84 type CustomValidator struct { ··· 167 181 return helpers.InputError(e, to.StringPtr("ExpiredToken")) 168 182 } 169 183 170 - e.Set("did", claims["sub"]) 171 - 172 184 repo, err := s.getRepoActorByDid(claims["sub"].(string)) 173 185 if err != nil { 174 186 s.logger.Error("error fetching repo", "error", err) 175 187 return helpers.ServerError(e, nil) 176 188 } 189 + 177 190 e.Set("repo", repo) 178 - 191 + e.Set("did", claims["sub"]) 179 192 e.Set("token", tokenstr) 180 193 181 194 if err := next(e); err != nil { ··· 312 325 ContactEmail: args.ContactEmail, 313 326 EnforcePeering: false, 314 327 Relays: args.Relays, 328 + SmtpName: args.SmtpName, 329 + SmtpEmail: args.SmtpEmail, 315 330 }, 316 331 evtman: events.NewEventManager(events.NewMemPersister()), 317 332 passport: identity.NewPassport(h, identity.NewMemCache(10_000)), ··· 319 334 320 335 s.repoman = NewRepoMan(s) // TODO: this is way too lazy, stop it 321 336 337 + // TODO: should validate these args 338 + if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" { 339 + args.Logger.Warn("not enough smpt args were provided. mailing will not work for your server.") 340 + } else { 341 + mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost)) 342 + mail.From(s.config.SmtpEmail) 343 + mail.From(s.config.SmtpName) 344 + 345 + s.mail = mail 346 + s.mailLk = &sync.Mutex{} 347 + } 348 + 322 349 return s, nil 323 350 } 324 351 325 352 func (s *Server) addRoutes() { 353 + // random stuff 326 354 s.echo.GET("/", s.handleRoot) 327 355 s.echo.GET("/xrpc/_health", s.handleHealth) 328 356 s.echo.GET("/.well-known/did.json", s.handleWellKnown) ··· 353 381 s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleSessionMiddleware) 354 382 s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleSessionMiddleware) 355 383 s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleSessionMiddleware) 384 + s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleSessionMiddleware) 385 + s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleSessionMiddleware) 356 386 357 387 // repo 358 388 s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleSessionMiddleware) ··· 364 394 s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleSessionMiddleware) 365 395 s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleSessionMiddleware) 366 396 397 + // are there any routes that we should be allowing without auth? i dont think so but idk 367 398 s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 368 399 s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 369 400 } ··· 395 426 396 427 for _, relay := range s.config.Relays { 397 428 cli := xrpc.Client{Host: relay} 398 - atproto.SyncRequestCrawl(context.TODO(), &cli, &atproto.SyncRequestCrawl_Input{ 429 + atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{ 399 430 Hostname: s.config.Hostname, 400 431 }) 401 432 }