package passwordresetroute import ( "crypto/sha256" "fmt" "math/rand" "strings" "sync" "time" "bitbucket.org/nemt/nemt-portal-api/application/applicationservice" "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" "bitbucket.org/nemt/nemt-portal-api/infra/config" "bitbucket.org/nemt/nemt-portal-api/server/router/routeutils" "github.com/labstack/echo" ) const ( tokenExpirationTime = 90 // in minutes randomStringLength = 15 baseURL = "http://localhost:5000" passwordResetEmailSubject = "Reset Your Password" passwordResetEmailMainBody = "To reset your password click here or copy the following link and paste it into your browser: \n\n " + baseURL + "/#/reset-password/" passwordResetEmailFooter = "\nThis link expires in 90 minutes" passwordResetCompleteEmailSubject = "Your Password Has been Reset" passwordResetCompleteEmailBody = "Your password has been reset. To login click here or copy the following link and paste it into your browser: \n\n" + baseURL + "/#/login" ) var ( instance *controller once sync.Once ) type controller struct { svc *applicationservice.Service cfg *config.Config } func controllerInstance(svc *applicationservice.Service, cfg *config.Config) *controller { once.Do(func() { instance = &controller{ svc: svc, cfg: cfg, } rand.Seed(time.Now().UTC().UnixNano()) }) return instance } func (c *controller) handleResetRequest(ctx echo.Context) error { userEmail, err := routeutils.GetAndValidateStringParam(ctx, "email", "mandatory field") if err != nil { return routeutils.HandleAPIError(ctx, err) } //find if user with email exists user, err := c.svc.Users.GetByEmail(userEmail) if err != nil { return routeutils.HandleAPIError(ctx, err) } if user.Email == nil || (*user.Email != userEmail) { return routeutils.ResponseAPIOK(ctx, nil) //more secure, don't inform user (attacker) that email doesn't exists } //create and store reset token timeNow := time.Now() expirationTime := timeNow.Add(time.Minute * tokenExpirationTime) randomArray := make([]byte, randomStringLength) rand.Read(randomArray) token := fmt.Sprintf("%x", sha256.Sum256(randomArray)) passwordResetEntry := viewmodel.PasswordReset{ User: user, Token: token, Expires: expirationTime, Opened: false, Used: false, } _, err = c.svc.PasswordReset.CreatePasswordResetEntry(passwordResetEntry) if err != nil { return routeutils.HandleAPIError(ctx, err) } //Send email with reset link notification := viewmodel.Notification{ Type: applicationservice.NotificationTypeEmail, From: c.cfg.Email.Sender, To: *user.Email, Subject: passwordResetEmailSubject, Message: passwordResetEmailMainBody + token + passwordResetEmailFooter, } notification, err = c.svc.Notification.SendNotificationWithoutWritingToDatabase(notification) if err != nil { return routeutils.HandleAPIError(ctx, err) } return routeutils.ResponseAPIOK(ctx, nil) } func (c *controller) handleResetComplete(ctx echo.Context) error { userToken, err := routeutils.GetAndValidateStringParam(ctx, "token", "mandatory field") if err != nil { return routeutils.HandleAPIError(ctx, err) } var user viewmodel.User if err = ctx.Bind(&user); err != nil { return routeutils.HandleAPIError(ctx, err) } if len(strings.TrimSpace(user.Pass)) < 1 { routeutils.ResponseAPIPasswordResetFailed(ctx, "No password") } passwordResetEntry, err := c.svc.PasswordReset.GetByToken(userToken) if err != nil || len(passwordResetEntry.Token) < 1 || passwordResetEntry.Expires.Before(time.Now()) || passwordResetEntry.Used == true { routeutils.ResponseAPIPasswordResetFailed(ctx, "Token error") } fullUserData, err := c.svc.Users.GetByUUID(passwordResetEntry.User.ID, "") if err != nil { routeutils.ResponseAPIPasswordResetFailed(ctx, "User problem") } fmt.Println(fullUserData) //write new password in database //TODO if err := c.svc.PasswordReset.SetTokenUsed(userToken); err != nil { routeutils.ResponseAPIPasswordResetFailed(ctx, "Reset failed") } //Send email with reset link notification := viewmodel.Notification{ Type: applicationservice.NotificationTypeEmail, From: c.cfg.Email.Sender, To: *user.Email, Subject: passwordResetCompleteEmailSubject, Message: passwordResetCompleteEmailBody, } notification, err = c.svc.Notification.SendNotificationWithoutWritingToDatabase(notification) if err != nil { return routeutils.HandleAPIError(ctx, err) } return routeutils.ResponseAPIOK(ctx, nil) } func (c *controller) handleTokenOpen(ctx echo.Context) error { token, err := routeutils.GetAndValidateStringParam(ctx, "token", "mandatory field") if err != nil { return routeutils.HandleAPIError(ctx, err) } if err := c.svc.PasswordReset.SetTokenOpened(token); err != nil { return routeutils.HandleAPIError(ctx, err) } return routeutils.ResponseAPIOK(ctx, nil) }