411 lines
15 KiB
Go
411 lines
15 KiB
Go
package user
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
cache2 "github.com/ente-io/museum/ente/cache"
|
|
"github.com/ente-io/museum/pkg/controller/discord"
|
|
"github.com/ente-io/museum/pkg/controller/usercache"
|
|
|
|
"github.com/ente-io/museum/ente"
|
|
"github.com/ente-io/museum/pkg/controller"
|
|
"github.com/ente-io/museum/pkg/controller/family"
|
|
"github.com/ente-io/museum/pkg/repo"
|
|
"github.com/ente-io/museum/pkg/repo/datacleanup"
|
|
"github.com/ente-io/museum/pkg/repo/passkey"
|
|
storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
|
|
"github.com/ente-io/museum/pkg/utils/billing"
|
|
"github.com/ente-io/museum/pkg/utils/crypto"
|
|
"github.com/ente-io/museum/pkg/utils/email"
|
|
"github.com/ente-io/stacktrace"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt"
|
|
"github.com/patrickmn/go-cache"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// UserController exposes request handlers for all user related requests
|
|
type UserController struct {
|
|
UserRepo *repo.UserRepository
|
|
UsageRepo *repo.UsageRepository
|
|
UserAuthRepo *repo.UserAuthRepository
|
|
TwoFactorRepo *repo.TwoFactorRepository
|
|
PasskeyRepo *passkey.Repository
|
|
StorageBonusRepo *storageBonusRepo.Repository
|
|
FileRepo *repo.FileRepository
|
|
CollectionRepo *repo.CollectionRepository
|
|
DataCleanupRepo *datacleanup.Repository
|
|
CollectionCtrl *controller.CollectionController
|
|
BillingRepo *repo.BillingRepository
|
|
BillingController *controller.BillingController
|
|
FamilyController *family.Controller
|
|
DiscordController *discord.DiscordController
|
|
MailingListsController *controller.MailingListsController
|
|
PushController *controller.PushController
|
|
HashingKey []byte
|
|
SecretEncryptionKey []byte
|
|
JwtSecret []byte
|
|
Cache *cache.Cache // refers to the auth token cache
|
|
HardCodedOTT HardCodedOTT
|
|
roadmapURLPrefix string
|
|
roadmapSSOSecret string
|
|
UserCache *cache2.UserCache
|
|
UserCacheController *usercache.Controller
|
|
}
|
|
|
|
const (
|
|
// OTTValidityDurationInMicroSeconds is the duration for which an OTT is valid
|
|
// (60 minutes)
|
|
OTTValidityDurationInMicroSeconds = 60 * 60 * 1000000
|
|
|
|
// OTTWrongAttemptLimit is the max number of wrong attempt to verify OTT (to prevent bruteforce guessing)
|
|
// When client hits this limit, they will need to trigger new OTT.
|
|
OTTWrongAttemptLimit = 20
|
|
|
|
// OTTActiveCodeLimit is the max number of active OTT a user can have in
|
|
// a time window of OTTValidityDurationInMicroSeconds duration
|
|
OTTActiveCodeLimit = 10
|
|
|
|
// TwoFactorValidityDurationInMicroSeconds is the duration for which an OTT is valid
|
|
// (10 minutes)
|
|
TwoFactorValidityDurationInMicroSeconds = 10 * 60 * 1000000
|
|
|
|
// TokenLength is the length of the token issued to a verified user
|
|
TokenLength = 32
|
|
|
|
// TwoFactorSessionIDLength is the length of the twoFactorSessionID issued to a verified user
|
|
TwoFactorSessionIDLength = 32
|
|
|
|
// PassKeySessionIDLength is the length of the passKey sessionID issued to a verified user
|
|
PassKeySessionIDLength = 32
|
|
|
|
CryptoPwhashMemLimitInteractive = 67108864
|
|
CryptoPwhashOpsLimitInteractive = 2
|
|
|
|
TOTPIssuerORG = "ente"
|
|
|
|
// Template and subject for the mail that we send when the user deletes
|
|
// their account.
|
|
AccountDeletedEmailTemplate = "account_deleted.html"
|
|
AccountDeletedWithActiveSubscriptionEmailTemplate = "account_deleted_active_sub.html"
|
|
AccountDeletedEmailSubject = "Your ente account has been deleted"
|
|
)
|
|
|
|
func NewUserController(
|
|
userRepo *repo.UserRepository,
|
|
usageRepo *repo.UsageRepository,
|
|
userAuthRepo *repo.UserAuthRepository,
|
|
twoFactorRepo *repo.TwoFactorRepository,
|
|
passkeyRepo *passkey.Repository,
|
|
storageBonusRepo *storageBonusRepo.Repository,
|
|
fileRepo *repo.FileRepository,
|
|
collectionController *controller.CollectionController,
|
|
collectionRepo *repo.CollectionRepository,
|
|
dataCleanupRepository *datacleanup.Repository,
|
|
billingRepo *repo.BillingRepository,
|
|
secretEncryptionKeyBytes []byte,
|
|
hashingKeyBytes []byte,
|
|
authCache *cache.Cache,
|
|
jwtSecretBytes []byte,
|
|
billingController *controller.BillingController,
|
|
familyController *family.Controller,
|
|
discordController *discord.DiscordController,
|
|
mailingListsController *controller.MailingListsController,
|
|
pushController *controller.PushController,
|
|
userCache *cache2.UserCache,
|
|
userCacheController *usercache.Controller,
|
|
) *UserController {
|
|
return &UserController{
|
|
UserRepo: userRepo,
|
|
UsageRepo: usageRepo,
|
|
UserAuthRepo: userAuthRepo,
|
|
StorageBonusRepo: storageBonusRepo,
|
|
TwoFactorRepo: twoFactorRepo,
|
|
PasskeyRepo: passkeyRepo,
|
|
FileRepo: fileRepo,
|
|
CollectionCtrl: collectionController,
|
|
CollectionRepo: collectionRepo,
|
|
DataCleanupRepo: dataCleanupRepository,
|
|
BillingRepo: billingRepo,
|
|
SecretEncryptionKey: secretEncryptionKeyBytes,
|
|
HashingKey: hashingKeyBytes,
|
|
Cache: authCache,
|
|
JwtSecret: jwtSecretBytes,
|
|
BillingController: billingController,
|
|
FamilyController: familyController,
|
|
DiscordController: discordController,
|
|
MailingListsController: mailingListsController,
|
|
PushController: pushController,
|
|
HardCodedOTT: ReadHardCodedOTTFromConfig(),
|
|
roadmapURLPrefix: viper.GetString("roadmap.url-prefix"),
|
|
roadmapSSOSecret: viper.GetString("roadmap.sso-secret"),
|
|
UserCache: userCache,
|
|
UserCacheController: userCacheController,
|
|
}
|
|
}
|
|
|
|
// GetAttributes returns the key attributes for a user
|
|
func (c *UserController) GetAttributes(userID int64) (ente.KeyAttributes, error) {
|
|
return c.UserRepo.GetKeyAttributes(userID)
|
|
}
|
|
|
|
// SetAttributes sets the attributes for a user. The request will fail if key attributes are already set
|
|
func (c *UserController) SetAttributes(userID int64, request ente.SetUserAttributesRequest) error {
|
|
_, err := c.UserRepo.GetKeyAttributes(userID)
|
|
if err == nil { // If there are key attributes already set
|
|
return stacktrace.Propagate(ente.ErrPermissionDenied, "key attributes are already set")
|
|
}
|
|
if request.KeyAttributes.MemLimit <= 0 || request.KeyAttributes.OpsLimit <= 0 {
|
|
// note for curious soul in the future
|
|
_ = fmt.Sprintf("Older clients were not passing these values, so server used %d & %d as ops and memLimit",
|
|
CryptoPwhashOpsLimitInteractive, CryptoPwhashMemLimitInteractive)
|
|
return stacktrace.Propagate(ente.ErrBadRequest, "mem or ops limit should be > 0")
|
|
}
|
|
err = c.UserRepo.SetKeyAttributes(userID, request.KeyAttributes)
|
|
if err != nil {
|
|
return stacktrace.Propagate(err, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateEmailMFA updates the email MFA for a user.
|
|
func (c *UserController) UpdateEmailMFA(context *gin.Context, userID int64, isEnabled bool) error {
|
|
if !isEnabled {
|
|
isSrpSetupDone, err := c.UserAuthRepo.IsSRPSetupDone(context, userID)
|
|
if err != nil {
|
|
return stacktrace.Propagate(err, "")
|
|
}
|
|
// if SRP is not setup, then we can not disable email MFA
|
|
if !isSrpSetupDone {
|
|
return stacktrace.Propagate(ente.NewConflictError("SRP setup incomplete"), "can not disable email MFA before SRP is setup")
|
|
}
|
|
}
|
|
return c.UserAuthRepo.UpdateEmailMFA(context, userID, isEnabled)
|
|
}
|
|
|
|
// UpdateKeys updates the user keys on password change
|
|
func (c *UserController) UpdateKeys(context *gin.Context, userID int64,
|
|
request ente.UpdateKeysRequest, token string) error {
|
|
/*
|
|
todo: send email to the user on password change and may be keep history of old keys for X days.
|
|
History will allow easy recovery of the account when password is changed by a bad actor
|
|
*/
|
|
isSRPSetupDone, err := c.UserAuthRepo.IsSRPSetupDone(context, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isSRPSetupDone {
|
|
return stacktrace.Propagate(ente.NewBadRequestWithMessage("Need to upgrade client"), "can not use old API to change password after SRP is setup")
|
|
}
|
|
err = c.UserRepo.UpdateKeys(userID, request)
|
|
if err != nil {
|
|
return stacktrace.Propagate(err, "")
|
|
}
|
|
err = c.UserAuthRepo.RemoveAllOtherTokens(userID, token)
|
|
if err != nil {
|
|
return stacktrace.Propagate(err, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetRecoveryKey sets the recovery key attributes for a user, if not already set
|
|
func (c *UserController) SetRecoveryKey(userID int64, request ente.SetRecoveryKeyRequest) error {
|
|
keyAttr, keyErr := c.UserRepo.GetKeyAttributes(userID)
|
|
if keyErr != nil {
|
|
return stacktrace.Propagate(keyErr, "User keys setup is not completed")
|
|
}
|
|
if keyAttr.RecoveryKeyEncryptedWithMasterKey != "" {
|
|
return stacktrace.Propagate(errors.New("recovery key is already set"), "")
|
|
}
|
|
err := c.UserRepo.SetRecoveryKeyAttributes(userID, request)
|
|
if err != nil {
|
|
return stacktrace.Propagate(err, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetPublicKey returns the public key of a user
|
|
func (c *UserController) GetPublicKey(email string) (string, error) {
|
|
userID, err := c.UserRepo.GetUserIDWithEmail(email)
|
|
if err != nil {
|
|
return "", stacktrace.Propagate(err, "")
|
|
}
|
|
key, err := c.UserRepo.GetPublicKey(userID)
|
|
if err != nil {
|
|
return "", stacktrace.Propagate(err, "")
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// GetRoadmapURL redirects the user to the feedback page
|
|
func (c *UserController) GetRoadmapURL(userID int64) (string, error) {
|
|
// If SSO is not configured, redirect the user to the plain roadmap
|
|
if c.roadmapURLPrefix == "" || c.roadmapSSOSecret == "" {
|
|
return "https://roadmap.ente.io", nil
|
|
}
|
|
user, err := c.UserRepo.Get(userID)
|
|
if err != nil {
|
|
return "", stacktrace.Propagate(err, "")
|
|
}
|
|
userData := jwt.MapClaims{
|
|
"full_name": "",
|
|
"email": user.Hash + "@ente.io",
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, userData)
|
|
signature, err := token.SignedString([]byte(c.roadmapSSOSecret))
|
|
if err != nil {
|
|
return "", stacktrace.Propagate(err, "")
|
|
}
|
|
return c.roadmapURLPrefix + signature, nil
|
|
}
|
|
|
|
// GetTwoFactorStatus returns a user's two factor status
|
|
func (c *UserController) GetTwoFactorStatus(userID int64) (bool, error) {
|
|
isTwoFactorEnabled, err := c.UserRepo.IsTwoFactorEnabled(userID)
|
|
if err != nil {
|
|
return false, stacktrace.Propagate(err, "")
|
|
}
|
|
return isTwoFactorEnabled, nil
|
|
}
|
|
|
|
func (c *UserController) HandleAccountDeletion(ctx *gin.Context, userID int64, logger *logrus.Entry) (*ente.DeleteAccountResponse, error) {
|
|
isSubscriptionCancelled, err := c.BillingController.HandleAccountDeletion(ctx, userID, logger)
|
|
if err != nil {
|
|
return nil, stacktrace.Propagate(err, "")
|
|
}
|
|
|
|
err = c.CollectionCtrl.HandleAccountDeletion(ctx, userID, logger)
|
|
if err != nil {
|
|
return nil, stacktrace.Propagate(err, "")
|
|
}
|
|
|
|
err = c.FamilyController.HandleAccountDeletion(ctx, userID, logger)
|
|
if err != nil {
|
|
return nil, stacktrace.Propagate(err, "")
|
|
}
|
|
|
|
logger.Info("remove push tokens for user")
|
|
c.PushController.RemoveTokensForUser(userID)
|
|
|
|
logger.Info("remove active tokens for user")
|
|
err = c.UserAuthRepo.RemoveAllTokens(userID)
|
|
if err != nil {
|
|
return nil, stacktrace.Propagate(err, "")
|
|
}
|
|
|
|
user, err := c.UserRepo.Get(userID)
|
|
if err != nil {
|
|
return nil, stacktrace.Propagate(err, "")
|
|
}
|
|
|
|
email := user.Email
|
|
// See also: Do not block on mailing list errors
|
|
go func() {
|
|
_ = c.MailingListsController.Unsubscribe(email)
|
|
}()
|
|
|
|
logger.Info("mark user as deleted")
|
|
err = c.UserRepo.Delete(userID)
|
|
if err != nil {
|
|
return nil, stacktrace.Propagate(err, "")
|
|
}
|
|
|
|
logger.Info("schedule data deletion")
|
|
err = c.DataCleanupRepo.Insert(ctx, userID)
|
|
if err != nil {
|
|
return nil, stacktrace.Propagate(err, "")
|
|
}
|
|
|
|
go c.NotifyAccountDeletion(email, isSubscriptionCancelled)
|
|
|
|
return &ente.DeleteAccountResponse{
|
|
IsSubscriptionCancelled: isSubscriptionCancelled,
|
|
UserID: userID,
|
|
}, nil
|
|
|
|
}
|
|
|
|
func (c *UserController) NotifyAccountDeletion(userEmail string, isSubscriptionCancelled bool) {
|
|
template := AccountDeletedEmailTemplate
|
|
if !isSubscriptionCancelled {
|
|
template = AccountDeletedWithActiveSubscriptionEmailTemplate
|
|
}
|
|
err := email.SendTemplatedEmail([]string{userEmail}, "ente", "team@ente.io",
|
|
AccountDeletedEmailSubject, template, nil, nil)
|
|
if err != nil {
|
|
logrus.WithError(err).Errorf("Failed to send the account deletion email to %s", userEmail)
|
|
}
|
|
}
|
|
|
|
func (c *UserController) HandleAccountRecovery(ctx *gin.Context, req ente.RecoverAccountRequest) error {
|
|
_, err := c.UserRepo.Get(req.UserID)
|
|
if err == nil {
|
|
return stacktrace.Propagate(ente.NewBadRequestError(&ente.ApiErrorParams{
|
|
Message: "User ID is linked to undeleted account",
|
|
}), "")
|
|
}
|
|
if !errors.Is(err, ente.ErrUserDeleted) {
|
|
return stacktrace.Propagate(err, "error while getting the user")
|
|
}
|
|
// check if the user keyAttributes are still available
|
|
if _, keyErr := c.UserRepo.GetKeyAttributes(req.UserID); keyErr != nil {
|
|
return stacktrace.Propagate(keyErr, "keyAttributes missing? Account can not be recovered")
|
|
}
|
|
email := strings.ToLower(req.EmailID)
|
|
encryptedEmail, err := crypto.Encrypt(email, c.SecretEncryptionKey)
|
|
if err != nil {
|
|
return stacktrace.Propagate(err, "")
|
|
}
|
|
emailHash, err := crypto.GetHash(email, c.HashingKey)
|
|
if err != nil {
|
|
return stacktrace.Propagate(err, "")
|
|
}
|
|
err = c.UserRepo.UpdateEmail(req.UserID, encryptedEmail, emailHash)
|
|
return stacktrace.Propagate(err, "failed to update email")
|
|
}
|
|
|
|
func (c *UserController) attachFreeSubscription(userID int64) (ente.Subscription, error) {
|
|
subscription := billing.GetFreeSubscription(userID)
|
|
generatedID, err := c.BillingRepo.AddSubscription(subscription)
|
|
if err != nil {
|
|
return subscription, stacktrace.Propagate(err, "")
|
|
}
|
|
subscription.ID = generatedID
|
|
return subscription, nil
|
|
}
|
|
|
|
func (c *UserController) createUser(email string, source *string) (int64, ente.Subscription, error) {
|
|
encryptedEmail, err := crypto.Encrypt(email, c.SecretEncryptionKey)
|
|
if err != nil {
|
|
return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
|
|
}
|
|
emailHash, err := crypto.GetHash(email, c.HashingKey)
|
|
if err != nil {
|
|
return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
|
|
}
|
|
userID, err := c.UserRepo.Create(encryptedEmail, emailHash, source)
|
|
if err != nil {
|
|
return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
|
|
}
|
|
err = c.UsageRepo.Create(userID)
|
|
if err != nil {
|
|
return -1, ente.Subscription{}, stacktrace.Propagate(err, "failed to add entry in usage")
|
|
}
|
|
subscription, err := c.attachFreeSubscription(userID)
|
|
if err != nil {
|
|
return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
|
|
}
|
|
// Do not block on mailing list errors
|
|
//
|
|
// The mailing lists are hosted on a third party (Zoho), so we do not wish
|
|
// to fail user creation in case Zoho is having temporary issues. So we
|
|
// perform these actions async, and ignore errors that happen with them (a
|
|
// notification will be sent to Discord for those).
|
|
go func() {
|
|
_ = c.MailingListsController.Subscribe(email)
|
|
}()
|
|
return userID, subscription, nil
|
|
}
|