2024-07-01 02:41:41 +01:00
|
|
|
package user
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"html"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
auth "donetick.com/core/internal/authorization"
|
|
|
|
cModel "donetick.com/core/internal/circle/model"
|
|
|
|
cRepo "donetick.com/core/internal/circle/repo"
|
|
|
|
"donetick.com/core/internal/email"
|
|
|
|
uModel "donetick.com/core/internal/user/model"
|
|
|
|
uRepo "donetick.com/core/internal/user/repo"
|
|
|
|
"donetick.com/core/internal/utils"
|
|
|
|
"donetick.com/core/logging"
|
|
|
|
jwt "github.com/appleboy/gin-jwt/v2"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
limiter "github.com/ulule/limiter/v3"
|
|
|
|
"google.golang.org/api/googleapi"
|
|
|
|
"google.golang.org/api/oauth2/v1"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Handler struct {
|
|
|
|
userRepo *uRepo.UserRepository
|
|
|
|
circleRepo *cRepo.CircleRepository
|
|
|
|
jwtAuth *jwt.GinJWTMiddleware
|
|
|
|
email *email.EmailSender
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewHandler(ur *uRepo.UserRepository, cr *cRepo.CircleRepository, jwtAuth *jwt.GinJWTMiddleware, email *email.EmailSender) *Handler {
|
|
|
|
return &Handler{
|
|
|
|
userRepo: ur,
|
|
|
|
circleRepo: cr,
|
|
|
|
jwtAuth: jwtAuth,
|
|
|
|
email: email,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) GetAllUsers() gin.HandlerFunc {
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
currentUser, ok := auth.CurrentUser(c)
|
|
|
|
if !ok {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error getting current user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
users, err := h.userRepo.GetAllUsers(c, currentUser.CircleID)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error getting users",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(200, gin.H{
|
|
|
|
"res": users,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) signUp(c *gin.Context) {
|
|
|
|
|
|
|
|
type SignUpReq struct {
|
|
|
|
Username string `json:"username" binding:"required,min=4,max=20"`
|
|
|
|
Password string `json:"password" binding:"required,min=8,max=45"`
|
2024-07-06 07:36:14 +01:00
|
|
|
Email string `json:"email" binding:"required,email"`
|
2024-07-01 02:41:41 +01:00
|
|
|
DisplayName string `json:"displayName"`
|
|
|
|
}
|
|
|
|
var signupReq SignUpReq
|
|
|
|
if err := c.BindJSON(&signupReq); err != nil {
|
|
|
|
c.JSON(400, gin.H{
|
|
|
|
"error": "Invalid request",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if signupReq.DisplayName == "" {
|
|
|
|
signupReq.DisplayName = signupReq.Username
|
|
|
|
}
|
|
|
|
password, err := auth.EncodePassword(signupReq.Password)
|
|
|
|
signupReq.Username = html.EscapeString(signupReq.Username)
|
|
|
|
signupReq.DisplayName = html.EscapeString(signupReq.DisplayName)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error encoding password",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var insertedUser *uModel.User
|
|
|
|
if insertedUser, err = h.userRepo.CreateUser(c, &uModel.User{
|
|
|
|
Username: signupReq.Username,
|
|
|
|
Password: password,
|
|
|
|
DisplayName: signupReq.DisplayName,
|
2024-07-06 07:36:14 +01:00
|
|
|
Email: signupReq.Email,
|
2024-07-01 02:41:41 +01:00
|
|
|
CreatedAt: time.Now(),
|
|
|
|
UpdatedAt: time.Now(),
|
|
|
|
}); err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error creating user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// var userCircle *circle.Circle
|
|
|
|
// var userRole string
|
|
|
|
userCircle, err := h.circleRepo.CreateCircle(c, &cModel.Circle{
|
|
|
|
Name: signupReq.DisplayName + "'s circle",
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
UpdatedAt: time.Now(),
|
|
|
|
InviteCode: utils.GenerateInviteCode(c),
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error creating circle",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := h.circleRepo.AddUserToCircle(c, &cModel.UserCircle{
|
|
|
|
UserID: insertedUser.ID,
|
|
|
|
CircleID: userCircle.ID,
|
|
|
|
Role: "admin",
|
|
|
|
IsActive: true,
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
UpdatedAt: time.Now(),
|
|
|
|
}); err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error adding user to circle",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
insertedUser.CircleID = userCircle.ID
|
|
|
|
if err := h.userRepo.UpdateUser(c, insertedUser); err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error updating user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(201, gin.H{})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) GetUserProfile(c *gin.Context) {
|
|
|
|
user, ok := auth.CurrentUser(c)
|
|
|
|
if !ok {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error getting user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c.JSON(200, gin.H{
|
|
|
|
"res": user,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) thirdPartyAuthCallback(c *gin.Context) {
|
|
|
|
|
|
|
|
// read :provider from path param, if param is google check the token with google if it's valid and fetch the user details:
|
|
|
|
logger := logging.FromContext(c)
|
|
|
|
provider := c.Param("provider")
|
|
|
|
logger.Infow("account.handler.thirdPartyAuthCallback", "provider", provider)
|
|
|
|
|
|
|
|
if provider == "google" {
|
|
|
|
c.Set("auth_provider", "3rdPartyAuth")
|
|
|
|
type OAuthRequest struct {
|
|
|
|
Token string `json:"token" binding:"required"`
|
|
|
|
Provider string `json:"provider" binding:"required"`
|
|
|
|
}
|
|
|
|
var body OAuthRequest
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
logger.Errorw("account.handler.thirdPartyAuthCallback failed to bind", "err", err)
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "Invalid request",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// logger.Infow("account.handler.thirdPartyAuthCallback", "token", token)
|
|
|
|
service, err := oauth2.New(http.DefaultClient)
|
|
|
|
|
|
|
|
// tokenInfo, err := service.Tokeninfo().AccessToken(token).Do()
|
|
|
|
userinfo, err := service.Userinfo.Get().Do(googleapi.QueryParameter("access_token", body.Token))
|
|
|
|
logger.Infow("account.handler.thirdPartyAuthCallback", "tokenInfo", userinfo)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorw("account.handler.thirdPartyAuthCallback failed to get token info", "err", err)
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "Invalid token",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
acc, err := h.userRepo.FindByEmail(c, userinfo.Email)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
// create a random password for the user using crypto/rand:
|
|
|
|
password := auth.GenerateRandomPassword(12)
|
|
|
|
encodedPassword, err := auth.EncodePassword(password)
|
|
|
|
acc = &uModel.User{
|
|
|
|
Username: userinfo.Id,
|
|
|
|
Email: userinfo.Email,
|
|
|
|
Image: userinfo.Picture,
|
|
|
|
Password: encodedPassword,
|
|
|
|
DisplayName: userinfo.GivenName,
|
|
|
|
Provider: 2,
|
|
|
|
}
|
|
|
|
createdUser, err := h.userRepo.CreateUser(c, acc)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
"error": "Unable to create user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
// Create Circle for the user:
|
|
|
|
userCircle, err := h.circleRepo.CreateCircle(c, &cModel.Circle{
|
|
|
|
Name: userinfo.GivenName + "'s circle",
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
UpdatedAt: time.Now(),
|
|
|
|
InviteCode: utils.GenerateInviteCode(c),
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error creating circle",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := h.circleRepo.AddUserToCircle(c, &cModel.UserCircle{
|
|
|
|
UserID: createdUser.ID,
|
|
|
|
CircleID: userCircle.ID,
|
|
|
|
Role: "admin",
|
|
|
|
IsActive: true,
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
UpdatedAt: time.Now(),
|
|
|
|
}); err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error adding user to circle",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
createdUser.CircleID = userCircle.ID
|
|
|
|
if err := h.userRepo.UpdateUser(c, createdUser); err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error updating user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// use auth to generate a token for the user:
|
|
|
|
c.Set("user_account", acc)
|
|
|
|
h.jwtAuth.Authenticator(c)
|
|
|
|
tokenString, expire, err := h.jwtAuth.TokenGenerator(acc)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorw("Unable to Generate a Token")
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
"error": "Unable to Generate a Token",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"token": tokenString, "expire": expire})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) resetPassword(c *gin.Context) {
|
|
|
|
log := logging.FromContext(c)
|
|
|
|
type ResetPasswordReq struct {
|
|
|
|
Email string `json:"email" binding:"required,email"`
|
|
|
|
}
|
|
|
|
var req ResetPasswordReq
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "Invalid request",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
user, err := h.userRepo.FindByEmail(c, req.Email)
|
|
|
|
if err != nil {
|
2024-07-07 07:59:04 +01:00
|
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
|
|
log.Error("account.handler.resetPassword failed to find user")
|
2024-07-01 02:41:41 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if user.Provider != 0 {
|
|
|
|
// user create account thought login with Gmail. they can reset the password they just need to login with google again
|
|
|
|
c.JSON(
|
|
|
|
http.StatusForbidden,
|
|
|
|
gin.H{
|
|
|
|
"error": "User account created with google login. Please login with google",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// generate a random password:
|
|
|
|
token, err := auth.GenerateEmailResetToken(c)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
"error": "Unable to generate token",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = h.userRepo.SetPasswordResetToken(c, req.Email, token)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
"error": "Unable to generate password",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// send an email to the user with the new password:
|
|
|
|
err = h.email.SendResetPasswordEmail(c, req.Email, token)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorw("account.handler.resetPassword failed to send email", "err", err)
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
"error": "Unable to send email",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// send an email to the user with the new password:
|
|
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) updateUserPassword(c *gin.Context) {
|
|
|
|
logger := logging.FromContext(c)
|
|
|
|
// read the code from query param:
|
|
|
|
code := c.Query("c")
|
|
|
|
email, code, err := email.DecodeEmailAndCode(code)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorw("account.handler.verify failed to decode email and code", "err", err)
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "Invalid code",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
// read password from body:
|
|
|
|
type RequestBody struct {
|
|
|
|
Password string `json:"password" binding:"required,min=8,max=32"`
|
|
|
|
}
|
|
|
|
var body RequestBody
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
logger.Errorw("user.handler.resetAccountPassword failed to bind", "err", err)
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "Invalid request",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
password, err := auth.EncodePassword(body.Password)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
"error": "Unable to process password",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = h.userRepo.UpdatePasswordByToken(c.Request.Context(), email, code, password)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorw("account.handler.resetAccountPassword failed to reset password", "err", err)
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
"error": "Unable to reset password",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) UpdateUserDetails(c *gin.Context) {
|
|
|
|
type UpdateUserReq struct {
|
|
|
|
DisplayName *string `json:"displayName" binding:"omitempty"`
|
|
|
|
ChatID *int64 `json:"chatID" binding:"omitempty"`
|
|
|
|
Image *string `json:"image" binding:"omitempty"`
|
|
|
|
}
|
|
|
|
user, ok := auth.CurrentUser(c)
|
|
|
|
if !ok {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error getting user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var req UpdateUserReq
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
c.JSON(400, gin.H{
|
|
|
|
"error": "Invalid request",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// update non-nil fields:
|
|
|
|
if req.DisplayName != nil {
|
|
|
|
user.DisplayName = *req.DisplayName
|
|
|
|
}
|
|
|
|
if req.ChatID != nil {
|
|
|
|
user.ChatID = *req.ChatID
|
|
|
|
}
|
|
|
|
if req.Image != nil {
|
|
|
|
user.Image = *req.Image
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := h.userRepo.UpdateUser(c, user); err != nil {
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
"error": "Error updating user",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c.JSON(200, user)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) CreateLongLivedToken(c *gin.Context) {
|
|
|
|
currentUser, ok := auth.CurrentUser(c)
|
|
|
|
if !ok {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get current user"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
type TokenRequest struct {
|
|
|
|
Name string `json:"name" binding:"required"`
|
|
|
|
}
|
|
|
|
var req TokenRequest
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Step 1: Generate a secure random number
|
|
|
|
randomBytes := make([]byte, 16) // 128 bits are enough for strong randomness
|
|
|
|
_, err := rand.Read(randomBytes)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate random part of the token"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
timestamp := time.Now().Unix()
|
|
|
|
hashInput := fmt.Sprintf("%s:%d:%x", currentUser.Username, timestamp, randomBytes)
|
|
|
|
hash := sha256.Sum256([]byte(hashInput))
|
|
|
|
|
|
|
|
token := hex.EncodeToString(hash[:])
|
|
|
|
|
|
|
|
tokenModel, err := h.userRepo.StoreAPIToken(c, currentUser.ID, req.Name, token)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store the token"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"res": tokenModel})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) GetAllUserToken(c *gin.Context) {
|
|
|
|
currentUser, ok := auth.CurrentUser(c)
|
|
|
|
if !ok {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get current user"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens, err := h.userRepo.GetAllUserTokens(c, currentUser.ID)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user tokens"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"res": tokens})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) DeleteUserToken(c *gin.Context) {
|
|
|
|
currentUser, ok := auth.CurrentUser(c)
|
|
|
|
if !ok {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get current user"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenID := c.Param("id")
|
|
|
|
|
|
|
|
err := h.userRepo.DeleteAPIToken(c, currentUser.ID, tokenID)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete the token"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
|
|
}
|
|
|
|
|
|
|
|
func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware, limiter *limiter.Limiter) {
|
|
|
|
|
|
|
|
userRoutes := router.Group("users")
|
|
|
|
userRoutes.Use(auth.MiddlewareFunc(), utils.RateLimitMiddleware(limiter))
|
|
|
|
{
|
|
|
|
userRoutes.GET("/", h.GetAllUsers())
|
|
|
|
userRoutes.GET("/profile", h.GetUserProfile)
|
|
|
|
userRoutes.PUT("", h.UpdateUserDetails)
|
|
|
|
userRoutes.POST("/tokens", h.CreateLongLivedToken)
|
|
|
|
userRoutes.GET("/tokens", h.GetAllUserToken)
|
|
|
|
userRoutes.DELETE("/tokens/:id", h.DeleteUserToken)
|
|
|
|
}
|
|
|
|
|
|
|
|
authRoutes := router.Group("auth")
|
|
|
|
authRoutes.Use(utils.RateLimitMiddleware(limiter))
|
|
|
|
{
|
|
|
|
authRoutes.POST("/", h.signUp)
|
|
|
|
authRoutes.POST("login", auth.LoginHandler)
|
|
|
|
authRoutes.GET("refresh", auth.RefreshHandler)
|
|
|
|
authRoutes.POST("/:provider/callback", h.thirdPartyAuthCallback)
|
|
|
|
authRoutes.POST("reset", h.resetPassword)
|
|
|
|
authRoutes.POST("password", h.updateUserPassword)
|
|
|
|
}
|
|
|
|
}
|