Upstream sync
This commit is contained in:
62
application/applicationservice/passwordresetservice.go
Normal file
62
application/applicationservice/passwordresetservice.go
Normal 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)
|
||||
}
|
||||
59
application/entitymapping/passwordreset.go
Normal file
59
application/entitymapping/passwordreset.go
Normal 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,
|
||||
}
|
||||
}
|
||||
14
application/viewmodel/passwordreset.go
Normal file
14
application/viewmodel/passwordreset.go
Normal 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"`
|
||||
}
|
||||
144
data/datamysql/passwordreset.go
Normal file
144
data/datamysql/passwordreset.go
Normal 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
|
||||
}
|
||||
14
domain/entity/passwordreset.go
Normal file
14
domain/entity/passwordreset.go
Normal 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"`
|
||||
}
|
||||
41
domain/service/passwordreset.go
Normal file
41
domain/service/passwordreset.go
Normal 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)
|
||||
}
|
||||
168
server/router/passwordresetroute/controller.go
Normal file
168
server/router/passwordresetroute/controller.go
Normal file
@@ -0,0 +1,168 @@
|
||||
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)
|
||||
}
|
||||
21
server/router/passwordresetroute/router.go
Normal file
21
server/router/passwordresetroute/router.go
Normal 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)
|
||||
}
|
||||
BIN
svijetlastrana
Executable file
BIN
svijetlastrana
Executable file
Binary file not shown.
Reference in New Issue
Block a user