ente/server/pkg/api/admin.go

496 lines
15 KiB
Go

package api
import (
"errors"
"fmt"
"github.com/ente-io/museum/pkg/controller/remotestore"
"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
RemoteStoreController *remotestore.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) UpdateFeatureFlag(c *gin.Context) {
var request ente.AdminUpdateKeyValueRequest
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) updating flag:%s to val:%s for %d", auth.GetUserID(c.Request.Header), request.Key, request.Value, 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": "update_feature_flag",
})
logger.Info("Start update")
err := h.RemoteStoreController.AdminInsertOrUpdate(c, request)
if err != nil {
logger.WithError(err).Error("Failed to update flag")
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
logger.Info("successfully updated flag")
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) ChangeEmail(c *gin.Context) {
var r ente.ChangeEmailRequest
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) updating email for user: %d", adminID, r.UserID))
err := h.UserController.UpdateEmail(c, r.UserID, r.Email)
if err != nil {
logrus.WithError(err).Error("Failed to update email")
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
logrus.Info("Updated email")
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{})
}