mirror of https://github.com/grafana/grafana.git
160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
package userimpl
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/mail"
|
|
"time"
|
|
|
|
"github.com/grafana/authlib/claims"
|
|
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/services/auth"
|
|
"github.com/grafana/grafana/pkg/services/authn"
|
|
"github.com/grafana/grafana/pkg/services/notifications"
|
|
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
var (
|
|
errInvalidCode = errutil.BadRequest("user.code.invalid", errutil.WithPublicMessage("Invalid verification code"))
|
|
errExpiredCode = errutil.BadRequest("user.code.expired", errutil.WithPublicMessage("Verification code has expired"))
|
|
)
|
|
|
|
var _ user.Verifier = (*Verifier)(nil)
|
|
|
|
func ProvideVerifier(cfg *setting.Cfg, us user.Service, ts tempuser.Service, ns notifications.Service, is auth.IDService) *Verifier {
|
|
return &Verifier{cfg, us, ts, ns, is}
|
|
}
|
|
|
|
type Verifier struct {
|
|
cfg *setting.Cfg
|
|
us user.Service
|
|
ts tempuser.Service
|
|
ns notifications.Service
|
|
is auth.IDService
|
|
}
|
|
|
|
func (s *Verifier) Start(ctx context.Context, cmd user.StartVerifyEmailCommand) error {
|
|
usr, err := s.us.GetByLogin(ctx, &user.GetUserByLoginQuery{
|
|
LoginOrEmail: cmd.Email,
|
|
})
|
|
|
|
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
|
return err
|
|
}
|
|
|
|
// if email is already used by another user we stop here
|
|
if usr != nil && usr.ID != cmd.User.ID {
|
|
return user.ErrEmailConflict.Errorf("email already used")
|
|
}
|
|
|
|
code, err := util.GetRandomString(20)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate verification code: %w", err)
|
|
}
|
|
|
|
// invalidate any pending verifications for user
|
|
if err = s.ts.ExpirePreviousVerifications(
|
|
ctx, &tempuser.ExpirePreviousVerificationsCommand{InvitedByUserID: cmd.User.ID},
|
|
); err != nil {
|
|
return fmt.Errorf("failed to expire previous verifications: %w", err)
|
|
}
|
|
|
|
tmpUsr, err := s.ts.CreateTempUser(ctx, &tempuser.CreateTempUserCommand{
|
|
OrgID: -1,
|
|
// used to determine if the user was updating their email or username in the second step of the verification flow
|
|
Name: string(cmd.Action),
|
|
// used to fetch the User in the second step of the verification flow
|
|
InvitedByUserID: cmd.User.ID,
|
|
Email: cmd.Email,
|
|
Code: code,
|
|
Status: tempuser.TmpUserEmailUpdateStarted,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate temp user for email verification: %w", err)
|
|
}
|
|
|
|
if err := s.ns.SendVerificationEmail(ctx, ¬ifications.SendVerifyEmailCommand{
|
|
User: &cmd.User,
|
|
Code: tmpUsr.Code,
|
|
Email: cmd.Email,
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to send verification email: %w", err)
|
|
}
|
|
|
|
if err := s.ts.UpdateTempUserWithEmailSent(ctx, &tempuser.UpdateTempUserWithEmailSentCommand{
|
|
Code: tmpUsr.Code,
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to mark email as sent: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Verifier) Complete(ctx context.Context, cmd user.CompleteEmailVerifyCommand) error {
|
|
tmpUsr, err := s.ts.GetTempUserByCode(ctx, &tempuser.GetTempUserByCodeQuery{Code: cmd.Code})
|
|
if err != nil {
|
|
return errInvalidCode.Errorf("failed to verify code: %w", err)
|
|
}
|
|
|
|
if tmpUsr.Status != tempuser.TmpUserEmailUpdateStarted {
|
|
return errInvalidCode.Errorf("wrong status for verification code: %s", tmpUsr.Status)
|
|
}
|
|
|
|
if !tmpUsr.EmailSent {
|
|
return errInvalidCode.Errorf("email was not marked as sent")
|
|
}
|
|
|
|
if tmpUsr.EmailSentOn.Add(s.cfg.VerificationEmailMaxLifetime).Before(time.Now()) {
|
|
return errExpiredCode.Errorf("verification code has expired")
|
|
}
|
|
|
|
usr, err := s.us.GetByID(ctx, &user.GetUserByIDQuery{ID: tmpUsr.InvitedByID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
verified := true
|
|
update := &user.UpdateUserCommand{
|
|
Email: tmpUsr.Email,
|
|
UserID: tmpUsr.InvitedByID,
|
|
EmailVerified: &verified,
|
|
}
|
|
switch tmpUsr.Name {
|
|
case string(user.EmailUpdateAction):
|
|
// User updated the email field
|
|
if _, err := mail.ParseAddress(usr.Login); err == nil {
|
|
// If username was also an email, we update it to keep it in sync with the email field
|
|
update.Login = tmpUsr.Email
|
|
}
|
|
case string(user.LoginUpdateAction):
|
|
// User updated the username field with a new email
|
|
update.Login = tmpUsr.Email
|
|
default:
|
|
return errors.New("trying to update email on unknown field")
|
|
}
|
|
|
|
if err := s.us.Update(ctx, update); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.ts.UpdateTempUserStatus(
|
|
ctx,
|
|
&tempuser.UpdateTempUserStatusCommand{Code: cmd.Code, Status: tempuser.TmpUserEmailUpdateCompleted},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// We store email and email verified in id tokens. So whenever we perform and update / confirmation we need to
|
|
// remove the current token, so a new one can be generated with correct values.
|
|
return s.is.RemoveIDToken(
|
|
ctx,
|
|
&authn.Identity{ID: identity.NewTypedID(claims.TypeUser, usr.ID), OrgID: usr.OrgID},
|
|
)
|
|
}
|