From c6b4cba8b49d88a20c49aa7c53b42a67befabe64 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:47:46 +0530 Subject: [PATCH] [server] Auto recovery post deletion 1/x --- server/cmd/museum/main.go | 1 + server/pkg/api/user.go | 14 ++++++++++++ server/pkg/controller/user/user.go | 34 ++++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index f3a92cf210..940f34459e 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -519,6 +519,7 @@ func main() { privateAPI.DELETE("/users/session", userHandler.TerminateSession) privateAPI.GET("/users/delete-challenge", userHandler.GetDeleteChallenge) privateAPI.DELETE("/users/delete", userHandler.DeleteUser) + publicAPI.GET("/users/recover-account", userHandler.RecoveryAccount) accountsJwtAuthAPI := server.Group("/") accountsJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.ACCOUNTS.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) diff --git a/server/pkg/api/user.go b/server/pkg/api/user.go index c62613ccb7..f1a11a6b82 100644 --- a/server/pkg/api/user.go +++ b/server/pkg/api/user.go @@ -540,6 +540,20 @@ func (h *UserHandler) DeleteUser(c *gin.Context) { c.JSON(http.StatusOK, response) } +func (h *UserHandler) RecoveryAccount(c *gin.Context) { + token := c.Query("token") + if token == "" { + handler.Error(c, stacktrace.Propagate(ente.NewBadRequestWithMessage("token missing"), "token is required")) + return + } + response, err := h.UserController.SelfDeleteAccount(c, request) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, response) +} + // GetSRPAttributes returns the SRP attributes for a user func (h *UserHandler) GetSRPAttributes(c *gin.Context) { var request ente.GetSRPAttributesRequest diff --git a/server/pkg/controller/user/user.go b/server/pkg/controller/user/user.go index c72d50546b..4917b29a0b 100644 --- a/server/pkg/controller/user/user.go +++ b/server/pkg/controller/user/user.go @@ -1,10 +1,13 @@ package user import ( + "database/sql" "errors" "fmt" + enteJWT "github.com/ente-io/museum/ente/jwt" "github.com/ente-io/museum/pkg/controller/collections" "github.com/ente-io/museum/pkg/repo/two_factor_recovery" + "github.com/ente-io/museum/pkg/utils/time" "strings" cache2 "github.com/ente-io/museum/ente/cache" @@ -271,7 +274,7 @@ func (c *UserController) HandleAccountDeletion(ctx *gin.Context, userID int64, l return nil, stacktrace.Propagate(err, "") } - go c.NotifyAccountDeletion(email, isSubscriptionCancelled) + go c.NotifyAccountDeletion(userID, email, isSubscriptionCancelled) return &ente.DeleteAccountResponse{ IsSubscriptionCancelled: isSubscriptionCancelled, @@ -280,23 +283,43 @@ func (c *UserController) HandleAccountDeletion(ctx *gin.Context, userID int64, l } -func (c *UserController) NotifyAccountDeletion(userEmail string, isSubscriptionCancelled bool) { +func (c *UserController) NotifyAccountDeletion(userID int64, userEmail string, isSubscriptionCancelled bool) { template := AccountDeletedEmailTemplate if !isSubscriptionCancelled { template = AccountDeletedWithActiveSubscriptionEmailTemplate } + recoverToken, err2 := c.GetJWTTokenForClaim(&enteJWT.WebCommonJWTClaim{ + UserID: userID, + ExpiryTime: time.NDaysFromNow(7), + ClaimScope: enteJWT.RestoreAccount.Ptr(), + Email: userEmail, + }) + if err2 != nil { + logrus.WithError(err2).Error("failed to generate recover token") + return + } + + templateData := make(map[string]interface{}) + templateData["RecoverLink"] = fmt.Sprintf("%s/user/recover-account?token=%s", "https://api.ente.io", recoverToken) err := email.SendTemplatedEmail([]string{userEmail}, "ente", "team@ente.io", - AccountDeletedEmailSubject, template, nil, nil) + AccountDeletedEmailSubject, template, templateData, nil) if err != nil { logrus.WithError(err).Errorf("Failed to send the account deletion email to %s", userEmail) } } func (c *UserController) HandleAccountRecovery(ctx *gin.Context, req ente.RecoverAccountRequest) error { + logger := logrus.WithFields(logrus.Fields{ + "req_id": ctx.GetString("req_id"), + "req_ctx": "account_recovery", + "email": req.EmailID, + "userID": req.UserID, + }) + logger.Info("recover account request") _, err := c.UserRepo.Get(req.UserID) if err == nil { return stacktrace.Propagate(ente.NewBadRequestError(&ente.ApiErrorParams{ - Message: "User ID is linked to undeleted account", + Message: "User ID is linked to an active account account", }), "") } if !errors.Is(err, ente.ErrUserDeleted) { @@ -304,6 +327,9 @@ func (c *UserController) HandleAccountRecovery(ctx *gin.Context, req ente.Recove } // check if the user keyAttributes are still available if _, keyErr := c.UserRepo.GetKeyAttributes(req.UserID); keyErr != nil { + if errors.Is(keyErr, sql.ErrNoRows) { + return stacktrace.Propagate(ente.NewBadRequestWithMessage("account can not be recovered now"), "") + } return stacktrace.Propagate(keyErr, "keyAttributes missing? Account can not be recovered") } email := strings.ToLower(req.EmailID)