[server] Add remaining mails for legacy

This commit is contained in:
Neeraj Gupta
2025-01-15 15:31:51 +05:30
parent 9fe58e44b0
commit 73a8550844
7 changed files with 132 additions and 14 deletions

View File

@@ -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()
})

View File

@@ -1,7 +1,7 @@
{{define "content"}}
<p>Hello,</p>
<p>{{.TrustedContact}} has initiated recovery on your account. After 2 days, they will be able to change the password and access your account. </p>
<p>{{.TrustedContact}} has initiated recovery on your account. After {{.DaysLeft}} days, they will be able to change the password and access your account. </p>
<p>If you want to block the recovery, please navigate to <strong>Settings > Account > Legacy </strong> in the Ente Photos app.</p>

View File

@@ -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,

View File

@@ -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, "")
}

View File

@@ -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)
}
}
}
}

View File

@@ -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, "")
}

View File

@@ -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