From a4c3adb24935d89ddc7daec4346af923a3595417 Mon Sep 17 00:00:00 2001 From: Juliet Date: Mon, 14 Apr 2025 19:41:50 +0200 Subject: [PATCH] implement createInviteCode & createInviteCodes (#4) Co-authored-by: hailey --- .env.example | 10 ++- README.md | 4 +- cmd/cocoon/main.go | 10 ++- server/handle_server_create_invite_code.go | 43 +++++++++++-- server/handle_server_create_invite_codes.go | 70 +++++++++++++++++++++ server/server.go | 26 ++++++++ 6 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 server/handle_server_create_invite_codes.go diff --git a/.env.example b/.env.example index 532eb4a..551d076 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,8 @@ -COCOON_DID= -COCOON_HOSTNAME= +COCOON_DID="did:web:cocoon.example.com" +COCOON_HOSTNAME="cocoon.example.com" +COCOON_ROTATION_KEY_PATH="./rotation.key" +COCOON_JWK_PATH="./jwk.key" +COCOON_CONTACT_EMAIL="me@example.com" +COCOON_RELAYS=https://bsky.network +# Generate with `openssl rand -hex 16` +COCOON_ADMIN_PASSWORD= diff --git a/README.md b/README.md index a571965..2b83391 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Just because something is implemented doesn't mean it is finisehd. Tons of these - [x] com.atproto.repo.applyWrites - [x] com.atproto.repo.createRecord - [x] com.atproto.repo.putRecord -- [ ] com.atproto.repo.deleteRecord +- [x] com.atproto.repo.deleteRecord - [x] com.atproto.repo.describeRepo - [x] com.atproto.repo.getRecord - [ ] com.atproto.repo.importRepo @@ -34,6 +34,8 @@ Just because something is implemented doesn't mean it is finisehd. Tons of these - [ ] com.atproto.server.checkAccountStatus - [x] com.atproto.server.confirmEmail - [x] com.atproto.server.createAccount +- [x] com.atproto.server.createInviteCode +- [x] com.atproto.server.createInviteCodes - [ ] com.atproto.server.deactivateAccount - [ ] com.atproto.server.deleteAccount - [x] com.atproto.server.deleteSession diff --git a/cmd/cocoon/main.go b/cmd/cocoon/main.go index 5404fc8..96ce226 100644 --- a/cmd/cocoon/main.go +++ b/cmd/cocoon/main.go @@ -56,6 +56,11 @@ func main() { Required: true, EnvVars: []string{"COCOON_RELAYS"}, }, + &cli.StringFlag{ + Name: "admin-password", + Required: true, + EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, + }, &cli.StringFlag{ Name: "smtp-user", Required: false, @@ -94,7 +99,9 @@ func main() { Version: Version, } - app.Run(os.Args) + if err := app.Run(os.Args); err != nil { + fmt.Printf("Error: %v\n", err) + } } var run = &cli.Command{ @@ -112,6 +119,7 @@ var run = &cli.Command{ ContactEmail: cmd.String("contact-email"), Version: Version, Relays: cmd.StringSlice("relays"), + AdminPassword: cmd.String("admin-password"), SmtpUser: cmd.String("smtp-user"), SmtpPass: cmd.String("smtp-pass"), SmtpHost: cmd.String("smtp-host"), diff --git a/server/handle_server_create_invite_code.go b/server/handle_server_create_invite_code.go index e8e0321..d179cdf 100644 --- a/server/handle_server_create_invite_code.go +++ b/server/handle_server_create_invite_code.go @@ -2,16 +2,51 @@ package server import ( "github.com/google/uuid" + "github.com/haileyok/cocoon/internal/helpers" "github.com/haileyok/cocoon/models" "github.com/labstack/echo/v4" ) +type ComAtprotoServerCreateInviteCodeRequest struct { + UseCount int `json:"useCount" validate:"required"` + ForAccount *string `json:"forAccount,omitempty"` +} + +type ComAtprotoServerCreateInviteCodeResponse struct { + Code string `json:"code"` +} + func (s *Server) handleCreateInviteCode(e echo.Context) error { - ic := models.InviteCode{ - Code: uuid.NewString(), + var req ComAtprotoServerCreateInviteCodeRequest + if err := e.Bind(&req); err != nil { + s.logger.Error("error binding", "error", err) + return helpers.ServerError(e, nil) + } + + if err := e.Validate(req); err != nil { + s.logger.Error("error validating", "error", err) + return helpers.InputError(e, nil) + } + + ic := uuid.NewString() + + var acc string + if req.ForAccount == nil { + acc = "admin" + } else { + acc = *req.ForAccount + } + + if err := s.db.Create(&models.InviteCode{ + Code: ic, + Did: acc, + RemainingUseCount: req.UseCount, + }).Error; err != nil { + s.logger.Error("error creating invite code", "error", err) + return helpers.ServerError(e, nil) } - return e.JSON(200, map[string]string{ - "code": ic.Code, + return e.JSON(200, ComAtprotoServerCreateInviteCodeResponse{ + Code: ic, }) } diff --git a/server/handle_server_create_invite_codes.go b/server/handle_server_create_invite_codes.go new file mode 100644 index 0000000..261d841 --- /dev/null +++ b/server/handle_server_create_invite_codes.go @@ -0,0 +1,70 @@ +package server + +import ( + "github.com/Azure/go-autorest/autorest/to" + "github.com/google/uuid" + "github.com/haileyok/cocoon/internal/helpers" + "github.com/haileyok/cocoon/models" + "github.com/labstack/echo/v4" +) + +type ComAtprotoServerCreateInviteCodesRequest struct { + CodeCount *int `json:"codeCount,omitempty"` + UseCount int `json:"useCount" validate:"required"` + ForAccounts *[]string `json:"forAccounts,omitempty"` +} + +type ComAtprotoServerCreateInviteCodesResponse []ComAtprotoServerCreateInviteCodesItem + +type ComAtprotoServerCreateInviteCodesItem struct { + Account string `json:"account"` + Codes []string `json:"codes"` +} + +func (s *Server) handleCreateInviteCodes(e echo.Context) error { + var req ComAtprotoServerCreateInviteCodesRequest + if err := e.Bind(&req); err != nil { + s.logger.Error("error binding", "error", err) + return helpers.ServerError(e, nil) + } + + if err := e.Validate(req); err != nil { + s.logger.Error("error validating", "error", err) + return helpers.InputError(e, nil) + } + + if req.CodeCount == nil { + req.CodeCount = to.IntPtr(1) + } + + if req.ForAccounts == nil { + req.ForAccounts = to.StringSlicePtr([]string{"admin"}) + } + + var codes []ComAtprotoServerCreateInviteCodesItem + + for _, did := range *req.ForAccounts { + var ics []string + + for range *req.CodeCount { + ic := uuid.NewString() + ics = append(ics, ic) + + if err := s.db.Create(&models.InviteCode{ + Code: ic, + Did: did, + RemainingUseCount: req.UseCount, + }).Error; err != nil { + s.logger.Error("error creating invite code", "error", err) + return helpers.ServerError(e, nil) + } + } + + codes = append(codes, ComAtprotoServerCreateInviteCodesItem{ + Account: did, + Codes: ics, + }) + } + + return e.JSON(200, codes) +} diff --git a/server/server.go b/server/server.go index 81b5d70..8d8776f 100644 --- a/server/server.go +++ b/server/server.go @@ -61,6 +61,7 @@ type Args struct { JwkPath string ContactEmail string Relays []string + AdminPassword string SmtpUser string SmtpPass string @@ -77,6 +78,7 @@ type config struct { ContactEmail string EnforcePeering bool Relays []string + AdminPassword string SmtpEmail string SmtpName string } @@ -109,6 +111,21 @@ func (cv *CustomValidator) Validate(i any) error { return nil } +func (s *Server) handleAdminMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(e echo.Context) error { + username, password, ok := e.Request().BasicAuth() + if !ok || username != "admin" || password != s.config.AdminPassword { + return helpers.InputError(e, to.StringPtr("Unauthorized")) + } + + if err := next(e); err != nil { + e.Error(err) + } + + return nil + } +} + func (s *Server) handleSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(e echo.Context) error { authheader := e.Request().Header.Get("authorization") @@ -225,6 +242,10 @@ func New(args *Args) (*Server, error) { return nil, fmt.Errorf("cocoon hostname must be set") } + if args.AdminPassword == "" { + return nil, fmt.Errorf("admin password must be set") + } + if args.Logger == nil { args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) } @@ -326,6 +347,7 @@ func New(args *Args) (*Server, error) { ContactEmail: args.ContactEmail, EnforcePeering: false, Relays: args.Relays, + AdminPassword: args.AdminPassword, SmtpName: args.SmtpName, SmtpEmail: args.SmtpEmail, }, @@ -403,6 +425,10 @@ func (s *Server) addRoutes() { // are there any routes that we should be allowing without auth? i dont think so but idk s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) + + // admin routes + s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware) + s.echo.POST("/xrpc/com.atproto.server.createInviteCodes", s.handleCreateInviteCodes, s.handleAdminMiddleware) } func (s *Server) Serve(ctx context.Context) error { -- 2.51.2