Skip to content

Commit 2ad85ce

Browse files
authored
Merge pull request #13 from hammercode-dev/be-03/04/07/feat(forget-password)-forgot&set-password
Be 03/04/07/feat(forget password) forgot, set password, and migration
2 parents 82f2edf + c41b12c commit 2ad85ce

32 files changed

+845
-39
lines changed

app/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func InitApp(
4444
middleware := middlewares.InitMiddleware(jwtInstance, userRepo)
4545

4646
// usecase
47-
userUsecase := users.InitUsecase(userRepo, dbTx, jwtInstance)
47+
userUsecase := users.InitUsecase(cfg, userRepo, dbTx, jwtInstance)
4848
newsletterUC := newsletters.InitUsecase(cfg, newsletterRepo, dbTx, jwt.NewJwt(cfg.JWT_SECRET_KEY))
4949
eventUC := events.InitUsecase(eventRepo, imgRepo, dbTx)
5050
imgUc := images.InitUsecase(imgRepo, dbTx)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package http
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"regexp"
8+
9+
"github.com/hammer-code/lms-be/domain"
10+
"github.com/hammer-code/lms-be/utils"
11+
)
12+
13+
func (h Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
14+
reEmail := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
15+
16+
bodyBytes, err := io.ReadAll(r.Body)
17+
if err != nil {
18+
utils.Response(domain.HttpResponse{
19+
Code: 500,
20+
Message: err.Error(),
21+
Data: nil,
22+
}, w)
23+
return
24+
}
25+
26+
forgotPassword := domain.ForgotPassword{}
27+
if err = json.Unmarshal(bodyBytes, &forgotPassword); err != nil {
28+
utils.Response(domain.HttpResponse{
29+
Code: 500,
30+
Message: err.Error(),
31+
}, w)
32+
return
33+
}
34+
35+
if isValidEmail := reEmail.MatchString(forgotPassword.Email); !isValidEmail {
36+
utils.Response(domain.HttpResponse{
37+
Code: 400,
38+
Message: "Email is not valid",
39+
}, w)
40+
return
41+
}
42+
43+
if err := h.usecase.ForgotPassword(r.Context(), forgotPassword); err != nil {
44+
utils.Response(domain.HttpResponse{
45+
Code: 500,
46+
Message: err.Error(),
47+
}, w)
48+
return
49+
}
50+
51+
utils.Response(domain.HttpResponse{
52+
Code: 200,
53+
Message: "Request reset password success check your email",
54+
Data: nil,
55+
}, w)
56+
57+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package http
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"regexp"
8+
9+
"github.com/hammer-code/lms-be/domain"
10+
"github.com/hammer-code/lms-be/utils"
11+
)
12+
13+
func (h Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
14+
15+
passwordRegex := regexp.MustCompile(`^[a-zA-Z\d]{8,}$`)
16+
17+
bodyBytes, err := io.ReadAll(r.Body)
18+
if err != nil {
19+
utils.Response(domain.HttpResponse{
20+
Code: 500,
21+
Message: "Failed to read request body :" + err.Error(),
22+
}, w)
23+
return
24+
}
25+
26+
forgotPasswordInstance := domain.ForgotPassword{}
27+
28+
if err := json.Unmarshal(bodyBytes, &forgotPasswordInstance); err != nil {
29+
utils.Response(domain.HttpResponse{
30+
Code: 500,
31+
Message: "Failed to unmarshal request body :" + err.Error(),
32+
}, w)
33+
return
34+
}
35+
36+
if forgotPasswordInstance.Password != forgotPasswordInstance.ConfirmPassword {
37+
utils.Response(domain.HttpResponse{
38+
Code: 400,
39+
Message: "Password and confirm password must be the same",
40+
}, w)
41+
return
42+
}
43+
44+
if isValidPass := passwordRegex.MatchString(forgotPasswordInstance.Password); !isValidPass {
45+
utils.Response(domain.HttpResponse{
46+
Code: 400,
47+
Message: "Password must contain at least 8 characters, one uppercase letter, one lowercase letter, and one number",
48+
}, w)
49+
return
50+
}
51+
52+
if err := h.usecase.ResetPassword(r.Context(), forgotPasswordInstance); err != nil {
53+
utils.Response(domain.HttpResponse{
54+
Code: 500,
55+
Message: "Failed to reset password :" + err.Error(),
56+
}, w)
57+
return
58+
59+
}
60+
61+
utils.Response(domain.HttpResponse{
62+
Code: 200,
63+
Message: "Reset password success",
64+
}, w)
65+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package repository
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/hammer-code/lms-be/domain"
8+
"github.com/sirupsen/logrus"
9+
)
10+
11+
func (repo *repository) ForgotPassword(ctx context.Context, token string, expiredAt time.Time, user domain.User) (err error) {
12+
err = repo.db.DB(ctx).Create(&domain.ResetPasswordToken{
13+
Token: token,
14+
UserID: uint64(user.ID),
15+
ExpiryDate: expiredAt,
16+
}).Error
17+
if err != nil {
18+
logrus.Error("repo.ForgotPassword : failed to create reset password token")
19+
return err
20+
}
21+
22+
return nil
23+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package repository
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/hammer-code/lms-be/domain"
8+
"github.com/sirupsen/logrus"
9+
)
10+
11+
func (repo *repository) ResetPassword(ctx context.Context, email, password, token string) error {
12+
if err := repo.db.StartTransaction(ctx, func(ctx context.Context) error {
13+
resetPasswordTokenInstance := domain.ResetPasswordToken{}
14+
if err := repo.db.DB(ctx).Model(resetPasswordTokenInstance).Where("token = ?", token).First(&resetPasswordTokenInstance).Error; err != nil {
15+
logrus.Error("repo.ResetPassword: failed to find token")
16+
return err
17+
}
18+
19+
if resetPasswordTokenInstance.IsUsed {
20+
logrus.Error("repo.ResetPassword: token already used")
21+
return errors.New("token already used")
22+
}
23+
24+
if err := repo.db.DB(ctx).Model(domain.User{}).Where("email = ?", email).Update("password", password).Error; err != nil {
25+
logrus.Error("repo.ResetPassword: failed to update password")
26+
return err
27+
}
28+
29+
if err := repo.db.DB(ctx).Model(domain.ResetPasswordToken{}).Where("token = ?", token).Update("is_used", true).Error; err != nil {
30+
logrus.Error("repo.ResetPassword: failed to update token state")
31+
return err
32+
}
33+
return nil
34+
}); err != nil {
35+
logrus.Error("repo.ResetPassword: failed to reset password")
36+
return err
37+
}
38+
return nil
39+
}

app/users/usecase/forgot_password.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package usecase
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"html/template"
8+
"net/smtp"
9+
"os"
10+
11+
"github.com/hammer-code/lms-be/domain"
12+
"github.com/sirupsen/logrus"
13+
)
14+
15+
func (us *usecase) ForgotPassword(ctx context.Context, emailForgot domain.ForgotPassword) (err error) {
16+
user := domain.User{}
17+
err = us.dbTX.StartTransaction(ctx, func(txCtx context.Context) error {
18+
user, err = us.userRepo.FindByEmail(ctx, emailForgot.Email)
19+
if err != nil {
20+
logrus.Error("us.ForgotPassword: failed to get Email", err)
21+
return err
22+
}
23+
return nil
24+
})
25+
26+
if err != nil {
27+
logrus.Error("us.ForgotPassword: failed to get Email", err)
28+
return
29+
}
30+
31+
resetToken, err := us.jwt.GenerateAccessToken(ctx, &user, 30)
32+
if err != nil {
33+
logrus.Error("us.ForgotPassword: failed to generate token", err)
34+
return
35+
}
36+
37+
jwtData, err := us.jwt.VerifyToken(*resetToken)
38+
39+
if err != nil {
40+
logrus.Error("us.ForgotPassword: failed to verify token", err)
41+
return
42+
}
43+
44+
if err = us.userRepo.ForgotPassword(ctx, *resetToken, jwtData.ExpiresAt.Time, user); err != nil {
45+
logrus.Error("us.ForgotPassword: failed to save token", err)
46+
return
47+
}
48+
49+
// link to reset password
50+
link := us.cfg.BASE_URL_FE + "/forgot_password?token=" + *resetToken
51+
52+
htmlTmpl, err := os.ReadFile("./assets/reset_button_on_email.html")
53+
if err != nil {
54+
logrus.Error("us.ForgotPassword: failed to read template file", err)
55+
return
56+
}
57+
58+
tmpl, err := template.New("reset_email").Parse(string(htmlTmpl))
59+
if err != nil {
60+
logrus.Error("us.ForgotPassword: failed to parse template", err)
61+
return
62+
}
63+
64+
var bodyBuffer bytes.Buffer
65+
if err = tmpl.Execute(&bodyBuffer, link); err != nil {
66+
logrus.Error("us.ForgotPassword: failed to execute template", err)
67+
return
68+
}
69+
70+
subject := "Reset Password"
71+
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
72+
message := []byte(fmt.Sprintf("To: %s\r\n"+
73+
"Subject: %s\r\n"+
74+
"%s\r\n"+
75+
"%s\r\n", emailForgot.Email, subject, mime, bodyBuffer.String()))
76+
77+
auth := smtp.PlainAuth("", us.cfg.SMTP_EMAIL, us.cfg.SMTP_PASSWORD, us.cfg.SMTP_HOST)
78+
79+
host := fmt.Sprintf("%s:%s", us.cfg.SMTP_HOST, us.cfg.SMTP_PORT)
80+
if err := smtp.SendMail(host, auth, us.cfg.SMTP_EMAIL, []string{emailForgot.Email}, message); err != nil {
81+
return err
82+
}
83+
84+
return nil
85+
}

app/users/usecase/login_users.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (us *usecase) Login(ctx context.Context, userReq domain.Login) (user domain
2727
return
2828
}
2929

30-
signToken, err := us.jwt.GenerateAccessToken(ctx, &user)
30+
signToken, err := us.jwt.GenerateAccessToken(ctx, &user, 60)
3131
if err != nil {
3232
logrus.Error("us.Login: failed to login. ", err)
3333
return

app/users/usecase/reset_password.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package usecase
2+
3+
import (
4+
"context"
5+
6+
"github.com/hammer-code/lms-be/domain"
7+
"github.com/sirupsen/logrus"
8+
"golang.org/x/crypto/bcrypt"
9+
)
10+
11+
func (us *usecase) ResetPassword(ctx context.Context, reqBodyInstance domain.ForgotPassword) error {
12+
jwtData, err := us.jwt.VerifyToken(reqBodyInstance.Token)
13+
if err != nil {
14+
logrus.Error("us.ResetPassword: failed to verify token", err)
15+
return err
16+
}
17+
18+
hashPassword, err := bcrypt.GenerateFromPassword([]byte(reqBodyInstance.Password), bcrypt.DefaultCost)
19+
if err != nil {
20+
logrus.Error("us.ResetPassword: failed to hash password", err)
21+
return err
22+
}
23+
24+
if err := us.userRepo.ResetPassword(ctx, jwtData.Email, string(hashPassword), reqBodyInstance.Token); err != nil {
25+
logrus.Error("us.ResetPassword: failed to reset password", err)
26+
return err
27+
}
28+
29+
return nil
30+
31+
}

app/users/usecase/usecase.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package usecase
22

33
import (
4+
"github.com/hammer-code/lms-be/config"
45
"github.com/hammer-code/lms-be/domain"
56
"github.com/hammer-code/lms-be/pkg/db"
67
"github.com/hammer-code/lms-be/pkg/jwt"
@@ -10,15 +11,17 @@ type usecase struct {
1011
userRepo domain.UserRepository
1112
dbTX db.DatabaseTransaction
1213
jwt jwt.JWT
14+
cfg config.Config
1315
}
1416

1517
var (
1618
usec *usecase
1719
)
1820

19-
func NewUsecase(userRepo domain.UserRepository, dbTX db.DatabaseTransaction, jwt jwt.JWT) domain.UserUsecase {
21+
func NewUsecase(cfg config.Config, userRepo domain.UserRepository, dbTX db.DatabaseTransaction, jwt jwt.JWT) domain.UserUsecase {
2022
if usec == nil {
2123
usec = &usecase{
24+
cfg: cfg,
2225
userRepo: userRepo,
2326
dbTX: dbTX,
2427
jwt: jwt,

app/users/users.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
users_handler "github.com/hammer-code/lms-be/app/users/delivery/http"
55
users_repo "github.com/hammer-code/lms-be/app/users/repository"
66
users_usecase "github.com/hammer-code/lms-be/app/users/usecase"
7+
"github.com/hammer-code/lms-be/config"
78
"github.com/hammer-code/lms-be/domain"
89
"github.com/hammer-code/lms-be/pkg/db"
910
"github.com/hammer-code/lms-be/pkg/jwt"
@@ -13,8 +14,8 @@ func InitRepository(db db.DatabaseTransaction) domain.UserRepository {
1314
return users_repo.NewRepository(db)
1415
}
1516

16-
func InitUsecase(repository domain.UserRepository, dbTX db.DatabaseTransaction, jwt jwt.JWT) domain.UserUsecase {
17-
return users_usecase.NewUsecase(repository, dbTX, jwt)
17+
func InitUsecase(cfg config.Config, repository domain.UserRepository, dbTX db.DatabaseTransaction, jwt jwt.JWT) domain.UserUsecase {
18+
return users_usecase.NewUsecase(cfg, repository, dbTX, jwt)
1819
}
1920

2021
func InitHandler(usecase domain.UserUsecase) domain.UserHandler {

0 commit comments

Comments
 (0)