+5
-5
README.md
+5
-5
README.md
···
143
143
144
144
### Identity
145
145
146
-
- [ ] `com.atproto.identity.getRecommendedDidCredentials`
147
-
- [ ] `com.atproto.identity.requestPlcOperationSignature`
146
+
- [x] `com.atproto.identity.getRecommendedDidCredentials`
147
+
- [x] `com.atproto.identity.requestPlcOperationSignature`
148
148
- [x] `com.atproto.identity.resolveHandle`
149
-
- [ ] `com.atproto.identity.signPlcOperation`
150
-
- [ ] `com.atproto.identity.submitPlcOperation`
149
+
- [x] `com.atproto.identity.signPlcOperation`
150
+
- [x] `com.atproto.identity.submitPlcOperation`
151
151
- [x] `com.atproto.identity.updateHandle`
152
152
153
153
### Repo
···
158
158
- [x] `com.atproto.repo.deleteRecord`
159
159
- [x] `com.atproto.repo.describeRepo`
160
160
- [x] `com.atproto.repo.getRecord`
161
-
- [x] `com.atproto.repo.importRepo` (Works "okay". You still have to handle PLC operations on your own when migrating. Use with extreme caution.)
161
+
- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
162
162
- [x] `com.atproto.repo.listRecords`
163
163
- [ ] `com.atproto.repo.listMissingBlobs`
164
164
+2
models/models.go
+2
models/models.go
+29
server/handle_identity_request_plc_operation.go
+29
server/handle_identity_request_plc_operation.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) handleIdentityRequestPlcOperationSignature(e echo.Context) error {
13
+
urepo := e.Get("repo").(*models.RepoActor)
14
+
15
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
16
+
eat := time.Now().Add(10 * time.Minute).UTC()
17
+
18
+
if err := s.db.Exec("UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
19
+
s.logger.Error("error updating user", "error", err)
20
+
return helpers.ServerError(e, nil)
21
+
}
22
+
23
+
if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil {
24
+
s.logger.Error("error sending mail", "error", err)
25
+
return helpers.ServerError(e, nil)
26
+
}
27
+
28
+
return e.NoContent(200)
29
+
}
+103
server/handle_identity_sign_plc_operation.go
+103
server/handle_identity_sign_plc_operation.go
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"strings"
6
+
"time"
7
+
8
+
"github.com/Azure/go-autorest/autorest/to"
9
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
10
+
"github.com/haileyok/cocoon/identity"
11
+
"github.com/haileyok/cocoon/internal/helpers"
12
+
"github.com/haileyok/cocoon/models"
13
+
"github.com/haileyok/cocoon/plc"
14
+
"github.com/labstack/echo/v4"
15
+
)
16
+
17
+
type ComAtprotoSignPlcOperationRequest struct {
18
+
Token string `json:"token"`
19
+
VerificationMethods *map[string]string `json:"verificationMethods"`
20
+
RotationKeys *[]string `json:"rotationKeys"`
21
+
AlsoKnownAs *[]string `json:"alsoKnownAs"`
22
+
Services *map[string]identity.OperationService `json:"services"`
23
+
}
24
+
25
+
type ComAtprotoSignPlcOperationResponse struct {
26
+
Operation plc.Operation `json:"operation"`
27
+
}
28
+
29
+
func (s *Server) handleSignPlcOperation(e echo.Context) error {
30
+
repo := e.Get("repo").(*models.RepoActor)
31
+
32
+
var req ComAtprotoSignPlcOperationRequest
33
+
if err := e.Bind(&req); err != nil {
34
+
s.logger.Error("error binding", "error", err)
35
+
return helpers.ServerError(e, nil)
36
+
}
37
+
38
+
if !strings.HasPrefix(repo.Repo.Did, "did:plc:") {
39
+
return helpers.InputError(e, nil)
40
+
}
41
+
42
+
if repo.PlcOperationCode == nil || repo.PlcOperationCodeExpiresAt == nil {
43
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
44
+
}
45
+
46
+
if *repo.PlcOperationCode != req.Token {
47
+
return helpers.InvalidTokenError(e)
48
+
}
49
+
50
+
if time.Now().UTC().After(*repo.PlcOperationCodeExpiresAt) {
51
+
return helpers.ExpiredTokenError(e)
52
+
}
53
+
54
+
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
55
+
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
56
+
if err != nil {
57
+
s.logger.Error("error fetching doc", "error", err)
58
+
return helpers.ServerError(e, nil)
59
+
}
60
+
61
+
latest := log[len(log)-1]
62
+
63
+
op := plc.Operation{
64
+
Type: "plc_operation",
65
+
VerificationMethods: latest.Operation.VerificationMethods,
66
+
RotationKeys: latest.Operation.RotationKeys,
67
+
AlsoKnownAs: latest.Operation.AlsoKnownAs,
68
+
Services: latest.Operation.Services,
69
+
Prev: &latest.Cid,
70
+
}
71
+
if req.VerificationMethods != nil {
72
+
op.VerificationMethods = *req.VerificationMethods
73
+
}
74
+
if req.RotationKeys != nil {
75
+
op.RotationKeys = *req.RotationKeys
76
+
}
77
+
if req.AlsoKnownAs != nil {
78
+
op.AlsoKnownAs = *req.AlsoKnownAs
79
+
}
80
+
if req.Services != nil {
81
+
op.Services = *req.Services
82
+
}
83
+
84
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
85
+
if err != nil {
86
+
s.logger.Error("error parsing signing key", "error", err)
87
+
return helpers.ServerError(e, nil)
88
+
}
89
+
90
+
if err := s.plcClient.SignOp(k, &op); err != nil {
91
+
s.logger.Error("error signing plc operation", "error", err)
92
+
return helpers.ServerError(e, nil)
93
+
}
94
+
95
+
if err := s.db.Exec("UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil {
96
+
s.logger.Error("error updating repo", "error", err)
97
+
return helpers.ServerError(e, nil)
98
+
}
99
+
100
+
return e.JSON(200, ComAtprotoSignPlcOperationResponse{
101
+
Operation: op,
102
+
})
103
+
}
+19
server/mail.go
+19
server/mail.go
···
40
40
return nil
41
41
}
42
42
43
+
func (s *Server) sendPlcTokenReset(email, handle, code string) error {
44
+
if s.mail == nil {
45
+
return nil
46
+
}
47
+
48
+
s.mailLk.Lock()
49
+
defer s.mailLk.Unlock()
50
+
51
+
s.mail.To(email)
52
+
s.mail.Subject("PLC token for " + s.config.Hostname)
53
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your PLC operation code is %s. This code will expire in ten minutes.", handle, code))
54
+
55
+
if err := s.mail.Send(); err != nil {
56
+
return err
57
+
}
58
+
59
+
return nil
60
+
}
61
+
43
62
func (s *Server) sendEmailUpdate(email, handle, code string) error {
44
63
if s.mail == nil {
45
64
return nil
+2
server/server.go
+2
server/server.go
···
461
461
s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
462
462
s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
463
463
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
464
+
s.echo.POST("/xrpc/com.atproto.identity.requestPlcOperationSignature", s.handleIdentityRequestPlcOperationSignature, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
465
+
s.echo.POST("/xrpc/com.atproto.identity.signPlcOperation", s.handleSignPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
464
466
s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
465
467
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
466
468
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)