diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index f2c2d8e2b3..0536871755 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -643,6 +643,8 @@ func main() { adminAPI.POST("/user/disable-2fa", adminHandler.DisableTwoFactor) adminAPI.POST("/user/update-referral", adminHandler.UpdateReferral) adminAPI.POST("/user/disable-passkeys", adminHandler.RemovePasskeys) + adminAPI.POST("/user/update-email-mfa", adminHandler.UpdateEmailMFA) + adminAPI.POST("/user/add-ott", adminHandler.AddOtt) adminAPI.POST("/user/close-family", adminHandler.CloseFamily) adminAPI.PUT("/user/change-email", adminHandler.ChangeEmail) adminAPI.DELETE("/user/delete", adminHandler.DeleteUser) diff --git a/server/ente/admin.go b/server/ente/admin.go index 5434ea4553..828173be7f 100644 --- a/server/ente/admin.go +++ b/server/ente/admin.go @@ -3,6 +3,7 @@ package ente import ( "errors" "fmt" + "time" ) // GetEmailsFromHashesRequest represents a request to convert hashes @@ -23,8 +24,29 @@ type UpdateReferralCodeRequest struct { Code string `json:"code" binding:"required"` } +type AdminOttReq struct { + Email string `json:"email" binding:"required"` + Code string `json:"code" binding:"required"` + App App `json:"app" binding:"required"` + ExpiryTime int64 `json:"expiryTime" binding:"required"` +} + +func (a AdminOttReq) Validate() error { + if !a.App.IsValid() { + return errors.New("invalid app") + } + if a.ExpiryTime < time.Now().UnixMicro() { + return errors.New("expiry time should be in future") + } + if len(a.Code) < 6 { + return errors.New("invalid code length, should be at least 6 digit") + } + return nil +} + type AdminOpsForUserRequest struct { - UserID int64 `json:"userID" binding:"required"` + UserID int64 `json:"userID" binding:"required"` + EmailMFA *bool `json:"emailMFA"` } // ReQueueItemRequest puts an item back into the queue for processing. diff --git a/server/pkg/api/admin.go b/server/pkg/api/admin.go index 4783f2e98b..f1006a1d6b 100644 --- a/server/pkg/api/admin.go +++ b/server/pkg/api/admin.go @@ -281,6 +281,67 @@ func (h *AdminHandler) RemovePasskeys(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) } +func (h *AdminHandler) UpdateEmailMFA(c *gin.Context) { + var request ente.AdminOpsForUserRequest + if err := c.ShouldBindJSON(&request); err != nil { + handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request")) + return + } + if request.EmailMFA == nil { + handler.Error(c, stacktrace.Propagate(ente.NewBadRequestWithMessage("emailMFA is required"), "")) + return + } + + go h.DiscordController.NotifyAdminAction( + fmt.Sprintf("Admin (%d) updating email mfa (%v) for account %d", auth.GetUserID(c.Request.Header), request.EmailMFA, request.UserID)) + logger := logrus.WithFields(logrus.Fields{ + "user_id": request.UserID, + "admin_id": auth.GetUserID(c.Request.Header), + "req_id": requestid.Get(c), + "req_ctx": "disable_email_mfa", + }) + logger.Info("Initiate remove passkeys") + err := h.UserController.UpdateEmailMFA(c, request.UserID, *request.EmailMFA) + if err != nil { + logger.WithError(err).Error("Failed to update email mfa") + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + logger.Info("Email MFA successfully updated") + c.JSON(http.StatusOK, gin.H{}) +} + +func (h *AdminHandler) AddOtt(c *gin.Context) { + var request ente.AdminOttReq + if err := c.ShouldBindJSON(&request); err != nil { + handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request")) + return + } + if err := request.Validate(); err != nil { + handler.Error(c, stacktrace.Propagate(ente.NewBadRequestWithMessage(err.Error()), "Bad request")) + return + } + + go h.DiscordController.NotifyAdminAction( + fmt.Sprintf("Admin (%d) adding custom ott", auth.GetUserID(c.Request.Header))) + logger := logrus.WithFields(logrus.Fields{ + "user_id": request.Email, + "code": request.Code, + "admin_id": auth.GetUserID(c.Request.Header), + "req_id": requestid.Get(c), + "req_ctx": "custom_ott", + }) + + err := h.UserController.AddAdminOtt(request) + if err != nil { + logger.WithError(err).Error("Failed to add ott") + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + logger.Info("Success added ott") + c.JSON(http.StatusOK, gin.H{}) +} + func (h *AdminHandler) UpdateFeatureFlag(c *gin.Context) { var request ente.AdminUpdateKeyValueRequest if err := c.ShouldBindJSON(&request); err != nil { diff --git a/server/pkg/controller/user/userauth.go b/server/pkg/controller/user/userauth.go index 8f662280f2..f57d95869b 100644 --- a/server/pkg/controller/user/userauth.go +++ b/server/pkg/controller/user/userauth.go @@ -134,6 +134,20 @@ func (c *UserController) SendEmailOTT(context *gin.Context, email string, purpos return nil } +func (c *UserController) AddAdminOtt(req ente.AdminOttReq) error { + emailHash, err := crypto.GetHash(req.Email, c.HashingKey) + if err != nil { + log.WithError(err).Error("Failed to get hash") + return nil + } + err = c.UserAuthRepo.AddOTT(emailHash, req.App, req.Code, req.ExpiryTime) + if err != nil { + log.WithError(err).Error("Failed to add ott") + return stacktrace.Propagate(err, "") + } + return nil +} + // verifyEmailOtt should be deprecated in favor of verifyEmailOttWithSession once clients are updated. func (c *UserController) verifyEmailOtt(context *gin.Context, email string, ott string) error { ott = strings.TrimSpace(ott) diff --git a/server/pkg/utils/email/email.go b/server/pkg/utils/email/email.go index 57ce28f6e1..5666aed556 100644 --- a/server/pkg/utils/email/email.go +++ b/server/pkg/utils/email/email.go @@ -98,8 +98,8 @@ func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject s err := smtp.SendMail(smtpServer+":"+smtpPort, auth, fromEmail, []string{toEmail}, []byte(emailMessage)) if err != nil { errMsg := err.Error() - for _, knownError := range knownInvalidEmailErrors { - if strings.Contains(errMsg, knownError) { + for i := range knownInvalidEmailErrors { + if strings.Contains(errMsg, knownInvalidEmailErrors[i]) { return stacktrace.Propagate(ente.NewBadRequestWithMessage(fmt.Sprintf("Invalid email %s", toEmail)), errMsg) } }