2018-05-30 16:52:57 +02:00
package passwordresetroute
import (
"crypto/sha256"
"fmt"
"math/rand"
2018-06-01 12:06:55 +02:00
"strings"
2018-05-30 16:52:57 +02:00
"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 (
2018-06-01 15:01:37 +02:00
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"
2018-05-30 16:52:57 +02:00
)
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 )
}
2018-05-30 18:24:53 +02:00
if user . Email == nil || ( * user . Email != userEmail ) {
return routeutils . ResponseAPIOK ( ctx , nil ) //more secure, don't inform user (attacker) that email doesn't exists
}
2018-05-30 16:52:57 +02:00
//create and store reset token
timeNow := time . Now ( )
2018-05-30 18:24:53 +02:00
expirationTime := timeNow . Add ( time . Minute * tokenExpirationTime )
2018-05-30 16:52:57 +02:00
randomArray := make ( [ ] byte , randomStringLength )
rand . Read ( randomArray )
2018-05-30 18:24:53 +02:00
token := fmt . Sprintf ( "%x" , sha256 . Sum256 ( randomArray ) )
2018-05-30 16:52:57 +02:00
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 ,
2018-06-01 15:01:37 +02:00
To : * user . Email ,
2018-05-30 16:52:57 +02:00
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 {
2018-06-01 05:02:56 +02:00
userToken , err := routeutils . GetAndValidateStringParam ( ctx , "token" , "mandatory field" )
if err != nil {
return routeutils . HandleAPIError ( ctx , err )
}
2018-05-30 16:52:57 +02:00
2018-06-01 05:02:56 +02:00
var user viewmodel . User
if err = ctx . Bind ( & user ) ; err != nil {
return routeutils . HandleAPIError ( ctx , err )
}
2018-06-01 12:06:55 +02:00
if len ( strings . TrimSpace ( user . Pass ) ) < 1 {
2018-06-01 18:53:06 +02:00
return routeutils . ResponseAPIPasswordResetFailed ( ctx , "No password" )
2018-06-01 12:06:55 +02:00
}
passwordResetEntry , err := c . svc . PasswordReset . GetByToken ( userToken )
if err != nil || len ( passwordResetEntry . Token ) < 1 || passwordResetEntry . Expires . Before ( time . Now ( ) ) || passwordResetEntry . Used == true {
2018-06-01 18:53:06 +02:00
return routeutils . ResponseAPIPasswordResetFailed ( ctx , "Token error" )
2018-06-01 12:06:55 +02:00
}
fullUserData , err := c . svc . Users . GetByUUID ( passwordResetEntry . User . ID , "" )
if err != nil {
2018-06-01 18:53:06 +02:00
return routeutils . ResponseAPIPasswordResetFailed ( ctx , "User error" )
2018-06-01 12:06:55 +02:00
}
2018-06-01 18:53:06 +02:00
fullUserData . Pass = user . Pass //user contains just password sent from reset form
if err = c . svc . Users . UpdateLoginPassword ( fullUserData ) ; err != nil {
return routeutils . ResponseAPIPasswordResetFailed ( ctx , "Error updating password" )
}
2018-05-30 16:52:57 +02:00
2018-06-01 12:06:55 +02:00
if err := c . svc . PasswordReset . SetTokenUsed ( userToken ) ; err != nil {
2018-06-01 18:53:06 +02:00
return routeutils . ResponseAPIPasswordResetFailed ( ctx , "Reset failed" )
2018-06-01 12:06:55 +02:00
}
2018-05-30 16:52:57 +02:00
2018-06-01 15:01:37 +02:00
//Send email with reset link
notification := viewmodel . Notification {
Type : applicationservice . NotificationTypeEmail ,
From : c . cfg . Email . Sender ,
2018-06-01 18:53:06 +02:00
To : * fullUserData . Email ,
2018-06-01 15:01:37 +02:00
Subject : passwordResetCompleteEmailSubject ,
Message : passwordResetCompleteEmailBody ,
}
notification , err = c . svc . Notification . SendNotificationWithoutWritingToDatabase ( notification )
if err != nil {
return routeutils . HandleAPIError ( ctx , err )
}
2018-05-30 16:52:57 +02:00
return routeutils . ResponseAPIOK ( ctx , nil )
}
2018-05-31 16:26:40 +02:00
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 )
}