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

implement createInviteCode & createInviteCodes (#4)

Co-authored-by: hailey <hailey@blueskyweb.xyz>

authored by juli.ee hailey and committed by GitHub a4c3adb2 5f5b61b7

+8 -2
.env.example
··· 1 - COCOON_DID= 2 - COCOON_HOSTNAME=
··· 1 + COCOON_DID="did:web:cocoon.example.com" 2 + COCOON_HOSTNAME="cocoon.example.com" 3 + COCOON_ROTATION_KEY_PATH="./rotation.key" 4 + COCOON_JWK_PATH="./jwk.key" 5 + COCOON_CONTACT_EMAIL="me@example.com" 6 + COCOON_RELAYS=https://bsky.network 7 + # Generate with `openssl rand -hex 16` 8 + COCOON_ADMIN_PASSWORD=
+3 -1
README.md
··· 22 - [x] com.atproto.repo.applyWrites 23 - [x] com.atproto.repo.createRecord 24 - [x] com.atproto.repo.putRecord 25 - - [ ] com.atproto.repo.deleteRecord 26 - [x] com.atproto.repo.describeRepo 27 - [x] com.atproto.repo.getRecord 28 - [ ] com.atproto.repo.importRepo ··· 34 - [ ] com.atproto.server.checkAccountStatus 35 - [x] com.atproto.server.confirmEmail 36 - [x] com.atproto.server.createAccount 37 - [ ] com.atproto.server.deactivateAccount 38 - [ ] com.atproto.server.deleteAccount 39 - [x] com.atproto.server.deleteSession
··· 22 - [x] com.atproto.repo.applyWrites 23 - [x] com.atproto.repo.createRecord 24 - [x] com.atproto.repo.putRecord 25 + - [x] com.atproto.repo.deleteRecord 26 - [x] com.atproto.repo.describeRepo 27 - [x] com.atproto.repo.getRecord 28 - [ ] com.atproto.repo.importRepo ··· 34 - [ ] com.atproto.server.checkAccountStatus 35 - [x] com.atproto.server.confirmEmail 36 - [x] com.atproto.server.createAccount 37 + - [x] com.atproto.server.createInviteCode 38 + - [x] com.atproto.server.createInviteCodes 39 - [ ] com.atproto.server.deactivateAccount 40 - [ ] com.atproto.server.deleteAccount 41 - [x] com.atproto.server.deleteSession
+9 -1
cmd/cocoon/main.go
··· 57 EnvVars: []string{"COCOON_RELAYS"}, 58 }, 59 &cli.StringFlag{ 60 Name: "smtp-user", 61 Required: false, 62 EnvVars: []string{"COCOON_SMTP_USER"}, ··· 94 Version: Version, 95 } 96 97 - app.Run(os.Args) 98 } 99 100 var run = &cli.Command{ ··· 112 ContactEmail: cmd.String("contact-email"), 113 Version: Version, 114 Relays: cmd.StringSlice("relays"), 115 SmtpUser: cmd.String("smtp-user"), 116 SmtpPass: cmd.String("smtp-pass"), 117 SmtpHost: cmd.String("smtp-host"),
··· 57 EnvVars: []string{"COCOON_RELAYS"}, 58 }, 59 &cli.StringFlag{ 60 + Name: "admin-password", 61 + Required: true, 62 + EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, 63 + }, 64 + &cli.StringFlag{ 65 Name: "smtp-user", 66 Required: false, 67 EnvVars: []string{"COCOON_SMTP_USER"}, ··· 99 Version: Version, 100 } 101 102 + if err := app.Run(os.Args); err != nil { 103 + fmt.Printf("Error: %v\n", err) 104 + } 105 } 106 107 var run = &cli.Command{ ··· 119 ContactEmail: cmd.String("contact-email"), 120 Version: Version, 121 Relays: cmd.StringSlice("relays"), 122 + AdminPassword: cmd.String("admin-password"), 123 SmtpUser: cmd.String("smtp-user"), 124 SmtpPass: cmd.String("smtp-pass"), 125 SmtpHost: cmd.String("smtp-host"),
+39 -4
server/handle_server_create_invite_code.go
··· 2 3 import ( 4 "github.com/google/uuid" 5 "github.com/haileyok/cocoon/models" 6 "github.com/labstack/echo/v4" 7 ) 8 9 func (s *Server) handleCreateInviteCode(e echo.Context) error { 10 - ic := models.InviteCode{ 11 - Code: uuid.NewString(), 12 } 13 14 - return e.JSON(200, map[string]string{ 15 - "code": ic.Code, 16 }) 17 }
··· 2 3 import ( 4 "github.com/google/uuid" 5 + "github.com/haileyok/cocoon/internal/helpers" 6 "github.com/haileyok/cocoon/models" 7 "github.com/labstack/echo/v4" 8 ) 9 10 + type ComAtprotoServerCreateInviteCodeRequest struct { 11 + UseCount int `json:"useCount" validate:"required"` 12 + ForAccount *string `json:"forAccount,omitempty"` 13 + } 14 + 15 + type ComAtprotoServerCreateInviteCodeResponse struct { 16 + Code string `json:"code"` 17 + } 18 + 19 func (s *Server) handleCreateInviteCode(e echo.Context) error { 20 + var req ComAtprotoServerCreateInviteCodeRequest 21 + if err := e.Bind(&req); err != nil { 22 + s.logger.Error("error binding", "error", err) 23 + return helpers.ServerError(e, nil) 24 + } 25 + 26 + if err := e.Validate(req); err != nil { 27 + s.logger.Error("error validating", "error", err) 28 + return helpers.InputError(e, nil) 29 + } 30 + 31 + ic := uuid.NewString() 32 + 33 + var acc string 34 + if req.ForAccount == nil { 35 + acc = "admin" 36 + } else { 37 + acc = *req.ForAccount 38 + } 39 + 40 + if err := s.db.Create(&models.InviteCode{ 41 + Code: ic, 42 + Did: acc, 43 + RemainingUseCount: req.UseCount, 44 + }).Error; err != nil { 45 + s.logger.Error("error creating invite code", "error", err) 46 + return helpers.ServerError(e, nil) 47 } 48 49 + return e.JSON(200, ComAtprotoServerCreateInviteCodeResponse{ 50 + Code: ic, 51 }) 52 }
+70
server/handle_server_create_invite_codes.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/Azure/go-autorest/autorest/to" 5 + "github.com/google/uuid" 6 + "github.com/haileyok/cocoon/internal/helpers" 7 + "github.com/haileyok/cocoon/models" 8 + "github.com/labstack/echo/v4" 9 + ) 10 + 11 + type ComAtprotoServerCreateInviteCodesRequest struct { 12 + CodeCount *int `json:"codeCount,omitempty"` 13 + UseCount int `json:"useCount" validate:"required"` 14 + ForAccounts *[]string `json:"forAccounts,omitempty"` 15 + } 16 + 17 + type ComAtprotoServerCreateInviteCodesResponse []ComAtprotoServerCreateInviteCodesItem 18 + 19 + type ComAtprotoServerCreateInviteCodesItem struct { 20 + Account string `json:"account"` 21 + Codes []string `json:"codes"` 22 + } 23 + 24 + func (s *Server) handleCreateInviteCodes(e echo.Context) error { 25 + var req ComAtprotoServerCreateInviteCodesRequest 26 + if err := e.Bind(&req); err != nil { 27 + s.logger.Error("error binding", "error", err) 28 + return helpers.ServerError(e, nil) 29 + } 30 + 31 + if err := e.Validate(req); err != nil { 32 + s.logger.Error("error validating", "error", err) 33 + return helpers.InputError(e, nil) 34 + } 35 + 36 + if req.CodeCount == nil { 37 + req.CodeCount = to.IntPtr(1) 38 + } 39 + 40 + if req.ForAccounts == nil { 41 + req.ForAccounts = to.StringSlicePtr([]string{"admin"}) 42 + } 43 + 44 + var codes []ComAtprotoServerCreateInviteCodesItem 45 + 46 + for _, did := range *req.ForAccounts { 47 + var ics []string 48 + 49 + for range *req.CodeCount { 50 + ic := uuid.NewString() 51 + ics = append(ics, ic) 52 + 53 + if err := s.db.Create(&models.InviteCode{ 54 + Code: ic, 55 + Did: did, 56 + RemainingUseCount: req.UseCount, 57 + }).Error; err != nil { 58 + s.logger.Error("error creating invite code", "error", err) 59 + return helpers.ServerError(e, nil) 60 + } 61 + } 62 + 63 + codes = append(codes, ComAtprotoServerCreateInviteCodesItem{ 64 + Account: did, 65 + Codes: ics, 66 + }) 67 + } 68 + 69 + return e.JSON(200, codes) 70 + }
+26
server/server.go
··· 61 JwkPath string 62 ContactEmail string 63 Relays []string 64 65 SmtpUser string 66 SmtpPass string ··· 77 ContactEmail string 78 EnforcePeering bool 79 Relays []string 80 SmtpEmail string 81 SmtpName string 82 } ··· 109 return nil 110 } 111 112 func (s *Server) handleSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 113 return func(e echo.Context) error { 114 authheader := e.Request().Header.Get("authorization") ··· 225 return nil, fmt.Errorf("cocoon hostname must be set") 226 } 227 228 if args.Logger == nil { 229 args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) 230 } ··· 326 ContactEmail: args.ContactEmail, 327 EnforcePeering: false, 328 Relays: args.Relays, 329 SmtpName: args.SmtpName, 330 SmtpEmail: args.SmtpEmail, 331 }, ··· 403 // are there any routes that we should be allowing without auth? i dont think so but idk 404 s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 405 s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 406 } 407 408 func (s *Server) Serve(ctx context.Context) error {
··· 61 JwkPath string 62 ContactEmail string 63 Relays []string 64 + AdminPassword string 65 66 SmtpUser string 67 SmtpPass string ··· 78 ContactEmail string 79 EnforcePeering bool 80 Relays []string 81 + AdminPassword string 82 SmtpEmail string 83 SmtpName string 84 } ··· 111 return nil 112 } 113 114 + func (s *Server) handleAdminMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 115 + return func(e echo.Context) error { 116 + username, password, ok := e.Request().BasicAuth() 117 + if !ok || username != "admin" || password != s.config.AdminPassword { 118 + return helpers.InputError(e, to.StringPtr("Unauthorized")) 119 + } 120 + 121 + if err := next(e); err != nil { 122 + e.Error(err) 123 + } 124 + 125 + return nil 126 + } 127 + } 128 + 129 func (s *Server) handleSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 130 return func(e echo.Context) error { 131 authheader := e.Request().Header.Get("authorization") ··· 242 return nil, fmt.Errorf("cocoon hostname must be set") 243 } 244 245 + if args.AdminPassword == "" { 246 + return nil, fmt.Errorf("admin password must be set") 247 + } 248 + 249 if args.Logger == nil { 250 args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) 251 } ··· 347 ContactEmail: args.ContactEmail, 348 EnforcePeering: false, 349 Relays: args.Relays, 350 + AdminPassword: args.AdminPassword, 351 SmtpName: args.SmtpName, 352 SmtpEmail: args.SmtpEmail, 353 }, ··· 425 // are there any routes that we should be allowing without auth? i dont think so but idk 426 s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 427 s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 428 + 429 + // admin routes 430 + s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware) 431 + s.echo.POST("/xrpc/com.atproto.server.createInviteCodes", s.handleCreateInviteCodes, s.handleAdminMiddleware) 432 } 433 434 func (s *Server) Serve(ctx context.Context) error {