123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- package api
- import (
- "errors"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "github.com/ente-io/museum/pkg/controller/family"
- "github.com/ente-io/museum/pkg/repo/storagebonus"
- gTime "time"
- "github.com/ente-io/museum/pkg/controller"
- "github.com/ente-io/museum/pkg/controller/discord"
- "github.com/ente-io/museum/pkg/controller/user"
- "github.com/ente-io/museum/pkg/utils/auth"
- "github.com/ente-io/museum/pkg/utils/time"
- "github.com/gin-contrib/requestid"
- "github.com/sirupsen/logrus"
- "github.com/ente-io/museum/pkg/utils/crypto"
- "github.com/ente-io/stacktrace"
- "github.com/ente-io/museum/ente"
- "github.com/ente-io/museum/pkg/repo"
- emailUtil "github.com/ente-io/museum/pkg/utils/email"
- "github.com/ente-io/museum/pkg/utils/handler"
- "github.com/gin-gonic/gin"
- )
- // AdminHandler exposes request handlers for all admin related requests
- type AdminHandler struct {
- QueueRepo *repo.QueueRepository
- UserRepo *repo.UserRepository
- CollectionRepo *repo.CollectionRepository
- UserAuthRepo *repo.UserAuthRepository
- FileRepo *repo.FileRepository
- BillingRepo *repo.BillingRepository
- StorageBonusRepo *storagebonus.Repository
- BillingController *controller.BillingController
- UserController *user.UserController
- FamilyController *family.Controller
- ObjectCleanupController *controller.ObjectCleanupController
- MailingListsController *controller.MailingListsController
- DiscordController *discord.DiscordController
- HashingKey []byte
- PasskeyController *controller.PasskeyController
- }
- // Duration for which an admin's token is considered valid
- const AdminTokenValidityInMinutes = 10
- func (h *AdminHandler) SendMail(c *gin.Context) {
- var req ente.SendEmailRequest
- err := c.ShouldBindJSON(&req)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- err = emailUtil.Send(req.To, req.FromName, req.FromEmail, req.Subject, req.Body, nil)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- c.JSON(http.StatusOK, gin.H{})
- }
- func (h *AdminHandler) SubscribeMail(c *gin.Context) {
- email := c.Query("email")
- err := h.MailingListsController.Subscribe(email)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- c.Status(http.StatusOK)
- }
- func (h *AdminHandler) UnsubscribeMail(c *gin.Context) {
- email := c.Query("email")
- err := h.MailingListsController.Unsubscribe(email)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- c.Status(http.StatusOK)
- }
- func (h *AdminHandler) GetUsers(c *gin.Context) {
- err := h.isFreshAdminToken(c)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- sinceTime, err := strconv.ParseInt(c.Query("sinceTime"), 10, 64)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- users, err := h.UserRepo.GetAll(sinceTime, time.Microseconds())
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- c.JSON(http.StatusOK, gin.H{"users": users})
- }
- func (h *AdminHandler) GetUser(c *gin.Context) {
- e := c.Query("email")
- if e == "" {
- id, err := strconv.ParseInt(c.Query("id"), 10, 64)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
- return
- }
- user, err := h.UserRepo.GetUserByIDInternal(id)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- response := gin.H{
- "user": user,
- }
- h.attachSubscription(c, user.ID, response)
- c.JSON(http.StatusOK, response)
- return
- }
- emailHash, err := crypto.GetHash(e, h.HashingKey)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- user, err := h.UserRepo.GetUserByEmailHash(emailHash)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- user.Email = e
- response := gin.H{
- "user": user,
- }
- h.attachSubscription(c, user.ID, response)
- c.JSON(http.StatusOK, response)
- }
- func (h *AdminHandler) DeleteUser(c *gin.Context) {
- err := h.isFreshAdminToken(c)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- email := c.Query("email")
- email = strings.TrimSpace(email)
- if email == "" {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "email id is missing"))
- return
- }
- emailHash, err := crypto.GetHash(email, h.HashingKey)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- user, err := h.UserRepo.GetUserByEmailHash(emailHash)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- adminID := auth.GetUserID(c.Request.Header)
- logger := logrus.WithFields(logrus.Fields{
- "user_id": user.ID,
- "admin_id": adminID,
- "user_email": email,
- "req_id": requestid.Get(c),
- "req_ctx": "account_deletion",
- })
- response, err := h.UserController.HandleAccountDeletion(c, user.ID, logger)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) deleting account for %d", adminID, user.ID))
- c.JSON(http.StatusOK, response)
- }
- func (h *AdminHandler) isFreshAdminToken(c *gin.Context) error {
- token := auth.GetToken(c)
- creationTime, err := h.UserAuthRepo.GetTokenCreationTime(token)
- if err != nil {
- return err
- }
- if (creationTime + time.MicroSecondsInOneMinute*AdminTokenValidityInMinutes) < time.Microseconds() {
- err = ente.NewBadRequestError(&ente.ApiErrorParams{
- Message: "Token is too old",
- })
- return err
- }
- return nil
- }
- func (h *AdminHandler) DisableTwoFactor(c *gin.Context) {
- err := h.isFreshAdminToken(c)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- var request ente.DisableTwoFactorRequest
- if err := c.ShouldBindJSON(&request); err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
- return
- }
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) disabling 2FA for account %d", auth.GetUserID(c.Request.Header), 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_2fa",
- })
- logger.Info("Initiate disable 2FA")
- err = h.UserController.DisableTwoFactor(request.UserID)
- if err != nil {
- logger.WithError(err).Error("Failed to disable 2FA")
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- logger.Info("2FA successfully disabled")
- c.JSON(http.StatusOK, gin.H{})
- }
- // RemovePasskeys is an admin API request to disable passkey 2FA for a user account by removing its passkeys.
- // This is used when we get a user request to reset their passkeys 2FA when they might've lost access to their devices or synced stores. We verify their identity out of band.
- // BY DEFAULT, IF THE USER HAS TOTP BASED 2FA ENABLED, REMOVING PASSKEYS WILL NOT DISABLE TOTP 2FA.
- func (h *AdminHandler) RemovePasskeys(c *gin.Context) {
- var request ente.AdminOpsForUserRequest
- if err := c.ShouldBindJSON(&request); err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
- return
- }
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) removing passkeys for account %d", auth.GetUserID(c.Request.Header), 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": "remove_passkeys",
- })
- logger.Info("Initiate remove passkeys")
- err := h.PasskeyController.RemovePasskey2FA(request.UserID)
- if err != nil {
- logger.WithError(err).Error("Failed to remove passkeys")
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- logger.Info("Passkeys successfully removed")
- c.JSON(http.StatusOK, gin.H{})
- }
- func (h *AdminHandler) CloseFamily(c *gin.Context) {
- var request ente.AdminOpsForUserRequest
- if err := c.ShouldBindJSON(&request); err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
- return
- }
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) closing family for account %d", auth.GetUserID(c.Request.Header), 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": "close_family",
- })
- logger.Info("Start close family")
- err := h.FamilyController.CloseFamily(c, request.UserID)
- if err != nil {
- logger.WithError(err).Error("Failed to close family")
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- logger.Info("Finished close family")
- c.JSON(http.StatusOK, gin.H{})
- }
- func (h *AdminHandler) UpdateSubscription(c *gin.Context) {
- var r ente.UpdateSubscriptionRequest
- if err := c.ShouldBindJSON(&r); err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
- return
- }
- r.AdminID = auth.GetUserID(c.Request.Header)
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) updating subscription for user: %d", r.AdminID, r.UserID))
- err := h.BillingController.UpdateSubscription(r)
- if err != nil {
- logrus.WithError(err).Error("Failed to update subscription")
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- logrus.Info("Updated subscription")
- c.JSON(http.StatusOK, gin.H{})
- }
- func (h *AdminHandler) ReQueueItem(c *gin.Context) {
- var r ente.ReQueueItemRequest
- if err := c.ShouldBindJSON(&r); err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
- return
- }
- adminID := auth.GetUserID(c.Request.Header)
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) requeueing item %d for queue: %s", adminID, r.ID, r.QueueName))
- err := h.QueueRepo.RequeueItem(c, r.QueueName, r.ID)
- if err != nil {
- logrus.WithError(err).Error("Failed to re-queue item")
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- c.JSON(http.StatusOK, gin.H{})
- }
- func (h *AdminHandler) UpdateBFDeal(c *gin.Context) {
- var r ente.UpdateBlackFridayDeal
- if err := c.ShouldBindJSON(&r); err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
- return
- }
- if err := r.Validate(); err != nil {
- handler.Error(c, stacktrace.Propagate(ente.NewBadRequestWithMessage(err.Error()), "Bad request"))
- return
- }
- adminID := auth.GetUserID(c.Request.Header)
- var storage, validTill int64
- if r.Testing {
- storage = r.StorageInMB * 1024 * 1024
- validTill = gTime.Now().Add(gTime.Duration(r.Minute) * gTime.Minute).UnixMicro()
- } else {
- storage = r.StorageInGB * 1024 * 1024 * 1024
- validTill = gTime.Now().AddDate(r.Year, 0, 0).UnixMicro()
- }
- var err error
- switch r.Action {
- case ente.ADD:
- err = h.StorageBonusRepo.InsertBFBonus(c, r.UserID, validTill, storage)
- case ente.UPDATE:
- err = h.StorageBonusRepo.UpdateBFBonus(c, r.UserID, validTill, storage)
- case ente.REMOVE:
- _, err = h.StorageBonusRepo.RemoveBFBonus(c, r.UserID)
- }
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) : User %d %s", adminID, r.UserID, r.UpdateLog()))
- c.JSON(http.StatusOK, gin.H{})
- }
- func (h *AdminHandler) RecoverAccount(c *gin.Context) {
- err := h.isFreshAdminToken(c)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- var request ente.RecoverAccountRequest
- if err := c.ShouldBindJSON(&request); err != nil {
- handler.Error(c, stacktrace.Propagate(err, "Bad request"))
- return
- }
- if request.EmailID == "" || !strings.Contains(request.EmailID, "@") {
- handler.Error(c, stacktrace.Propagate(errors.New("invalid email"), "Bad request"))
- return
- }
- go h.DiscordController.NotifyAdminAction(
- fmt.Sprintf("Admin (%d) recovering account for %d", auth.GetUserID(c.Request.Header), request.UserID))
- logger := logrus.WithFields(logrus.Fields{
- "user_id": request.UserID,
- "admin_id": auth.GetUserID(c.Request.Header),
- "user_email": request.EmailID,
- "req_id": requestid.Get(c),
- "req_ctx": "account_recovery",
- })
- logger.Info("Initiate account recovery")
- err = h.UserController.HandleAccountRecovery(c, request)
- if err != nil {
- logger.WithError(err).Error("Failed to recover account")
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- logger.Info("Account successfully recovered")
- c.JSON(http.StatusOK, gin.H{})
- }
- func (h *AdminHandler) GetEmailHash(c *gin.Context) {
- e := c.Query("email")
- hash, err := crypto.GetHash(e, h.HashingKey)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- c.JSON(http.StatusOK, gin.H{"hash": hash})
- }
- func (h *AdminHandler) GetEmailsFromHashes(c *gin.Context) {
- var request ente.GetEmailsFromHashesRequest
- if err := c.ShouldBindJSON(&request); err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- emails, err := h.UserRepo.GetEmailsFromHashes(request.Hashes)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(err, ""))
- return
- }
- c.JSON(http.StatusOK, gin.H{"emails": emails})
- }
- func (h *AdminHandler) attachSubscription(ctx *gin.Context, userID int64, response gin.H) {
- subscription, err := h.BillingRepo.GetUserSubscription(userID)
- if err == nil {
- response["subscription"] = subscription
- }
- details, err := h.UserController.GetDetailsV2(ctx, userID, false, ente.Photos)
- if err == nil {
- response["details"] = details
- }
- }
- func (h *AdminHandler) ClearOrphanObjects(c *gin.Context) {
- var req ente.ClearOrphanObjectsRequest
- err := c.ShouldBindJSON(&req)
- if err != nil {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
- return
- }
- if !h.ObjectCleanupController.IsValidClearOrphanObjectsDC(req.DC) {
- handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "unsupported dc %s", req.DC))
- return
- }
- go h.ObjectCleanupController.ClearOrphanObjects(req.DC, req.Prefix, req.ForceTaskLock)
- c.JSON(http.StatusOK, gin.H{})
- }
|