+8
-2
.env.example
+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
+3
-1
README.md
···
22
22
- [x] com.atproto.repo.applyWrites
23
23
- [x] com.atproto.repo.createRecord
24
24
- [x] com.atproto.repo.putRecord
25
-
- [ ] com.atproto.repo.deleteRecord
25
+
- [x] com.atproto.repo.deleteRecord
26
26
- [x] com.atproto.repo.describeRepo
27
27
- [x] com.atproto.repo.getRecord
28
28
- [ ] com.atproto.repo.importRepo
···
34
34
- [ ] com.atproto.server.checkAccountStatus
35
35
- [x] com.atproto.server.confirmEmail
36
36
- [x] com.atproto.server.createAccount
37
+
- [x] com.atproto.server.createInviteCode
38
+
- [x] com.atproto.server.createInviteCodes
37
39
- [ ] com.atproto.server.deactivateAccount
38
40
- [ ] com.atproto.server.deleteAccount
39
41
- [x] com.atproto.server.deleteSession
+9
-1
cmd/cocoon/main.go
+9
-1
cmd/cocoon/main.go
···
57
57
EnvVars: []string{"COCOON_RELAYS"},
58
58
},
59
59
&cli.StringFlag{
60
+
Name: "admin-password",
61
+
Required: true,
62
+
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
63
+
},
64
+
&cli.StringFlag{
60
65
Name: "smtp-user",
61
66
Required: false,
62
67
EnvVars: []string{"COCOON_SMTP_USER"},
···
94
99
Version: Version,
95
100
}
96
101
97
-
app.Run(os.Args)
102
+
if err := app.Run(os.Args); err != nil {
103
+
fmt.Printf("Error: %v\n", err)
104
+
}
98
105
}
99
106
100
107
var run = &cli.Command{
···
112
119
ContactEmail: cmd.String("contact-email"),
113
120
Version: Version,
114
121
Relays: cmd.StringSlice("relays"),
122
+
AdminPassword: cmd.String("admin-password"),
115
123
SmtpUser: cmd.String("smtp-user"),
116
124
SmtpPass: cmd.String("smtp-pass"),
117
125
SmtpHost: cmd.String("smtp-host"),
+39
-4
server/handle_server_create_invite_code.go
+39
-4
server/handle_server_create_invite_code.go
···
2
2
3
3
import (
4
4
"github.com/google/uuid"
5
+
"github.com/haileyok/cocoon/internal/helpers"
5
6
"github.com/haileyok/cocoon/models"
6
7
"github.com/labstack/echo/v4"
7
8
)
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
+
9
19
func (s *Server) handleCreateInviteCode(e echo.Context) error {
10
-
ic := models.InviteCode{
11
-
Code: uuid.NewString(),
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)
12
47
}
13
48
14
-
return e.JSON(200, map[string]string{
15
-
"code": ic.Code,
49
+
return e.JSON(200, ComAtprotoServerCreateInviteCodeResponse{
50
+
Code: ic,
16
51
})
17
52
}
+70
server/handle_server_create_invite_codes.go
+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
+26
server/server.go
···
61
61
JwkPath string
62
62
ContactEmail string
63
63
Relays []string
64
+
AdminPassword string
64
65
65
66
SmtpUser string
66
67
SmtpPass string
···
77
78
ContactEmail string
78
79
EnforcePeering bool
79
80
Relays []string
81
+
AdminPassword string
80
82
SmtpEmail string
81
83
SmtpName string
82
84
}
···
109
111
return nil
110
112
}
111
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
+
112
129
func (s *Server) handleSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
113
130
return func(e echo.Context) error {
114
131
authheader := e.Request().Header.Get("authorization")
···
225
242
return nil, fmt.Errorf("cocoon hostname must be set")
226
243
}
227
244
245
+
if args.AdminPassword == "" {
246
+
return nil, fmt.Errorf("admin password must be set")
247
+
}
248
+
228
249
if args.Logger == nil {
229
250
args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}))
230
251
}
···
326
347
ContactEmail: args.ContactEmail,
327
348
EnforcePeering: false,
328
349
Relays: args.Relays,
350
+
AdminPassword: args.AdminPassword,
329
351
SmtpName: args.SmtpName,
330
352
SmtpEmail: args.SmtpEmail,
331
353
},
···
403
425
// are there any routes that we should be allowing without auth? i dont think so but idk
404
426
s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware)
405
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)
406
432
}
407
433
408
434
func (s *Server) Serve(ctx context.Context) error {