+2
-2
README.md
+2
-2
README.md
···
32
32
33
33
- [ ] com.atproto.server.activateAccount
34
34
- [ ] com.atproto.server.checkAccountStatus
35
-
- [ ] com.atproto.server.confirmEmail
35
+
- [x] com.atproto.server.confirmEmail
36
36
- [x] com.atproto.server.createAccount
37
37
- [ ] com.atproto.server.deactivateAccount
38
38
- [ ] com.atproto.server.deleteAccount
···
43
43
- [ ] com.atproto.server.listAppPasswords
44
44
- [x] com.atproto.server.refreshSession
45
45
- [ ] com.atproto.server.requestAccountDelete
46
-
- [ ] com.atproto.server.requestEmailConfirmation
46
+
- [x] com.atproto.server.requestEmailConfirmation
47
47
- [ ] com.atproto.server.requestEmailUpdate
48
48
- [ ] com.atproto.server.requestPasswordReset
49
49
- [ ] com.atproto.server.reserveSigningKey
+15
-10
models/models.go
+15
-10
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
-
EmailVerificationCode *string
16
-
Password string
17
-
SigningKey []byte
18
-
Rev string
19
-
Root []byte
20
-
Preferences []byte
11
+
Did string `gorm:"primaryKey"`
12
+
CreatedAt time.Time
13
+
Email string `gorm:"uniqueIndex"`
14
+
EmailConfirmedAt *time.Time
15
+
EmailVerificationCode *string
16
+
EmailVerificationCodeExpiresAt *time.Time
17
+
EmailUpdateCode *string
18
+
EmailUpdateCodeExpiresAt *time.Time
19
+
PasswordResetCode *string
20
+
PasswordResetCodeExpiresAt *time.Time
21
+
Password string
22
+
SigningKey []byte
23
+
Rev string
24
+
Root []byte
25
+
Preferences []byte
21
26
}
22
27
23
28
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+6
-2
server/handle_server_confirm_email.go
+6
-2
server/handle_server_confirm_email.go
···
27
27
return helpers.InputError(e, nil)
28
28
}
29
29
30
-
if urepo.EmailVerificationCode == nil {
30
+
if urepo.EmailVerificationCode == nil || urepo.EmailVerificationCodeExpiresAt == nil {
31
31
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
32
32
}
33
33
···
35
35
return helpers.InputError(e, to.StringPtr("InvalidToken"))
36
36
}
37
37
38
+
if time.Now().UTC().After(*urepo.EmailVerificationCodeExpiresAt) {
39
+
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
40
+
}
41
+
38
42
now := time.Now().UTC()
39
43
40
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_confirmed_at = ? WHERE did = ?", now, urepo.Repo.Did).Error; err != nil {
44
+
if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", now, urepo.Repo.Did).Error; err != nil {
41
45
s.logger.Error("error updating user", "error", err)
42
46
return helpers.ServerError(e, nil)
43
47
}
+3
-1
server/handle_server_request_email_confirmation.go
+3
-1
server/handle_server_request_email_confirmation.go
···
2
2
3
3
import (
4
4
"fmt"
5
+
"time"
5
6
6
7
"github.com/Azure/go-autorest/autorest/to"
7
8
"github.com/haileyok/cocoon/internal/helpers"
···
17
18
}
18
19
19
20
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))
21
+
eat := time.Now().Add(10 * time.Minute).UTC()
20
22
21
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = ? WHERE did = ?", code, urepo.Repo.Did).Error; err != nil {
23
+
if err := s.db.Exec("UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", code, eat, urepo.Repo.Did).Error; err != nil {
22
24
s.logger.Error("error updating user", "error", err)
23
25
return helpers.ServerError(e, nil)
24
26
}
+29
server/handle_server_request_email_update.go
+29
server/handle_server_request_email_update.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) handleServerRequestEmailUpdate(e echo.Context) error {
13
+
urepo := e.Get("repo").(*models.RepoActor)
14
+
15
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))
16
+
eat := time.Now().Add(10 * time.Minute).UTC()
17
+
18
+
if err := s.db.Exec("UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", code, eat, urepo.Repo.Did).Error; err != nil {
19
+
s.logger.Error("error updating repo", "error", err)
20
+
return helpers.ServerError(e, nil)
21
+
}
22
+
23
+
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
24
+
s.logger.Error("error sending email", "error", err)
25
+
return helpers.ServerError(e, nil)
26
+
}
27
+
28
+
return e.NoContent(200)
29
+
}
+29
server/handle_server_request_password_reset.go
+29
server/handle_server_request_password_reset.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) handleServerRequestPasswordReset(e echo.Context) error {
13
+
urepo := e.Get("repo").(*models.RepoActor)
14
+
15
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))
16
+
eat := time.Now().Add(10 * time.Minute).UTC()
17
+
18
+
if err := s.db.Exec("UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", code, eat, urepo.Repo.Did).Error; err != nil {
19
+
s.logger.Error("error updating repo", "error", err)
20
+
return helpers.ServerError(e, nil)
21
+
}
22
+
23
+
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
24
+
s.logger.Error("error sending email", "error", err)
25
+
return helpers.ServerError(e, nil)
26
+
}
27
+
28
+
return e.NoContent(200)
29
+
}
+55
server/handle_server_reset_password.go
+55
server/handle_server_reset_password.go
···
1
+
package server
2
+
3
+
import (
4
+
"time"
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
+
"golang.org/x/crypto/bcrypt"
11
+
)
12
+
13
+
type ComAtprotoServerResetPasswordRequest struct {
14
+
Token string `json:"token" validate:"required"`
15
+
Password string `json:"password" validate:"required"`
16
+
}
17
+
18
+
func (s *Server) handleServerResetPassword(e echo.Context) error {
19
+
urepo := e.Get("repo").(*models.RepoActor)
20
+
21
+
var req ComAtprotoServerResetPasswordRequest
22
+
if err := e.Bind(&req); err != nil {
23
+
s.logger.Error("error binding", "error", err)
24
+
return helpers.ServerError(e, nil)
25
+
}
26
+
27
+
if err := e.Validate(req); err != nil {
28
+
return helpers.InputError(e, nil)
29
+
}
30
+
31
+
if urepo.PasswordResetCode == nil || urepo.PasswordResetCodeExpiresAt == nil {
32
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
33
+
}
34
+
35
+
if *urepo.PasswordResetCode != req.Token {
36
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
37
+
}
38
+
39
+
if time.Now().UTC().After(*urepo.PasswordResetCodeExpiresAt) {
40
+
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
41
+
}
42
+
43
+
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
44
+
if err != nil {
45
+
s.logger.Error("error creating hash", "error", err)
46
+
return helpers.ServerError(e, nil)
47
+
}
48
+
49
+
if err := s.db.Exec("UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", hash, urepo.Repo.Did).Error; err != nil {
50
+
s.logger.Error("error updating repo", "error", err)
51
+
return helpers.ServerError(e, nil)
52
+
}
53
+
54
+
return e.NoContent(200)
55
+
}
+49
server/handle_server_update_email.go
+49
server/handle_server_update_email.go
···
1
+
package server
2
+
3
+
import (
4
+
"time"
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
+
type ComAtprotoServerUpdateEmailRequest struct {
13
+
Email string `json:"email" validate:"required"`
14
+
EmailAuthFactor bool `json:"emailAuthFactor"`
15
+
Token string `json:"token" validate:"required"`
16
+
}
17
+
18
+
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
19
+
urepo := e.Get("repo").(*models.RepoActor)
20
+
21
+
var req ComAtprotoServerUpdateEmailRequest
22
+
if err := e.Bind(&req); err != nil {
23
+
s.logger.Error("error binding", "error", err)
24
+
return helpers.ServerError(e, nil)
25
+
}
26
+
27
+
if err := e.Validate(req); err != nil {
28
+
return helpers.InputError(e, nil)
29
+
}
30
+
31
+
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
32
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
33
+
}
34
+
35
+
if *urepo.EmailUpdateCode != req.Token {
36
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
37
+
}
38
+
39
+
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
40
+
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
41
+
}
42
+
43
+
if err := s.db.Exec("UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email = ? WHERE did = ?", req.Email, urepo.Repo.Did).Error; err != nil {
44
+
s.logger.Error("error updating repo", "error", err)
45
+
return helpers.ServerError(e, nil)
46
+
}
47
+
48
+
return e.NoContent(200)
49
+
}
+17
-2
server/mail.go
+17
-2
server/mail.go
···
23
23
24
24
s.mail.To(email)
25
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))
26
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your password reset code is %s. This code will expire in ten minutes.", 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) sendEmailUpdate(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 update for " + s.config.Hostname)
41
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email update code is %s. This code will expire in ten minutes.", handle, code))
27
42
28
43
if err := s.mail.Send(); err != nil {
29
44
return err
···
38
53
39
54
s.mail.To(email)
40
55
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))
56
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email verification code is %s. This code will expire in ten minutes.", handle, code))
42
57
43
58
if err := s.mail.Send(); err != nil {
44
59
return err
+4
server/server.go
+4
server/server.go
···
383
383
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleSessionMiddleware)
384
384
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleSessionMiddleware)
385
385
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleSessionMiddleware)
386
+
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset, s.handleSessionMiddleware)
387
+
s.echo.POST("/xrpc/com.atproto.server.requestEmailUpdate", s.handleServerRequestEmailUpdate, s.handleSessionMiddleware)
388
+
s.echo.POST("/xrpc/com.atproto.server.resetPassword", s.handleServerResetPassword, s.handleSessionMiddleware)
389
+
s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleSessionMiddleware)
386
390
387
391
// repo
388
392
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleSessionMiddleware)