github.com/resonatecoop/id@v1.1.0-43/oauth/email_token.go (about)

     1  package oauth
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"time"
     9  
    10  	jwt "github.com/form3tech-oss/jwt-go"
    11  	uuid "github.com/google/uuid"
    12  	"github.com/mailgun/mailgun-go/v4"
    13  	"github.com/resonatecoop/id/util"
    14  	"github.com/resonatecoop/user-api/model"
    15  	"github.com/uptrace/bun"
    16  )
    17  
    18  var (
    19  	ErrEmailTokenNotFound    = errors.New("this token was not found")
    20  	ErrEmailTokenInvalid     = errors.New("this token is invalid or has expired")
    21  	ErrInvalidEmailTokenLink = errors.New("email token link is invalid")
    22  )
    23  
    24  // GetValidEmailToken ...
    25  func (s *Service) GetValidEmailToken(token string) (*model.EmailToken, *model.User, error) {
    26  	ctx := context.Background()
    27  	claims := &model.EmailTokenClaims{}
    28  
    29  	jwtKey := []byte(s.cnf.EmailTokenSecretKey)
    30  
    31  	tkn, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
    32  		return jwtKey, nil
    33  	})
    34  
    35  	if err != nil {
    36  		return nil, nil, err
    37  	}
    38  
    39  	if !tkn.Valid {
    40  		return nil, nil, ErrEmailTokenInvalid
    41  	}
    42  
    43  	emailToken := new(model.EmailToken)
    44  
    45  	err = s.db.NewSelect().
    46  		Model(emailToken).
    47  		Where("reference = ?", claims.Reference).
    48  		Limit(1).
    49  		Scan(ctx)
    50  
    51  	// Not Found!
    52  	if err != nil {
    53  		return nil, nil, ErrEmailTokenNotFound
    54  	}
    55  
    56  	user, err := s.FindUserByUsername(claims.Username)
    57  
    58  	if err != nil {
    59  		return nil, nil, ErrEmailTokenNotFound
    60  	}
    61  
    62  	return emailToken, user, nil
    63  }
    64  
    65  // SendEmailToken ...
    66  func (s *Service) SendEmailToken(
    67  	email *model.Email,
    68  	emailTokenLink string,
    69  ) (*model.EmailToken, error) {
    70  	if !util.ValidateEmail(email.Recipient) {
    71  		return nil, ErrEmailInvalid
    72  	}
    73  
    74  	// Check if user is registered
    75  	_, err := s.FindUserByUsername(email.Recipient)
    76  
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	return s.sendEmailTokenCommon(s.db, email, emailTokenLink)
    82  }
    83  
    84  // SendEmailTokenTx ...
    85  func (s *Service) SendEmailTokenTx(
    86  	tx *bun.DB,
    87  	email *model.Email,
    88  	emailTokenLink string,
    89  ) (*model.EmailToken, error) {
    90  	return s.sendEmailTokenCommon(tx, email, emailTokenLink)
    91  }
    92  
    93  // CreateEmailToken ...
    94  func (s *Service) CreateEmailToken(email string) (*model.EmailToken, error) {
    95  	expiresIn := 30 * time.Minute // 30 minutes
    96  
    97  	emailToken := model.NewOauthEmailToken(&expiresIn)
    98  
    99  	emailToken.EmailSentAt = &time.Time{}
   100  	emailToken.Reference = uuid.New()
   101  
   102  	ctx := context.Background()
   103  
   104  	_, err := s.db.NewInsert().Column(
   105  		"id",
   106  		"reference",
   107  		"email_sent_at",
   108  		"email_sent",
   109  		"expires_at",
   110  	).
   111  		Model(emailToken).
   112  		Exec(ctx)
   113  
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	return emailToken, nil
   119  }
   120  
   121  // createJwtTokenWithEmailTokenClaims ...
   122  func (s *Service) createJwtTokenWithEmailTokenClaims(
   123  	claims *model.EmailTokenClaims,
   124  ) (string, error) {
   125  	jwtKey := []byte(s.cnf.EmailTokenSecretKey)
   126  
   127  	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   128  
   129  	tokenString, err := token.SignedString(jwtKey)
   130  
   131  	if err != nil {
   132  		return "", err
   133  	}
   134  
   135  	return tokenString, nil
   136  }
   137  
   138  // sendEmailTokenCommon ...
   139  func (s *Service) sendEmailTokenCommon(
   140  	db *bun.DB,
   141  	email *model.Email,
   142  	link string,
   143  ) (
   144  	*model.EmailToken,
   145  	error,
   146  ) {
   147  	// Check if email token link is valid
   148  	_, err := url.ParseRequestURI(link)
   149  
   150  	if err != nil {
   151  		return nil, ErrInvalidEmailTokenLink
   152  	}
   153  
   154  	recipient := email.Recipient
   155  
   156  	emailToken, err := s.CreateEmailToken(recipient)
   157  
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	// Create the JWT claims, which includes the username, expiry time and uuid reference
   163  	claims := model.NewOauthEmailTokenClaims(email.Recipient, emailToken)
   164  
   165  	token, err := s.createJwtTokenWithEmailTokenClaims(claims)
   166  
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	emailTokenLink := fmt.Sprintf(
   172  		"%s?token=%s",
   173  		link, // base url for email token link
   174  		token,
   175  	)
   176  
   177  	mg := mailgun.NewMailgun(s.cnf.Mailgun.Domain, s.cnf.Mailgun.Key)
   178  	sender := s.cnf.Mailgun.Sender
   179  	body := ""
   180  	subject := email.Subject
   181  	message := mg.NewMessage(sender, subject, body, recipient)
   182  	message.SetTemplate(email.Template) // set mailgun template
   183  	err = message.AddTemplateVariable("email", email.Recipient)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	err = message.AddTemplateVariable("emailTokenLink", emailTokenLink)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   193  	defer cancel()
   194  
   195  	// Send the message with a 10 second timeout
   196  	_, _, err = mg.Send(ctx, message)
   197  
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	_, err = s.db.NewUpdate().
   203  		Model(emailToken).
   204  		Set("email_sent = ?", true).
   205  		Set("email_sent_at = ?", time.Now().UTC()).
   206  		Where("reference = ?", emailToken.Reference).
   207  		Exec(ctx)
   208  
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	return emailToken, nil
   214  }
   215  
   216  // ClearExpiredEmailTokens ...
   217  func (s *Service) ClearExpiredEmailTokens() error {
   218  	ctx := context.Background()
   219  
   220  	now := time.Now().UTC()
   221  
   222  	emailToken := new(model.EmailToken)
   223  
   224  	_, err := s.db.NewDelete().
   225  		Model(emailToken).
   226  		Where(
   227  			"expires_at < ?",
   228  			now.AddDate(0, -30, 0), // 30 days ago
   229  		).
   230  		ForceDelete().
   231  		Exec(ctx)
   232  
   233  	return err
   234  }
   235  
   236  // DeleteEmailToken ...
   237  func (s *Service) DeleteEmailToken(emailToken *model.EmailToken, soft bool) error {
   238  	ctx := context.Background()
   239  
   240  	if soft {
   241  
   242  		_, err := s.db.NewDelete().
   243  			Model(emailToken).
   244  			WherePK().
   245  			Exec(ctx)
   246  
   247  		return err
   248  	}
   249  
   250  	_, err := s.db.NewDelete().
   251  		Model(emailToken).
   252  		WherePK().
   253  		ForceDelete().
   254  		Exec(ctx)
   255  
   256  	return err
   257  }