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 }