[cli] Add admin API to bump up storage for free users
This commit is contained in:
parent
51d3238a52
commit
0d38346722
9 changed files with 373 additions and 17 deletions
83
cli/cmd/admin.go
Normal file
83
cli/cmd/admin.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/pkg/model"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var _adminCmd = &cobra.Command{
|
||||
Use: "admin",
|
||||
Short: "Commands for admin actions",
|
||||
Long: "Commands for admin actions like disable or enabling 2fa, bumping up the storage limit, etc.",
|
||||
}
|
||||
|
||||
var _userDetailsCmd = &cobra.Command{
|
||||
Use: "get-user-id",
|
||||
Short: "Get user id",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
if f.Name == "user" {
|
||||
flags.UserEmail = f.Value.String()
|
||||
}
|
||||
})
|
||||
return ctrl.GetUserId(context.Background(), *flags)
|
||||
},
|
||||
}
|
||||
|
||||
var _disable2faCmd = &cobra.Command{
|
||||
Use: "disable-2fa",
|
||||
Short: "Disable 2fa for a user",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
if f.Name == "user" {
|
||||
flags.UserEmail = f.Value.String()
|
||||
}
|
||||
})
|
||||
fmt.Println("Not supported yet")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var _updateFreeUserStorage = &cobra.Command{
|
||||
Use: "update-subscription",
|
||||
Short: "Update subscription for the free user",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
if f.Name == "user" {
|
||||
flags.UserEmail = f.Value.String()
|
||||
}
|
||||
})
|
||||
return ctrl.UpdateFreeStorage(context.Background(), *flags)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(_adminCmd)
|
||||
_ = _userDetailsCmd.MarkFlagRequired("admin-user")
|
||||
_ = _userDetailsCmd.MarkFlagRequired("user")
|
||||
_userDetailsCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
|
||||
_userDetailsCmd.Flags().StringP("user", "u", "", "The email of the user to fetch details for. (required)")
|
||||
_disable2faCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
|
||||
_disable2faCmd.Flags().StringP("user", "u", "", "The email of the user to disable 2FA for. (required)")
|
||||
_updateFreeUserStorage.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
|
||||
_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
|
||||
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _updateFreeUserStorage)
|
||||
}
|
57
cli/internal/api/admin.go
Normal file
57
cli/internal/api/admin.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal/api/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) GetUserIdFromEmail(ctx context.Context, email string) (*models.UserDetails, error) {
|
||||
var res models.UserDetails
|
||||
r, err := c.restClient.R().
|
||||
SetContext(ctx).
|
||||
SetResult(&res).
|
||||
SetQueryParam("email", email).
|
||||
Get("/admin/user/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.IsError() {
|
||||
return nil, &ApiError{
|
||||
StatusCode: r.StatusCode(),
|
||||
Message: r.String(),
|
||||
}
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.UserDetails, storageInBytes int64, expiryTimeInMicro int64) error {
|
||||
var res interface{}
|
||||
if userDetails.Subscription.ProductID != "free" {
|
||||
return fmt.Errorf("user is not on free plan")
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"userID": userDetails.User.ID,
|
||||
"expiryTime": expiryTimeInMicro,
|
||||
"transactionID": fmt.Sprintf("cli-on-%d", time.Now().Unix()),
|
||||
"productID": "free",
|
||||
"paymentProvider": "",
|
||||
"storage": storageInBytes,
|
||||
}
|
||||
r, err := c.restClient.R().
|
||||
SetContext(ctx).
|
||||
SetResult(&res).
|
||||
SetBody(payload).
|
||||
Put("/admin/user/subscription")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.IsError() {
|
||||
return &ApiError{
|
||||
StatusCode: r.StatusCode(),
|
||||
Message: r.String(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
16
cli/internal/api/models/user_details.go
Normal file
16
cli/internal/api/models/user_details.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package models
|
||||
|
||||
type UserDetails struct {
|
||||
User struct {
|
||||
ID int64 `json:"id"`
|
||||
} `json:"user"`
|
||||
Usage int64 `json:"usage"`
|
||||
Email string `json:"email"`
|
||||
|
||||
Subscription struct {
|
||||
ExpiryTime int64 `json:"expiryTime"`
|
||||
Storage int64 `json:"storage"`
|
||||
ProductID string `json:"productID"`
|
||||
PaymentProvider string `json:"paymentProvider"`
|
||||
} `json:"subscription"`
|
||||
}
|
|
@ -5,11 +5,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal/api"
|
||||
"golang.org/x/term"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func GetSensitiveField(label string) (string, error) {
|
||||
|
@ -81,6 +82,79 @@ func GetCode(promptText string, length int) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// parseStorageSize parses a string representing a storage size (e.g., "500MB", "2GB") into bytes.
|
||||
func parseStorageSize(input string) (int64, error) {
|
||||
units := map[string]int64{
|
||||
"MB": 1 << 20,
|
||||
"GB": 1 << 30,
|
||||
"TB": 1 << 40,
|
||||
}
|
||||
re := regexp.MustCompile(`(?i)^(\d+(?:\.\d+)?)(MB|GB|TB)$`)
|
||||
matches := re.FindStringSubmatch(input)
|
||||
|
||||
if matches == nil {
|
||||
return 0, errors.New("invalid format")
|
||||
}
|
||||
|
||||
number, err := strconv.ParseFloat(matches[1], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid number: %s", matches[1])
|
||||
}
|
||||
|
||||
unit := strings.ToUpper(matches[2])
|
||||
bytes := int64(number * float64(units[unit]))
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func ConfirmAction(promptText string) (bool, error) {
|
||||
for {
|
||||
input, err := GetUserInput(promptText)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if input == "" {
|
||||
log.Fatal("No input entered")
|
||||
return false, errors.New("invalid input. Please enter 'y' or 'n'")
|
||||
}
|
||||
if input == "c" {
|
||||
return false, errors.New("cancelled")
|
||||
}
|
||||
if input == "y" {
|
||||
return true, nil
|
||||
}
|
||||
if input == "n" {
|
||||
return false, nil
|
||||
}
|
||||
fmt.Println("Invalid input. Please enter 'y' or 'n'.")
|
||||
}
|
||||
}
|
||||
|
||||
// GetStorageSize prompts the user for a storage size and returns the size in bytes.
|
||||
func GetStorageSize(promptText string) (int64, error) {
|
||||
for {
|
||||
input, err := GetUserInput(promptText)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if input == "" {
|
||||
log.Fatal("No storage size entered")
|
||||
return 0, errors.New("no storage size entered")
|
||||
}
|
||||
if input == "c" {
|
||||
return 0, errors.New("storage size entry cancelled")
|
||||
}
|
||||
|
||||
bytes, err := parseStorageSize(input)
|
||||
if err != nil {
|
||||
fmt.Println("Invalid storage size format. Please use a valid format like '500MB', '2GB'.")
|
||||
continue
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetExportDir() string {
|
||||
for {
|
||||
exportDir, err := GetUserInput("Enter export directory")
|
||||
|
|
|
@ -86,7 +86,6 @@ func initConfig(cliConfigPath string) {
|
|||
viper.SetDefault("log.http", false)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
log.Printf("Config file not found; using defaults %s", cliConfigPath)
|
||||
} else {
|
||||
// Config file was found but another error was produced
|
||||
}
|
||||
|
|
104
cli/pkg/admin_actions.go
Normal file
104
cli/pkg/admin_actions.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal"
|
||||
"github.com/ente-io/cli/pkg/model"
|
||||
"github.com/ente-io/cli/utils"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *ClICtrl) GetUserId(ctx context.Context, params model.AdminActionForUser) error {
|
||||
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := c.Client.GetUserIdFromEmail(accountCtx, params.UserEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(id.User.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActionForUser) error {
|
||||
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userDetails, err := c.Client.GetUserIdFromEmail(accountCtx, params.UserEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storageSize, err := internal.GetStorageSize("Enter a storage size (e.g.'5MB', '10GB', '2Tb'): ")
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
dateStr, err := internal.GetUserInput("Enter sub expiry date in YYYY-MM-DD format (e.g.'2040-12-31')")
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
date, err := _parseDateOrDateTime(dateStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Updating storage for user %s to %s (old %s) with new expirty %s (old %s) \n",
|
||||
params.UserEmail,
|
||||
utils.ByteCountDecimalGIB(storageSize), utils.ByteCountDecimalGIB(userDetails.Subscription.Storage),
|
||||
date.Format("2006-01-02"),
|
||||
time.UnixMicro(userDetails.Subscription.ExpiryTime).Format("2006-01-02"))
|
||||
// press y to confirm
|
||||
confirmed, _ := internal.ConfirmAction("Are you sure you want to update the storage ('y' or 'n')?")
|
||||
if !confirmed {
|
||||
return nil
|
||||
} else {
|
||||
err := c.Client.UpdateFreePlanSub(accountCtx, userDetails, storageSize, date.UnixMicro())
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
fmt.Println("Successfully updated storage and expiry date for user")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) {
|
||||
accounts, err := c.GetAccounts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var acc *model.Account
|
||||
for _, a := range accounts {
|
||||
if a.Email == adminEmail {
|
||||
acc = &a
|
||||
break
|
||||
}
|
||||
}
|
||||
if acc == nil {
|
||||
return nil, fmt.Errorf("account not found for %s, use `account list` to list accounts", adminEmail)
|
||||
}
|
||||
secretInfo, err := c.KeyHolder.LoadSecrets(*acc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accountCtx := c.buildRequestContext(ctx, *acc)
|
||||
c.Client.AddToken(acc.AccountKey(), secretInfo.TokenStr())
|
||||
return accountCtx, nil
|
||||
}
|
||||
|
||||
func _parseDateOrDateTime(input string) (time.Time, error) {
|
||||
var layout string
|
||||
if strings.Contains(input, " ") {
|
||||
// If the input contains a space, assume it's a date-time format
|
||||
layout = "2006-01-02 15:04:05"
|
||||
} else {
|
||||
// If there's no space, assume it's just a date
|
||||
layout = "2006-01-02"
|
||||
}
|
||||
return time.Parse(layout, input)
|
||||
}
|
6
cli/pkg/model/admin.go
Normal file
6
cli/pkg/model/admin.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package model
|
||||
|
||||
type AdminActionForUser struct {
|
||||
UserEmail string
|
||||
AdminEmail string
|
||||
}
|
31
cli/utils/convert.go
Normal file
31
cli/utils/convert.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ByteCountDecimal(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
func ByteCountDecimalGIB(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
@ -10,16 +9,3 @@ func TimeTrack(start time.Time, name string) {
|
|||
elapsed := time.Since(start)
|
||||
log.Printf("%s took %s", name, elapsed)
|
||||
}
|
||||
|
||||
func ByteCountDecimal(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue