diff --git a/application/applicationservice/applicationservice.go b/application/applicationservice/applicationservice.go index 5cf4b7c..6745970 100644 --- a/application/applicationservice/applicationservice.go +++ b/application/applicationservice/applicationservice.go @@ -17,15 +17,16 @@ var ( // Service holds the domain service repositories type Service struct { - Users *userService - Rides *rideService - Visits *visitService - Provider *providerService - Notification *notificationService - Profile *profileService - Organization *organizationService - Zipcodes *zipcodeService - Plan *planService + Users *userService + Rides *rideService + Visits *visitService + Provider *providerService + Notification *notificationService + Profile *profileService + Organization *organizationService + Zipcodes *zipcodeService + Plan *planService + PasswordReset *passwordResetService } // New returns a new domain Service instance @@ -34,15 +35,16 @@ func New(svc *service.Service, mapper *entitymapping.Mapper, notification *notif bcbsi := bcbsi.New(cfg) instance = &Service{ - Users: newUserService(svc, mapper, bcbsi, cfg), - Rides: newRideService(svc, mapper), - Visits: newVisitService(svc, mapper), - Provider: newProviderService(svc, mapper), - Notification: newNotificationService(svc, mapper, notification, cfg), - Profile: newProfileService(svc, mapper), - Organization: newOrganizationService(svc, mapper), - Zipcodes: newZipcodeService(svc, mapper), - Plan: newPlanService(svc, mapper), + Users: newUserService(svc, mapper), + Rides: newRideService(svc, mapper), + Visits: newVisitService(svc, mapper), + Provider: newProviderService(svc, mapper), + Notification: newNotificationService(svc, mapper, notification, cfg), + Profile: newProfileService(svc, mapper), + Organization: newOrganizationService(svc, mapper), + Zipcodes: newZipcodeService(svc, mapper), + Plan: newPlanService(svc, mapper), + PasswordReset: newPasswordResetService(svc, mapper), } }) diff --git a/application/applicationservice/passwordresetservice.go b/application/applicationservice/passwordresetservice.go new file mode 100644 index 0000000..dc3a8de --- /dev/null +++ b/application/applicationservice/passwordresetservice.go @@ -0,0 +1,62 @@ +package applicationservice + +import ( + "bitbucket.org/nemt/nemt-portal-api/application/entitymapping" + "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" + "bitbucket.org/nemt/nemt-portal-api/domain/service" +) + +// zipcodeService holds methods to participating zipcode application service +type passwordResetService struct { + svc *service.Service + mapEntity *entitymapping.Mapper +} + +// newZipcodeService returns a zipcodeService instance +func newPasswordResetService(svc *service.Service, mapper *entitymapping.Mapper) *passwordResetService { + return &passwordResetService{ + svc: svc, + mapEntity: mapper, + } +} + +func (s *passwordResetService) GetAll() ([]viewmodel.PasswordReset, error) { + result, err := s.svc.PasswordReset.GetAll() + if err != nil { + return nil, err + } + return s.mapEntity.PasswordReset.ToPasswordResetModelSlice(result), nil +} + +func (s *passwordResetService) CreatePasswordResetEntry(passwordResetEntry viewmodel.PasswordReset) (viewmodel.PasswordReset, error) { + passwordResetEntity := s.mapEntity.PasswordReset.ToPasswordResetEntity(passwordResetEntry) + result, err := s.svc.PasswordReset.CreatePasswordResetEntry(passwordResetEntity) + if err != nil { + return viewmodel.PasswordReset{}, err + } + return s.mapEntity.PasswordReset.ToPasswordResetModel(result), nil +} + +func (s *passwordResetService) GetByID(ID int64) (viewmodel.PasswordReset, error) { + result, err := s.svc.PasswordReset.GetByID(ID) + if err != nil { + return viewmodel.PasswordReset{}, err + } + return s.mapEntity.PasswordReset.ToPasswordResetModel(result), nil +} + +func (s *passwordResetService) GetByToken(token string) (viewmodel.PasswordReset, error) { + result, err := s.svc.PasswordReset.GetByToken(token) + if err != nil { + return viewmodel.PasswordReset{}, err + } + return s.mapEntity.PasswordReset.ToPasswordResetModel(result), nil +} + +func (s *passwordResetService) SetTokenOpened(token string) error { + return s.svc.PasswordReset.SetTokenOpened(token) +} + +func (s *passwordResetService) SetTokenUsed(token string) error { + return s.svc.PasswordReset.SetTokenUsed(token) +} diff --git a/application/applicationservice/user.go b/application/applicationservice/user.go index a0abcf4..29d8b2a 100644 --- a/application/applicationservice/user.go +++ b/application/applicationservice/user.go @@ -77,6 +77,16 @@ func (s *userService) GetByMemberID(memberID string) (retVal viewmodel.User, err return s.mapEntity.User.ToUserModel(user), nil } +// GetByEmail returns a specific user by its email +func (s *userService) GetByEmail(email string) (retVal viewmodel.User, err error) { + user, err := s.svc.Users.GetByEmail(email) + if err != nil { + return retVal, errors.Wrap(err) + } + + return s.mapEntity.User.ToUserModel(user), nil +} + // Login returns a specific user by email and pass func (s *userService) FullLogin(loginType string, key string, pass string, profile string) (retVal viewmodel.User, err error) { user, err := s.svc.Users.FullLogin(loginType, key, pass, profile) diff --git a/application/entitymapping/entitymapping.go b/application/entitymapping/entitymapping.go index 1e39153..9213463 100644 --- a/application/entitymapping/entitymapping.go +++ b/application/entitymapping/entitymapping.go @@ -13,16 +13,17 @@ var ( // Mapper has mapping methods to map entities to view models type Mapper struct { - User *userMapping - Ride *rideMapping - Visit *visitMapping - Address *addressMapping - Provider *providerMapping - Notification *notificationMapping - Profile *profileMapping - Organization *organizationMapping - Zipcode *zipcodeMapping - Plan *planMapping + User *userMapping + Ride *rideMapping + Visit *visitMapping + Address *addressMapping + Provider *providerMapping + Notification *notificationMapping + Profile *profileMapping + Organization *organizationMapping + Zipcode *zipcodeMapping + Plan *planMapping + PasswordReset *passwordResetMapping } // New returns an EntityMapper for fluent mapping @@ -40,6 +41,7 @@ func New() *Mapper { instance.Organization = &organizationMapping{instance} instance.Zipcode = &zipcodeMapping{instance} instance.Plan = &planMapping{instance} + instance.PasswordReset = &passwordResetMapping{instance} }) return instance diff --git a/application/entitymapping/passwordreset.go b/application/entitymapping/passwordreset.go new file mode 100644 index 0000000..5e5878e --- /dev/null +++ b/application/entitymapping/passwordreset.go @@ -0,0 +1,59 @@ +package entitymapping + +import ( + "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" + "bitbucket.org/nemt/nemt-portal-api/domain/entity" +) + +// zipcodeMapping has method to map zipcode entities to view models +type passwordResetMapping struct { + mapper *Mapper +} + +// ToUserEntitySlice maps a User entity slice to User view model slice +func (mapping *passwordResetMapping) ToPasswordResetEntitySlice(list []viewmodel.PasswordReset) (retVal []entity.PasswordReset) { + retVal = make([]entity.PasswordReset, 0) + + for _, item := range list { + retVal = append(retVal, mapping.ToPasswordResetEntity(item)) + } + + return retVal +} + +func (mapping *passwordResetMapping) ToPasswordResetEntity(model viewmodel.PasswordReset) entity.PasswordReset { + return entity.PasswordReset{ + ID: model.ID, + UUID: model.UUID, + User: mapping.mapper.User.ToUserEntity(model.User), + Token: model.Token, + Created: model.Created, + Expires: model.Expires, + Used: model.Used, + Opened: model.Opened, + } +} + +// ToUserEntitySlice maps a User entity slice to User view model slice +func (mapping *passwordResetMapping) ToPasswordResetModelSlice(list []entity.PasswordReset) (retVal []viewmodel.PasswordReset) { + retVal = make([]viewmodel.PasswordReset, 0) + + for _, item := range list { + retVal = append(retVal, mapping.ToPasswordResetModel(item)) + } + + return retVal +} + +func (mapping *passwordResetMapping) ToPasswordResetModel(model entity.PasswordReset) viewmodel.PasswordReset { + return viewmodel.PasswordReset{ + ID: model.ID, + UUID: model.UUID, + User: mapping.mapper.User.ToUserModel(model.User), + Token: model.Token, + Created: model.Created, + Expires: model.Expires, + Used: model.Used, + Opened: model.Opened, + } +} diff --git a/application/viewmodel/passwordreset.go b/application/viewmodel/passwordreset.go new file mode 100644 index 0000000..c2a2232 --- /dev/null +++ b/application/viewmodel/passwordreset.go @@ -0,0 +1,14 @@ +package viewmodel + +import "time" + +type PasswordReset struct { + ID int64 `json:"-"` + UUID string `json:"uuid"` + User User `json:"user"` + Token string `json:"token"` + Created time.Time `json:"create_date"` + Expires time.Time `json:"expire_date"` + Used bool `json:"used"` + Opened bool `json:"opened"` +} diff --git a/data/datamysql/datamysql.go b/data/datamysql/datamysql.go index d57838d..1a2e056 100644 --- a/data/datamysql/datamysql.go +++ b/data/datamysql/datamysql.go @@ -20,16 +20,17 @@ var ( // Conn is the MySQL connection manager type Conn struct { - db *sql.DB - users *userRepo - rides *rideRepo - visit *visitRepo - provider *providerRepo - notification *notificationRepo - profile *profileRepo - organization *organizationRepo - zipcodes *zipcodeRepo - plan *planRepo + db *sql.DB + users *userRepo + rides *rideRepo + visit *visitRepo + provider *providerRepo + notification *notificationRepo + profile *profileRepo + organization *organizationRepo + zipcodes *zipcodeRepo + plan *planRepo + passwordReset *passwordResetRepo } // Begin starts a transaction @@ -90,6 +91,10 @@ func (c *Conn) Plans() contract.PlanRepo { return c.plan } +func (c *Conn) PasswordReset() contract.PasswordResetRepo { + return c.passwordReset +} + // Instance returns an instance of a DataManager func Instance(cfg *config.Config) (contract.DataManager, error) { once.Do(func() { @@ -123,6 +128,7 @@ func Instance(cfg *config.Config) (contract.DataManager, error) { instance.organization = newOrganizationRepo(db) instance.zipcodes = newZipcodeRepo(db) instance.plan = newPlanRepo(db) + instance.passwordReset = newPasswordResetRepo(db) }) return instance, connErr diff --git a/data/datamysql/passwordreset.go b/data/datamysql/passwordreset.go new file mode 100644 index 0000000..36297f8 --- /dev/null +++ b/data/datamysql/passwordreset.go @@ -0,0 +1,132 @@ +package datamysql + +import ( + "database/sql" + + "bitbucket.org/nemt/nemt-portal-api/domain/entity" + "bitbucket.org/nemt/nemt-portal-api/infra/errors" + uuid "github.com/satori/go.uuid" +) + +// rideRepo maps methods to database +type passwordResetRepo struct { + conn executor +} + +func newPasswordResetRepo(conn executor) *passwordResetRepo { + return &passwordResetRepo{ + conn: conn, + } +} + +func (c *passwordResetRepo) getQuery() string { + const ( + query = `SELECT + a.password_reset_id, + a.password_reset_uuid, + a.user_id, + b.user_uuid, + a.token, + a.create_date, + a.expire_date, + (IFNULL(a.used, b'0') = b'1') used + (IFNULL(a.opened, b'0') = b'1') opened + FROM + tab_password_reset a + INNER JOIN tab_user b + ON a.user_id = b.user_id` + ) + + return query +} + +// parseSet parses a result set result to an entity array +func (c *passwordResetRepo) parseSet(rows *sql.Rows, err error) ([]entity.PasswordReset, error) { + if err != nil { + return nil, errors.Wrap(err) + } + result := make([]entity.PasswordReset, 0) + for rows.Next() { + entity, err := c.parseEntity(rows) + if err != nil { + return nil, errors.Wrap(err) + } + result = append(result, entity) + } + return result, nil +} + +// parseEntity parses a result to an entity +func (c *passwordResetRepo) parseEntity(row scanner) (retVal entity.PasswordReset, err error) { + err = row.Scan( + &retVal.ID, &retVal.UUID, &retVal.User.ID, &retVal.User.UUID, &retVal.Token, &retVal.Created, &retVal.Expires, &retVal.Used, &retVal.Opened) + + return retVal, errors.Wrap(err) +} + +func (c *passwordResetRepo) GetAll() ([]entity.PasswordReset, error) { + return c.parseSet(c.conn.Query(c.getQuery())) +} + +/* +func (c *zipcodeRepo) GetByParticipatingZipcode(zipcode string) (entity.Zipcode, error) { + return c.parseEntity(c.conn.QueryRow(c.getQuery()+"WHERE a.participating = 1 AND a.zipcode = ?", zipcode)) +}*/ + +func (c *passwordResetRepo) CreatePasswordResetEntry(passwordResetEntry entity.PasswordReset) (entity.PasswordReset, error) { + const ( + query = `INSERT INTO tab_password_reset(password_reset_uuid, user_id, token, expire_date, used, opened) VALUES(?, ?, ?, ?, ?, 0, 0);` + ) + + retVal := passwordResetEntry + guid, _ := uuid.NewV4() + + results, err := c.conn.Exec(query, guid, passwordResetEntry.User.ID, passwordResetEntry.Token, passwordResetEntry.Expires) + if err != nil { + return retVal, err + } + + retVal.ID, err = results.LastInsertId() + if err != nil { + return retVal, err + } + + return c.GetByID(retVal.ID) +} + +func (c *passwordResetRepo) GetByID(ID int64) (entity.PasswordReset, error) { + return c.parseEntity(c.conn.QueryRow(c.getQuery()+" WHERE a.password_reset_id = ?; ", ID)) +} + +func (c *passwordResetRepo) GetByToken(token string) (entity.PasswordReset, error) { + return c.parseEntity(c.conn.QueryRow(c.getQuery()+" WHERE a.token = ? AND a.used = 0; ", token)) +} + +func (c *passwordResetRepo) SetTokenOpened(token string) error { + const ( + query = `UPDATE tab_password_reset a + SET a.opened = 1, + WHERE a.token = ? AND a.used = 0` + ) + + if _, err := c.conn.Exec(query, token); err != nil { + return err + } + + return nil +} + +func (c *passwordResetRepo) SetTokenUsed(token string) error { + const ( + query = `UPDATE tab_password_reset a + SET a.opened = 1, + a.used = 1, + WHERE a.token = ? AND a.used = 0` + ) + + if _, err := c.conn.Exec(query, token); err != nil { + return err + } + + return nil +} diff --git a/data/datamysql/transaction.go b/data/datamysql/transaction.go index 6bc7fd0..5da061e 100644 --- a/data/datamysql/transaction.go +++ b/data/datamysql/transaction.go @@ -8,16 +8,17 @@ import ( ) type transaction struct { - tx *sql.Tx - users *userRepo - rides *rideRepo - visits *visitRepo - provider *providerRepo - notification *notificationRepo - profile *profileRepo - organization *organizationRepo - zipcodes *zipcodeRepo - plan *planRepo + tx *sql.Tx + users *userRepo + rides *rideRepo + visits *visitRepo + provider *providerRepo + notification *notificationRepo + profile *profileRepo + organization *organizationRepo + zipcodes *zipcodeRepo + plan *planRepo + passwordReset *passwordResetRepo } func newTransaction(tx *sql.Tx) *transaction { @@ -34,6 +35,7 @@ func newTransaction(tx *sql.Tx) *transaction { t.organization = newOrganizationRepo(tx) t.zipcodes = newZipcodeRepo(tx) t.plan = newPlanRepo(tx) + t.passwordReset = newPasswordResetRepo(tx) return t } @@ -81,6 +83,10 @@ func (t transaction) Plans() contract.PlanRepo { return t.plan } +func (t transaction) PasswordReset() contract.PasswordResetRepo { + return t.passwordReset +} + func (t *transaction) Commit() error { err := t.tx.Commit() diff --git a/data/datamysql/user.go b/data/datamysql/user.go index 4e39bc7..5c681bc 100644 --- a/data/datamysql/user.go +++ b/data/datamysql/user.go @@ -48,6 +48,32 @@ func (c *userRepo) GetByMemberID(memberID string) (entity.User, error) { } } +func (c *userRepo) GetByEmail(email string) (entity.User, error) { + finalQuery := c.getQuery() + " AND b.email = ?" + + user, err := c.parseSet(c.conn.Query(finalQuery, email)) + if err != nil { + return entity.User{}, err + } + + if len(user) > 0 { + retVal := user[0] + retVal.Contacts, err = c.GetContacts(retVal.ID) + if err != nil { + return entity.User{}, err + } + + retVal.Addresses = nil + retVal.Addresses, err = c.getAddressByUserID(retVal.ID) + if err != nil { + return entity.User{}, err + } + return retVal, nil + } else { + return entity.User{}, nil + } +} + func (c *userRepo) GetByUUID(uuid string, profile string) (entity.User, error) { params := make([]interface{}, 0) params = append(params, uuid) diff --git a/domain/contract/repo.go b/domain/contract/repo.go index f875f7d..6d07202 100644 --- a/domain/contract/repo.go +++ b/domain/contract/repo.go @@ -14,6 +14,7 @@ type repoManager interface { Organization() OrganizationRepo Zipcodes() ZipcodeRepo Plans() PlanRepo + PasswordReset() PasswordResetRepo } // UserRepo defines the data set for users @@ -22,6 +23,7 @@ type UserRepo interface { GetByID(userID int64) (retVal entity.User, err error) GetByUUID(uuid string, profile string) (entity.User, error) GetByMemberID(memberID string) (entity.User, error) + GetByEmail(email string) (entity.User, error) Login(email string, pass string) (entity.User, error) FullLogin(loginType string, key string, pass string, profile string) (entity.User, error) Create(user entity.User) (entity.User, error) @@ -124,3 +126,12 @@ type ZipcodeRepo interface { GetAll() ([]entity.Zipcode, error) GetByParticipatingZipcode(zipcode string) (entity.Zipcode, error) } + +type PasswordResetRepo interface { + GetAll() ([]entity.PasswordReset, error) + CreatePasswordResetEntry(passwordResetEntry entity.PasswordReset) (entity.PasswordReset, error) + GetByID(ID int64) (entity.PasswordReset, error) + GetByToken(token string) (entity.PasswordReset, error) + SetTokenOpened(token string) error + SetTokenUsed(token string) error +} diff --git a/domain/entity/passwordreset.go b/domain/entity/passwordreset.go new file mode 100644 index 0000000..633d1df --- /dev/null +++ b/domain/entity/passwordreset.go @@ -0,0 +1,14 @@ +package entity + +import "time" + +type PasswordReset struct { + ID int64 `db:"password_reset_id" json:"-"` + UUID string `db:"password_reset_uuid" json:"uuid"` + User User `db:"-" json:"user"` + Token string `db:"token" json:"token"` + Created time.Time `db:"create_date" json:"create_date"` + Expires time.Time `db:"expire_date" json:"expire_date"` + Used bool `db:"used" json:"used"` + Opened bool `db:"opend" json:"opened"` +} diff --git a/domain/service/passwordreset.go b/domain/service/passwordreset.go new file mode 100644 index 0000000..2e97aae --- /dev/null +++ b/domain/service/passwordreset.go @@ -0,0 +1,41 @@ +package service + +import ( + "bitbucket.org/nemt/nemt-portal-api/domain/entity" +) + +// userService is the domain service for user operations +type passwordResetService struct { + svc *Service +} + +// newUserService returns an instance of userService +func newPasswordResetService(svc *Service) *passwordResetService { + return &passwordResetService{ + svc: svc, + } +} + +func (s *passwordResetService) GetAll() ([]entity.PasswordReset, error) { + return s.svc.db.PasswordReset().GetAll() +} + +func (s *passwordResetService) GetByID(ID int64) (entity.PasswordReset, error) { + return s.svc.db.PasswordReset().GetByID(ID) +} + +func (s *passwordResetService) GetByToken(token string) (entity.PasswordReset, error) { + return s.svc.db.PasswordReset().GetByToken(token) +} + +func (s *passwordResetService) CreatePasswordResetEntry(passwordResetEntry entity.PasswordReset) (entity.PasswordReset, error) { + return s.svc.db.PasswordReset().CreatePasswordResetEntry(passwordResetEntry) +} + +func (s *passwordResetService) SetTokenOpened(token string) error { + return s.svc.db.PasswordReset().SetTokenOpened(token) +} + +func (s *passwordResetService) SetTokenUsed(token string) error { + return s.svc.db.PasswordReset().SetTokenUsed(token) +} diff --git a/domain/service/service.go b/domain/service/service.go index 1bfb2b6..419a798 100644 --- a/domain/service/service.go +++ b/domain/service/service.go @@ -15,18 +15,19 @@ var ( // Service holds the domain service repositories type Service struct { - db contract.DataManager - cache contract.CacheManager - tnc contract.TNCManager - Users *userService - Rides *rideService - Visits *visitService - Provider *providerService - Notification *notificationService - Profile *profileService - Organization *organizationService - Zipcodes *zipcodeService - Plans *planService + db contract.DataManager + cache contract.CacheManager + tnc contract.TNCManager + Users *userService + Rides *rideService + Visits *visitService + Provider *providerService + Notification *notificationService + Profile *profileService + Organization *organizationService + Zipcodes *zipcodeService + Plans *planService + PasswordReset *passwordResetService } // New returns a new domain Service instance @@ -43,6 +44,7 @@ func New(db contract.DataManager, cache contract.CacheManager, cfg *config.Confi instance.Organization = newOrganizationService(instance) instance.Zipcodes = newZipcodeService(instance) instance.Plans = newPlanService(instance) + instance.PasswordReset = newPasswordResetService(instance) }) return instance, nil diff --git a/domain/service/user.go b/domain/service/user.go index ea00135..ca53aab 100644 --- a/domain/service/user.go +++ b/domain/service/user.go @@ -37,6 +37,10 @@ func (s *userService) GetByMemberID(memberID string) (entity.User, error) { return s.svc.db.Users().GetByMemberID(memberID) } +func (s *userService) GetByEmail(email string) (entity.User, error) { + return s.svc.db.Users().GetByEmail(email) +} + // Login returns a specific user by email and pass func (s *userService) Login(email string, pass string) (entity.User, error) { return s.svc.db.Users().Login(email, pass) @@ -72,7 +76,7 @@ func (s *userService) CreateBulk(users []entity.User) ([]entity.User, error) { return users, nil } -func (s *userService) UpdateLogin(user entity.User) error { +func (s *userService) UpdateLogin(user entity.User) error { return s.svc.db.Users().UpdateLogin(user) } diff --git a/server/router/passwordresetroute/controller.go b/server/router/passwordresetroute/controller.go new file mode 100644 index 0000000..9d1205c --- /dev/null +++ b/server/router/passwordresetroute/controller.go @@ -0,0 +1,123 @@ +package passwordresetroute + +import ( + "crypto/sha256" + "fmt" + "math/rand" + "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 " + string(tokenExpirationTime) + " minutes" +) + +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 { + fmt.Println("\n\nRequest...") + userEmail, err := routeutils.GetAndValidateStringParam(ctx, "email", "mandatory field") + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + fmt.Println("\nEmail : ", userEmail) + + //find if user with email exists + user, err := c.svc.Users.GetByEmail(userEmail) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + //create and store reset token + + timeNow := time.Now() + expirationTime := timeNow.Add(time.Hour * tokenExpirationTime) + + randomArray := make([]byte, randomStringLength) + rand.Read(randomArray) + h := sha256.New() + h.Write(randomArray) + token := string(h.Sum(nil)) + + 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 { + /* + 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) + } + + //create and store reset token + + //send email with reset link + */ + + return routeutils.ResponseAPIOK(ctx, nil) +} diff --git a/server/router/passwordresetroute/router.go b/server/router/passwordresetroute/router.go new file mode 100644 index 0000000..0207f24 --- /dev/null +++ b/server/router/passwordresetroute/router.go @@ -0,0 +1,19 @@ +package passwordresetroute + +import ( + "bitbucket.org/nemt/nemt-portal-api/application/applicationservice" + "bitbucket.org/nemt/nemt-portal-api/infra/config" + "github.com/labstack/echo" +) + +const ( + resetRequest = "/request/:email" + resetComplete = "/complete" +) + +func Register(r *echo.Group, cfg *config.Config, svc *applicationservice.Service) { + ctrl := controllerInstance(svc, cfg) + + r.POST(resetRequest, ctrl.handleResetRequest) + r.POST(resetComplete, ctrl.handleResetComplete) +} diff --git a/server/router/router.go b/server/router/router.go index de0f3b3..63a0ef0 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -13,6 +13,7 @@ import ( "bitbucket.org/nemt/nemt-portal-api/server/router/lyfthookroute" "bitbucket.org/nemt/nemt-portal-api/server/router/notificationroute" "bitbucket.org/nemt/nemt-portal-api/server/router/organizationroute" + "bitbucket.org/nemt/nemt-portal-api/server/router/passwordresetroute" "bitbucket.org/nemt/nemt-portal-api/server/router/placesroute" "bitbucket.org/nemt/nemt-portal-api/server/router/profileroute" "bitbucket.org/nemt/nemt-portal-api/server/router/providerroute" @@ -39,6 +40,7 @@ func Register(e *echo.Echo, cfg *config.Config, svc *applicationservice.Service, externalroute.Register(prefixGroup.Group("/ext"), cfg, svc, tnc, notification) authenticateroute.Register(prefixGroup.Group("/authenticate"), cfg, svc) selfregisterroute.Register(prefixGroup.Group("/selfregister"), cfg, svc) + passwordresetroute.Register(prefixGroup.Group("/passwordreset"), cfg, svc) appGroup := prefixGroup.Group("/" + cfg.App.Name) usersroute.Register(appGroup.Group("/users"), cfg, svc)