aboutsummaryrefslogtreecommitdiffstats
path: root/internal/user/handler.go
diff options
context:
space:
mode:
authorLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-06-30 21:41:41 -0400
committerLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-06-30 21:41:41 -0400
commitc13dd9addbf89f716e4ef5cfdf1d673139ffcb68 (patch)
treebc09646ce1d6d3a402abb4694e19da51b57204f6 /internal/user/handler.go
downloaddonetick-c13dd9addbf89f716e4ef5cfdf1d673139ffcb68.tar.gz
donetick-c13dd9addbf89f716e4ef5cfdf1d673139ffcb68.tar.bz2
donetick-c13dd9addbf89f716e4ef5cfdf1d673139ffcb68.zip
Move to Donetick Org, first commit
Diffstat (limited to 'internal/user/handler.go')
-rw-r--r--internal/user/handler.go511
1 files changed, 511 insertions, 0 deletions
diff --git a/internal/user/handler.go b/internal/user/handler.go
new file mode 100644
index 0000000..0eee6f2
--- /dev/null
+++ b/internal/user/handler.go
@@ -0,0 +1,511 @@
+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"`
+ 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,
+ 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 {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "User not found",
+ })
+ 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)
+ }
+}