6 Commits

Author SHA1 Message Date
GotPPay
4b78235ed7 handle complete 2018-06-01 12:07:42 +02:00
GotPPay
f61c8b084d skeleton for completing reset procedure 2018-06-01 12:07:42 +02:00
GotPPay
636b1a1523 temp commit 2018-06-01 12:07:42 +02:00
GotPPay
eec4d177b1 skip token auth on password reset request 2018-06-01 12:07:42 +02:00
GotPPay
4731cfe7c2 add custom error for password reset 2018-06-01 12:07:42 +02:00
GotPPay
06ea1cb44d add password reset model ; create entry in database; send email 2018-06-01 12:07:42 +02:00
20 changed files with 645 additions and 62 deletions

View File

@@ -17,15 +17,16 @@ var (
// Service holds the domain service repositories // Service holds the domain service repositories
type Service struct { type Service struct {
Users *userService Users *userService
Rides *rideService Rides *rideService
Visits *visitService Visits *visitService
Provider *providerService Provider *providerService
Notification *notificationService Notification *notificationService
Profile *profileService Profile *profileService
Organization *organizationService Organization *organizationService
Zipcodes *zipcodeService Zipcodes *zipcodeService
Plan *planService Plan *planService
PasswordReset *passwordResetService
} }
// New returns a new domain Service instance // 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) bcbsi := bcbsi.New(cfg)
instance = &Service{ instance = &Service{
Users: newUserService(svc, mapper, bcbsi, cfg), Users: newUserService(svc, mapper),
Rides: newRideService(svc, mapper), Rides: newRideService(svc, mapper),
Visits: newVisitService(svc, mapper), Visits: newVisitService(svc, mapper),
Provider: newProviderService(svc, mapper), Provider: newProviderService(svc, mapper),
Notification: newNotificationService(svc, mapper, notification, cfg), Notification: newNotificationService(svc, mapper, notification, cfg),
Profile: newProfileService(svc, mapper), Profile: newProfileService(svc, mapper),
Organization: newOrganizationService(svc, mapper), Organization: newOrganizationService(svc, mapper),
Zipcodes: newZipcodeService(svc, mapper), Zipcodes: newZipcodeService(svc, mapper),
Plan: newPlanService(svc, mapper), Plan: newPlanService(svc, mapper),
PasswordReset: newPasswordResetService(svc, mapper),
} }
}) })

View File

@@ -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)
}

View File

@@ -77,6 +77,16 @@ func (s *userService) GetByMemberID(memberID string) (retVal viewmodel.User, err
return s.mapEntity.User.ToUserModel(user), nil 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 // 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) { 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) user, err := s.svc.Users.FullLogin(loginType, key, pass, profile)

View File

@@ -13,16 +13,17 @@ var (
// Mapper has mapping methods to map entities to view models // Mapper has mapping methods to map entities to view models
type Mapper struct { type Mapper struct {
User *userMapping User *userMapping
Ride *rideMapping Ride *rideMapping
Visit *visitMapping Visit *visitMapping
Address *addressMapping Address *addressMapping
Provider *providerMapping Provider *providerMapping
Notification *notificationMapping Notification *notificationMapping
Profile *profileMapping Profile *profileMapping
Organization *organizationMapping Organization *organizationMapping
Zipcode *zipcodeMapping Zipcode *zipcodeMapping
Plan *planMapping Plan *planMapping
PasswordReset *passwordResetMapping
} }
// New returns an EntityMapper for fluent mapping // New returns an EntityMapper for fluent mapping
@@ -40,6 +41,7 @@ func New() *Mapper {
instance.Organization = &organizationMapping{instance} instance.Organization = &organizationMapping{instance}
instance.Zipcode = &zipcodeMapping{instance} instance.Zipcode = &zipcodeMapping{instance}
instance.Plan = &planMapping{instance} instance.Plan = &planMapping{instance}
instance.PasswordReset = &passwordResetMapping{instance}
}) })
return instance return instance

View File

@@ -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,
}
}

View File

@@ -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"`
}

View File

@@ -20,16 +20,17 @@ var (
// Conn is the MySQL connection manager // Conn is the MySQL connection manager
type Conn struct { type Conn struct {
db *sql.DB db *sql.DB
users *userRepo users *userRepo
rides *rideRepo rides *rideRepo
visit *visitRepo visit *visitRepo
provider *providerRepo provider *providerRepo
notification *notificationRepo notification *notificationRepo
profile *profileRepo profile *profileRepo
organization *organizationRepo organization *organizationRepo
zipcodes *zipcodeRepo zipcodes *zipcodeRepo
plan *planRepo plan *planRepo
passwordReset *passwordResetRepo
} }
// Begin starts a transaction // Begin starts a transaction
@@ -90,6 +91,10 @@ func (c *Conn) Plans() contract.PlanRepo {
return c.plan return c.plan
} }
func (c *Conn) PasswordReset() contract.PasswordResetRepo {
return c.passwordReset
}
// Instance returns an instance of a DataManager // Instance returns an instance of a DataManager
func Instance(cfg *config.Config) (contract.DataManager, error) { func Instance(cfg *config.Config) (contract.DataManager, error) {
once.Do(func() { once.Do(func() {
@@ -123,6 +128,7 @@ func Instance(cfg *config.Config) (contract.DataManager, error) {
instance.organization = newOrganizationRepo(db) instance.organization = newOrganizationRepo(db)
instance.zipcodes = newZipcodeRepo(db) instance.zipcodes = newZipcodeRepo(db)
instance.plan = newPlanRepo(db) instance.plan = newPlanRepo(db)
instance.passwordReset = newPasswordResetRepo(db)
}) })
return instance, connErr return instance, connErr

View File

@@ -0,0 +1,144 @@
package datamysql
import (
"database/sql"
"fmt"
"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 *passwordResetRepo) CreatePasswordResetEntry(passwordResetEntry entity.PasswordReset) (entity.PasswordReset, error) {
const (
insertQuery = `INSERT INTO tab_password_reset(password_reset_uuid, user_id, token, expire_date, used, opened) VALUES(?, ?, ?, ?, 0, 0);`
)
retVal := passwordResetEntry
guid, _ := uuid.NewV4()
userRepo := newUserRepo(c.conn)
fullUser, err := userRepo.GetByEmail(passwordResetEntry.User.Email)
if err != nil {
return retVal, err
}
if fullUser.Email != passwordResetEntry.User.Email {
return retVal, fmt.Errorf("User not found")
}
results, err := c.conn.Exec(insertQuery, guid, fullUser.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 AND a.expire_date > CURRENT_TIMESTAMP`
)
result, err := c.conn.Exec(query, token)
if err != nil {
return err
}
if updateCount, err := result.RowsAffected(); err != nil || updateCount == 0 {
return fmt.Errorf("Invalid token")
}
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
}

View File

@@ -8,16 +8,17 @@ import (
) )
type transaction struct { type transaction struct {
tx *sql.Tx tx *sql.Tx
users *userRepo users *userRepo
rides *rideRepo rides *rideRepo
visits *visitRepo visits *visitRepo
provider *providerRepo provider *providerRepo
notification *notificationRepo notification *notificationRepo
profile *profileRepo profile *profileRepo
organization *organizationRepo organization *organizationRepo
zipcodes *zipcodeRepo zipcodes *zipcodeRepo
plan *planRepo plan *planRepo
passwordReset *passwordResetRepo
} }
func newTransaction(tx *sql.Tx) *transaction { func newTransaction(tx *sql.Tx) *transaction {
@@ -34,6 +35,7 @@ func newTransaction(tx *sql.Tx) *transaction {
t.organization = newOrganizationRepo(tx) t.organization = newOrganizationRepo(tx)
t.zipcodes = newZipcodeRepo(tx) t.zipcodes = newZipcodeRepo(tx)
t.plan = newPlanRepo(tx) t.plan = newPlanRepo(tx)
t.passwordReset = newPasswordResetRepo(tx)
return t return t
} }
@@ -81,6 +83,10 @@ func (t transaction) Plans() contract.PlanRepo {
return t.plan return t.plan
} }
func (t transaction) PasswordReset() contract.PasswordResetRepo {
return t.passwordReset
}
func (t *transaction) Commit() error { func (t *transaction) Commit() error {
err := t.tx.Commit() err := t.tx.Commit()

View File

@@ -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) { func (c *userRepo) GetByUUID(uuid string, profile string) (entity.User, error) {
params := make([]interface{}, 0) params := make([]interface{}, 0)
params = append(params, uuid) params = append(params, uuid)

View File

@@ -14,6 +14,7 @@ type repoManager interface {
Organization() OrganizationRepo Organization() OrganizationRepo
Zipcodes() ZipcodeRepo Zipcodes() ZipcodeRepo
Plans() PlanRepo Plans() PlanRepo
PasswordReset() PasswordResetRepo
} }
// UserRepo defines the data set for users // UserRepo defines the data set for users
@@ -22,6 +23,7 @@ type UserRepo interface {
GetByID(userID int64) (retVal entity.User, err error) GetByID(userID int64) (retVal entity.User, err error)
GetByUUID(uuid string, profile string) (entity.User, error) GetByUUID(uuid string, profile string) (entity.User, error)
GetByMemberID(memberID string) (entity.User, error) GetByMemberID(memberID string) (entity.User, error)
GetByEmail(email string) (entity.User, error)
Login(email string, pass string) (entity.User, error) Login(email string, pass string) (entity.User, error)
FullLogin(loginType string, key string, pass string, profile string) (entity.User, error) FullLogin(loginType string, key string, pass string, profile string) (entity.User, error)
Create(user entity.User) (entity.User, error) Create(user entity.User) (entity.User, error)
@@ -124,3 +126,12 @@ type ZipcodeRepo interface {
GetAll() ([]entity.Zipcode, error) GetAll() ([]entity.Zipcode, error)
GetByParticipatingZipcode(zipcode string) (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
}

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -15,18 +15,19 @@ var (
// Service holds the domain service repositories // Service holds the domain service repositories
type Service struct { type Service struct {
db contract.DataManager db contract.DataManager
cache contract.CacheManager cache contract.CacheManager
tnc contract.TNCManager tnc contract.TNCManager
Users *userService Users *userService
Rides *rideService Rides *rideService
Visits *visitService Visits *visitService
Provider *providerService Provider *providerService
Notification *notificationService Notification *notificationService
Profile *profileService Profile *profileService
Organization *organizationService Organization *organizationService
Zipcodes *zipcodeService Zipcodes *zipcodeService
Plans *planService Plans *planService
PasswordReset *passwordResetService
} }
// New returns a new domain Service instance // 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.Organization = newOrganizationService(instance)
instance.Zipcodes = newZipcodeService(instance) instance.Zipcodes = newZipcodeService(instance)
instance.Plans = newPlanService(instance) instance.Plans = newPlanService(instance)
instance.PasswordReset = newPasswordResetService(instance)
}) })
return instance, nil return instance, nil

View File

@@ -37,6 +37,10 @@ func (s *userService) GetByMemberID(memberID string) (entity.User, error) {
return s.svc.db.Users().GetByMemberID(memberID) 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 // Login returns a specific user by email and pass
func (s *userService) Login(email string, pass string) (entity.User, error) { func (s *userService) Login(email string, pass string) (entity.User, error) {
return s.svc.db.Users().Login(email, pass) 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 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) return s.svc.db.Users().UpdateLogin(user)
} }

View File

@@ -0,0 +1,151 @@
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"
)
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: "test.test.no@yandex.com",
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
if err := c.svc.PasswordReset.SetTokenUsed(userToken); err != nil {
routeutils.ResponseAPIPasswordResetFailed(ctx, "Reset failed")
}
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)
}

View File

@@ -0,0 +1,21 @@
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/:token"
tokenOpen = "/open/:token"
)
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)
r.POST(tokenOpen, ctrl.handleTokenOpen)
}

View File

@@ -13,6 +13,7 @@ import (
"bitbucket.org/nemt/nemt-portal-api/server/router/lyfthookroute" "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/notificationroute"
"bitbucket.org/nemt/nemt-portal-api/server/router/organizationroute" "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/placesroute"
"bitbucket.org/nemt/nemt-portal-api/server/router/profileroute" "bitbucket.org/nemt/nemt-portal-api/server/router/profileroute"
"bitbucket.org/nemt/nemt-portal-api/server/router/providerroute" "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) externalroute.Register(prefixGroup.Group("/ext"), cfg, svc, tnc, notification)
authenticateroute.Register(prefixGroup.Group("/authenticate"), cfg, svc) authenticateroute.Register(prefixGroup.Group("/authenticate"), cfg, svc)
selfregisterroute.Register(prefixGroup.Group("/selfregister"), cfg, svc) selfregisterroute.Register(prefixGroup.Group("/selfregister"), cfg, svc)
passwordresetroute.Register(prefixGroup.Group("/passwordreset"), cfg, svc)
appGroup := prefixGroup.Group("/" + cfg.App.Name) appGroup := prefixGroup.Group("/" + cfg.App.Name)
usersroute.Register(appGroup.Group("/users"), cfg, svc) usersroute.Register(appGroup.Group("/users"), cfg, svc)

View File

@@ -102,6 +102,11 @@ func ResponseAPINotEligibleWithMessageError(c echo.Context, message string) erro
return ResponseAPIError(c, http.StatusForbidden, message, false) return ResponseAPIError(c, http.StatusForbidden, message, false)
} }
//ResponseAPIPasswordResetFailed returns a standard API error when password reset fails
func ResponseAPIPasswordResetFailed(c echo.Context, message string) error {
return ResponseAPIError(c, http.StatusForbidden, message, false)
}
func ignoreDefaultWrappedErrors(c echo.Context, errorToHandle *errors.WrappedError, handler func(echo.Context, error) error) error { func ignoreDefaultWrappedErrors(c echo.Context, errorToHandle *errors.WrappedError, handler func(echo.Context, error) error) error {
err := errorToHandle.GetOriginalError() err := errorToHandle.GetOriginalError()

View File

@@ -19,7 +19,8 @@ func authSkipper(ctx echo.Context) bool {
strings.Contains(path, "/v1/notification/ws") || strings.Contains(path, "/v1/notification/ws") ||
strings.HasPrefix(path, "/v1/lyfthook") || strings.HasPrefix(path, "/v1/lyfthook") ||
strings.HasPrefix(path, "/v1/docs") || strings.HasPrefix(path, "/v1/docs") ||
strings.HasPrefix(path, "/v1/selfregister")) strings.HasPrefix(path, "/v1/selfregister") ||
strings.HasPrefix(path, "/v1/passwordreset"))
} }
// appSkipper is the default skipper for the application routes // appSkipper is the default skipper for the application routes