From 73a855084443d648bcd22ea43c04b43d88b103c3 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:31:51 +0530 Subject: [PATCH] [server] Add remaining mails for legacy --- server/cmd/museum/main.go | 5 +- .../legacy/recovery_reminder.html | 2 +- server/pkg/controller/emergency/controller.go | 11 ++- server/pkg/controller/emergency/email.go | 10 ++- server/pkg/controller/emergency/recovery.go | 81 ++++++++++++++++++- .../controller/emergency/recovery_contact.go | 8 +- server/pkg/repo/emergency/recovery.go | 29 +++++++ 7 files changed, 132 insertions(+), 14 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 5503879099..bb99f90c73 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -474,6 +474,7 @@ func main() { UserRepo: userRepo, UserCtrl: userController, PasskeyController: passkeyCtrl, + LockCtrl: lockController, } userHandler := &api.UserHandler{ UserController: userController, @@ -766,7 +767,7 @@ func main() { setupAndStartBackgroundJobs(objectCleanupController, replicationController3, fileDataCtrl) setupAndStartCrons( userAuthRepo, publicCollectionRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl, - trashController, pushController, objectController, dataCleanupController, storageBonusCtrl, + trashController, pushController, objectController, dataCleanupController, storageBonusCtrl, emergencyCtrl, embeddingController, healthCheckHandler, kexCtrl, castDb) // Create a new collector, the name will be used as a label on the metrics @@ -901,6 +902,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR objectController *controller.ObjectController, dataCleanupCtrl *dataCleanupCtrl.DeleteUserCleanupController, storageBonusCtrl *storagebonus.Controller, + emergencyCtrl *emergency.Controller, embeddingCtrl *embeddingCtrl.Controller, healthCheckHandler *api.HealthCheckHandler, kexCtrl *kexCtrl.Controller, @@ -994,6 +996,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR }) scheduleAndRun(c, "@every 60m", func() { + emergencyCtrl.SendRecoveryReminder() kexCtrl.DeleteOldKeys() }) diff --git a/server/mail-templates/legacy/recovery_reminder.html b/server/mail-templates/legacy/recovery_reminder.html index b9196115fe..5ffd23c2fd 100644 --- a/server/mail-templates/legacy/recovery_reminder.html +++ b/server/mail-templates/legacy/recovery_reminder.html @@ -1,7 +1,7 @@ {{define "content"}}
Hello,
-{{.TrustedContact}} has initiated recovery on your account. After 2 days, they will be able to change the password and access your account.
+{{.TrustedContact}} has initiated recovery on your account. After {{.DaysLeft}} days, they will be able to change the password and access your account.
If you want to block the recovery, please navigate to Settings > Account > Legacy in the Ente Photos app.
diff --git a/server/pkg/controller/emergency/controller.go b/server/pkg/controller/emergency/controller.go index df4a0b399d..22f4f9902c 100644 --- a/server/pkg/controller/emergency/controller.go +++ b/server/pkg/controller/emergency/controller.go @@ -3,6 +3,7 @@ package emergency import ( "fmt" "github.com/ente-io/museum/pkg/controller" + "github.com/ente-io/museum/pkg/controller/lock" "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/controller/user" @@ -14,10 +15,12 @@ import ( ) type Controller struct { - Repo *emergency.Repository - UserRepo *repo.UserRepository - UserCtrl *user.UserController - PasskeyController *controller.PasskeyController + Repo *emergency.Repository + UserRepo *repo.UserRepository + UserCtrl *user.UserController + PasskeyController *controller.PasskeyController + LockCtrl *lock.LockController + isReminderCronRunning bool } func (c *Controller) UpdateContact(ctx *gin.Context, diff --git a/server/pkg/controller/emergency/email.go b/server/pkg/controller/emergency/email.go index ad374197ce..0e734422d9 100644 --- a/server/pkg/controller/emergency/email.go +++ b/server/pkg/controller/emergency/email.go @@ -3,6 +3,7 @@ package emergency import ( "context" "fmt" + "github.com/ente-io/museum/ente" emailUtil "github.com/ente-io/museum/pkg/utils/email" "github.com/ente-io/stacktrace" @@ -131,11 +132,14 @@ func (c *Controller) sendContactNotification(ctx context.Context, legacyUserID i return nil } -func (c *Controller) createRecoveryEmailData(legacyUser, trustedUser ente.User, newStatus ente.RecoveryStatus) ([]emailData, error) { +func (c *Controller) createRecoveryEmailData(legacyUser, trustedUser ente.User, newStatus ente.RecoveryStatus, daysLeft *int64) ([]emailData, error) { templateData := map[string]interface{}{ "LegacyContact": legacyUser.Email, "TrustedContact": trustedUser.Email, } + if daysLeft != nil { + templateData["DaysLeft"] = *daysLeft + } var emailDatas []emailData @@ -210,7 +214,7 @@ func (c *Controller) createRecoveryEmailData(legacyUser, trustedUser ente.User, return emailDatas, nil } -func (c *Controller) sendRecoveryNotification(ctx context.Context, legacyUserID int64, trustedUserID int64, newStatus ente.RecoveryStatus) error { +func (c *Controller) sendRecoveryNotification(ctx context.Context, legacyUserID int64, trustedUserID int64, newStatus ente.RecoveryStatus, daysLeft *int64) error { legacyUser, err := c.UserRepo.Get(legacyUserID) if err != nil { return stacktrace.Propagate(err, "") @@ -220,7 +224,7 @@ func (c *Controller) sendRecoveryNotification(ctx context.Context, legacyUserID return stacktrace.Propagate(err, "") } - emailDatas, err := c.createRecoveryEmailData(legacyUser, trustedUser, newStatus) + emailDatas, err := c.createRecoveryEmailData(legacyUser, trustedUser, newStatus, daysLeft) if err != nil { return stacktrace.Propagate(err, "") } diff --git a/server/pkg/controller/emergency/recovery.go b/server/pkg/controller/emergency/recovery.go index be31254e6c..4f66fe0b5b 100644 --- a/server/pkg/controller/emergency/recovery.go +++ b/server/pkg/controller/emergency/recovery.go @@ -1,14 +1,21 @@ package emergency import ( + "context" + "fmt" "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/repo/emergency" + "github.com/ente-io/museum/pkg/utils/time" "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" "github.com/google/uuid" log "github.com/sirupsen/logrus" ) +const ( + _recoveryReminderLock = "recoveryReminderLock" +) + func (c *Controller) GetRecoveryInfo(ctx *gin.Context, userID int64, sessionID uuid.UUID, @@ -67,7 +74,7 @@ func (c *Controller) ChangePassword(ctx *gin.Context, userID int64, request ente log.WithField("userID", userID).WithField("req", request). Warn("no row updated while rejecting recovery") } else { - go c.sendRecoveryNotification(ctx, contact.UserID, contact.EmergencyContactID, ente.RecoveryStatusRecovered) + go c.sendRecoveryNotification(ctx, contact.UserID, contact.EmergencyContactID, ente.RecoveryStatusRecovered, nil) } return resp, nil @@ -95,3 +102,75 @@ func (c *Controller) checkRecoveryAndGetContact(ctx *gin.Context, } return contact, nil } + +func (c *Controller) SendRecoveryReminder() { + if c.isReminderCronRunning { + return + } + c.isReminderCronRunning = true + defer func() { + c.isReminderCronRunning = false + }() + lockStatus := c.LockCtrl.TryLock(_recoveryReminderLock, time.MicrosecondsAfterHours(1)) + if !lockStatus { + log.Error("Could not acquire lock to send storage limit exceeded mails") + return + } + defer c.LockCtrl.ReleaseLock(_recoveryReminderLock) + + rows, err := c.Repo.GetActiveRecoveryForNotification() + if err != nil { + log.WithError(err).Error("failed to get recovery rows") + return + } + + if len(*rows) == 0 { + return + } + log.Info(fmt.Sprintf("Found %d recovery rows", len(*rows))) + microsecondsInDay := 1000 * 1000 * 24 * 60 * 60 + for _, row := range *rows { + logger := log.WithFields(log.Fields{ + "userID": row.UserID, + "contactID": row.EmergencyContactID, + "status": row.Status, + "waitTill": row.WaitTill, + "nextReminderAt": row.NextReminderAt, + "sessionID": row.ID, + }) + + daysLeft := (row.WaitTill - row.NextReminderAt) / int64(microsecondsInDay) + logger.Infof("Days left: %d", daysLeft) + if row.WaitTill < time.Microseconds() && row.Status == ente.RecoveryStatusWaiting { + _, updateErr := c.Repo.UpdateRecoveryStatusForID(context.Background(), row.ID, ente.RecoveryStatusReady) + if updateErr != nil { + logger.WithError(updateErr).Error("failed to update recovery status") + continue + } + + go c.sendRecoveryNotification(context.Background(), row.UserID, row.EmergencyContactID, ente.RecoveryStatusReady, nil) + } else if daysLeft >= 2 && row.Status == ente.RecoveryStatusWaiting { + if daysLeft > 9 { + // set another reminder after 7 days + newNextReminderAt := row.NextReminderAt + int64(microsecondsInDay*7) + if err := c.Repo.UpdateNextReminder(context.Background(), row.ID, newNextReminderAt); err != nil { + logger.WithError(err).Error("failed to update next reminder") + continue + } + } else if daysLeft > 2 { + // send a reminder two days before the waitTill date + newNextReminderAt := row.WaitTill - int64(microsecondsInDay*2) + if err := c.Repo.UpdateNextReminder(context.Background(), row.ID, newNextReminderAt); err != nil { + logger.WithError(err).Error("failed to update next reminder") + continue + } + } + + if row.Status == ente.RecoveryStatusWaiting { + go c.sendRecoveryNotification(context.Background(), row.UserID, row.EmergencyContactID, ente.RecoveryStatusWaiting, &daysLeft) + } else { + logger.Warnf("No need to send email with status %v", row.Status) + } + } + } +} diff --git a/server/pkg/controller/emergency/recovery_contact.go b/server/pkg/controller/emergency/recovery_contact.go index c2d763d061..0fe2490306 100644 --- a/server/pkg/controller/emergency/recovery_contact.go +++ b/server/pkg/controller/emergency/recovery_contact.go @@ -27,7 +27,7 @@ func (c *Controller) StartRecovery(ctx *gin.Context, log.WithField("userID", actorUserID).WithField("req", req). Warn("No need to send email") } else { - go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusInitiated) + go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusInitiated, nil) } if err != nil { return stacktrace.Propagate(err, "") @@ -49,7 +49,7 @@ func (c *Controller) RejectRecovery(ctx *gin.Context, log.WithField("userID", userID).WithField("req", req). Warn("no row updated while rejecting recovery") } else { - go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusRejected) + go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusRejected, nil) } if err != nil { return stacktrace.Propagate(err, "") @@ -71,7 +71,7 @@ func (c *Controller) ApproveRecovery(ctx *gin.Context, log.WithField("userID", userID).WithField("req", req). Warn("no row updated while rejecting recovery") } else { - go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusReady) + go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusReady, nil) } if err != nil { return stacktrace.Propagate(err, "") @@ -96,7 +96,7 @@ func (c *Controller) StopRecovery(ctx *gin.Context, log.WithField("userID", userID).WithField("req", req). Warn("no row updated while stopping recovery") } else { - go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusStopped) + go c.sendRecoveryNotification(ctx, req.UserID, req.EmergencyContactID, ente.RecoveryStatusStopped, nil) } return stacktrace.Propagate(err, "") } diff --git a/server/pkg/repo/emergency/recovery.go b/server/pkg/repo/emergency/recovery.go index 23b015783d..965bf613b6 100644 --- a/server/pkg/repo/emergency/recovery.go +++ b/server/pkg/repo/emergency/recovery.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/sirupsen/logrus" "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/utils/time" @@ -91,6 +92,33 @@ FROM emergency_recovery WHERE user_id=$1 and emergency_contact_id=$2 AND status return sessions, nil } +func (r *Repository) GetActiveRecoveryForNotification() (*[]RecoverRow, error) { + rows, err := r.DB.Query(` +SELECT id, user_id, emergency_contact_id, status, wait_till, next_reminder_at, created_at +FROM emergency_recovery WHERE (status = $1) and next_reminder_at < now_utc_micro_seconds()`, ente.RecoveryStatusWaiting) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + defer rows.Close() + var sessions []RecoverRow + for rows.Next() { + var row RecoverRow + if err := rows.Scan(&row.ID, &row.UserID, &row.EmergencyContactID, &row.Status, &row.WaitTill, &row.NextReminderAt, &row.CreatedAt); err != nil { + return nil, stacktrace.Propagate(err, "") + } + sessions = append(sessions, row) + } + return &sessions, nil +} + +func (r *Repository) UpdateNextReminder(ctx context.Context, sessionID uuid.UUID, nextReminder int64) error { + _, err := r.DB.ExecContext(ctx, `UPDATE emergency_recovery SET next_reminder_at=$1 WHERE id=$2`, nextReminder, sessionID) + if err != nil { + return stacktrace.Propagate(err, "") + } + return nil +} + func (repo *Repository) UpdateRecoveryStatusForID(ctx context.Context, sessionID uuid.UUID, status ente.RecoveryStatus) (bool, error) { validPrevStatus := validPreviousStatus(status) var result sql.Result @@ -106,6 +134,7 @@ func (repo *Repository) UpdateRecoveryStatusForID(ctx context.Context, sessionID rows, _ := result.RowsAffected() return rows > 0, nil } + func (repo *Repository) GetRecoverRowByID(ctx context.Context, sessionID uuid.UUID) (*RecoverRow, error) { var row RecoverRow err := repo.DB.QueryRowContext(ctx, `SELECT id, user_id, emergency_contact_id, status, wait_till, next_reminder_at, created_at