495 lines
15 KiB
Go
495 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{})
|
|
}
|