diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 480d21d32b..0424f0065d 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -627,6 +627,7 @@ func main() { QueueRepo: queueRepo, UserRepo: userRepo, CollectionRepo: collectionRepo, + AuthenticatorRepo: authRepo, UserAuthRepo: userAuthRepo, UserController: userController, FamilyController: familyController, diff --git a/server/ente/admin.go b/server/ente/admin.go index 97aa64fde6..4809d2c5fa 100644 --- a/server/ente/admin.go +++ b/server/ente/admin.go @@ -36,6 +36,14 @@ type LogoutSessionReq struct { UserID int64 `json:"userID" binding:"required"` } +type TokenInfo struct { + CreationTime int64 `json:"creationTime"` + LastUsedTime int64 `json:"lastUsedTime"` + UA string `json:"ua"` + IsDeleted bool `json:"isDeleted"` + App App `json:"app"` +} + func (a AdminOttReq) Validate() error { if !a.App.IsValid() { return errors.New("invalid app") diff --git a/server/ente/user.go b/server/ente/user.go index 77858c4d93..b8db33116b 100644 --- a/server/ente/user.go +++ b/server/ente/user.go @@ -195,9 +195,10 @@ type TwoFactorRemovalRequest struct { type ProfileData struct { // CanDisableEmailMFA is used to decide if client should show disable email MFA option - CanDisableEmailMFA bool `json:"canDisableEmailMFA"` - IsEmailMFAEnabled bool `json:"isEmailMFAEnabled"` - IsTwoFactorEnabled bool `json:"isTwoFactorEnabled"` + CanDisableEmailMFA bool `json:"canDisableEmailMFA"` + IsEmailMFAEnabled bool `json:"isEmailMFAEnabled"` + IsTwoFactorEnabled bool `json:"isTwoFactorEnabled"` + PasskeyCount int64 `json:"passkeyCount"` } type Session struct { diff --git a/server/pkg/api/admin.go b/server/pkg/api/admin.go index a2e1b18165..3ec79db78d 100644 --- a/server/pkg/api/admin.go +++ b/server/pkg/api/admin.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/ente-io/museum/pkg/controller/remotestore" + "github.com/ente-io/museum/pkg/repo/authenticator" "net/http" "strconv" "strings" @@ -39,6 +40,7 @@ type AdminHandler struct { QueueRepo *repo.QueueRepository UserRepo *repo.UserRepository CollectionRepo *repo.CollectionRepository + AuthenticatorRepo *authenticator.Repository UserAuthRepo *repo.UserAuthRepository FileRepo *repo.FileRepository BillingRepo *repo.BillingRepository @@ -113,7 +115,7 @@ func (h *AdminHandler) GetUsers(c *gin.Context) { } func (h *AdminHandler) GetUser(c *gin.Context) { - e := c.Query("email") + e := strings.ToLower(strings.TrimSpace(c.Query("email"))) if e == "" { id, err := strconv.ParseInt(c.Query("id"), 10, 64) if err != nil { @@ -571,6 +573,14 @@ func (h *AdminHandler) attachSubscription(ctx *gin.Context, userID int64, respon if err == nil { response["details"] = details } + tokenInfos, err := h.UserAuthRepo.GetUserTokenInfo(userID) + if err == nil { + response["tokens"] = tokenInfos + } + authEntryCount, err := h.AuthenticatorRepo.GetAuthCodeCount(ctx, userID) + if err == nil { + response["authCodes"] = authEntryCount + } } func (h *AdminHandler) ClearOrphanObjects(c *gin.Context) { diff --git a/server/pkg/controller/user/user_details.go b/server/pkg/controller/user/user_details.go index ee4acb730e..70a50d39ae 100644 --- a/server/pkg/controller/user/user_details.go +++ b/server/pkg/controller/user/user_details.go @@ -5,7 +5,6 @@ import ( "github.com/ente-io/museum/ente" "github.com/ente-io/museum/ente/details" bonus "github.com/ente-io/museum/ente/storagebonus" - "github.com/ente-io/museum/pkg/utils/recover" "github.com/ente-io/museum/pkg/utils/time" "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" @@ -27,6 +26,7 @@ func (c *UserController) GetDetailsV2(ctx *gin.Context, userID int64, fetchMemor var familyData *ente.FamilyMemberResponse var subscription *ente.Subscription var canDisableEmailMFA bool + var passkeyCount int64 var fileCount, sharedCollectionCount, usage int64 var bonus *bonus.ActiveStorageBonus g.Go(func() error { @@ -69,7 +69,12 @@ func (c *UserController) GetDetailsV2(ctx *gin.Context, userID int64, fetchMemor return nil }) g.Go(func() error { - return recover.Int64ToInt64RecoverWrapper(userID, c.FileRepo.GetUsage, &usage) + cnt, err := c.PasskeyRepo.GetPasskeyCount(userID) + if err != nil { + return stacktrace.Propagate(err, "") + } + passkeyCount = cnt + return nil }) if fetchMemoryCount { @@ -111,6 +116,7 @@ func (c *UserController) GetDetailsV2(ctx *gin.Context, userID int64, fetchMemor CanDisableEmailMFA: canDisableEmailMFA, IsEmailMFAEnabled: *user.IsEmailMFAEnabled, IsTwoFactorEnabled: *user.IsTwoFactorEnabled, + PasskeyCount: passkeyCount, }, BonusData: bonus, } diff --git a/server/pkg/repo/authenticator/entity.go b/server/pkg/repo/authenticator/entity.go index 056d0043ba..d47892212f 100644 --- a/server/pkg/repo/authenticator/entity.go +++ b/server/pkg/repo/authenticator/entity.go @@ -89,6 +89,16 @@ func (r *Repository) Update(ctx context.Context, userID int64, req model.UpdateE return nil } +// GetAuthCodeCount returns the count of the authenticator entries for the given user +func (r *Repository) GetAuthCodeCount(ctx context.Context, userID int64) (int64, error) { + var count int64 + err := r.DB.QueryRowContext(ctx, `SELECT count(*) FROM authenticator_entity WHERE user_id = $1 and is_deleted = FALSE`, userID).Scan(&count) + if err != nil { + return 0, stacktrace.Propagate(err, "failed to get auth code count") + } + return count, nil +} + // GetDiff returns the &{[]ente.TotpEntity} which have been added or // modified after the given sinceTime func (r *Repository) GetDiff(ctx context.Context, userID int64, sinceTime int64, limit int16) ([]model.Entity, error) { diff --git a/server/pkg/repo/passkey/passkey.go b/server/pkg/repo/passkey/passkey.go index 52a908c928..b102350f71 100644 --- a/server/pkg/repo/passkey/passkey.go +++ b/server/pkg/repo/passkey/passkey.go @@ -101,6 +101,11 @@ func NewRepository( return } +func (r *Repository) GetPasskeyCount(userID int64) (count int64, err error) { + err = r.DB.QueryRow(`SELECT COUNT(*) FROM passkeys WHERE user_id = $1 AND deleted_at IS NULL`, userID).Scan(&count) + return +} + func (r *Repository) GetUserPasskeys(userID int64) (passkeys []ente.Passkey, err error) { rows, err := r.DB.Query(` SELECT id, user_id, friendly_name, created_at diff --git a/server/pkg/repo/userauth.go b/server/pkg/repo/userauth.go index 03d597072d..b7fa8c88d1 100644 --- a/server/pkg/repo/userauth.go +++ b/server/pkg/repo/userauth.go @@ -48,6 +48,24 @@ func (repo *UserAuthRepository) GetTokenCreationTime(token string) (int64, error return result, nil } +func (repo *UserAuthRepository) GetUserTokenInfo(userID int64) ([]ente.TokenInfo, error) { + rows, err := repo.DB.Query(`SELECT creation_time, last_used_at, user_agent, is_deleted, app FROM tokens WHERE user_id = $1 AND is_deleted = false`, userID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + defer rows.Close() + tokenInfos := make([]ente.TokenInfo, 0) + for rows.Next() { + var tokenInfo ente.TokenInfo + err := rows.Scan(&tokenInfo.CreationTime, &tokenInfo.LastUsedTime, &tokenInfo.UA, &tokenInfo.IsDeleted, &tokenInfo.App) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + tokenInfos = append(tokenInfos, tokenInfo) + } + return tokenInfos, nil +} + // GetValidOTTs returns the list of OTTs that haven't expired for a given user func (repo *UserAuthRepository) GetValidOTTs(emailHash string, app ente.App) ([]string, error) { rows, err := repo.DB.Query(`SELECT ott FROM otts WHERE email_hash = $1 AND app = $2 AND expiration_time > $3`,