diff --git a/cli/.gitattributes b/cli/.gitattributes new file mode 100644 index 000000000..71965b858 --- /dev/null +++ b/cli/.gitattributes @@ -0,0 +1 @@ +docs/generated/*.md linguist-generated=true diff --git a/cli/cmd/admin.go b/cli/cmd/admin.go index 0e41bbfe2..8a2d7f006 100644 --- a/cli/cmd/admin.go +++ b/cli/cmd/admin.go @@ -29,6 +29,9 @@ var _userDetailsCmd = &cobra.Command{ flags.UserEmail = f.Value.String() } }) + if flags.UserEmail == "" { + return fmt.Errorf("user email is required") + } return ctrl.GetUserId(context.Background(), *flags) }, } @@ -47,14 +50,55 @@ var _disable2faCmd = &cobra.Command{ flags.UserEmail = f.Value.String() } }) - fmt.Println("Not supported yet") - return nil + if flags.UserEmail == "" { + return fmt.Errorf("user email is required") + } + return ctrl.Disable2FA(context.Background(), *flags) + + }, +} + +var _deleteUser = &cobra.Command{ + Use: "delete-user", + Short: "Delete 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() + } + }) + if flags.UserEmail == "" { + return fmt.Errorf("user email is required") + } + return ctrl.DeleteUser(context.Background(), *flags) + + }, +} + +var _listUsers = &cobra.Command{ + Use: "list-users", + Short: "List all users", + 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() + } + }) + return ctrl.ListUsers(context.Background(), *flags) }, } var _updateFreeUserStorage = &cobra.Command{ Use: "update-subscription", - Short: "Update subscription for the free user", + Short: "Update subscription for user", + Long: "Update subscription for the free user. If you want to apply specific limits, use the `--no-limit False` flag", RunE: func(cmd *cobra.Command, args []string) error { recoverWithLog() var flags = &model.AdminActionForUser{} @@ -70,6 +114,9 @@ var _updateFreeUserStorage = &cobra.Command{ noLimit = strings.ToLower(f.Value.String()) == "true" } }) + if flags.UserEmail == "" { + return fmt.Errorf("user email is required") + } return ctrl.UpdateFreeStorage(context.Background(), *flags, noLimit) }, } @@ -78,13 +125,16 @@ 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("admin-user", "a", "", "The email of the admin user. ") _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)") + _listUsers.Flags().StringP("admin-user", "a", "", "The email of the admin user. ") + _disable2faCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. ") _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)") + _deleteUser.Flags().StringP("admin-user", "a", "", "The email of the admin user. ") + _deleteUser.Flags().StringP("user", "u", "", "The email of the user to delete. (required)") + _updateFreeUserStorage.Flags().StringP("admin-user", "a", "", "The email of the admin user.") _updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)") // add a flag with no value --no-limit _updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years") - _adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _updateFreeUserStorage) + _adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _updateFreeUserStorage, _listUsers, _deleteUser) } diff --git a/cli/docs/generated/ente.md b/cli/docs/generated/ente.md index 6d0263ce4..b9d3cde17 100644 --- a/cli/docs/generated/ente.md +++ b/cli/docs/generated/ente.md @@ -25,4 +25,4 @@ ente [flags] * [ente export](ente_export.md) - Starts the export process * [ente version](ente_version.md) - Prints the current version -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_account.md b/cli/docs/generated/ente_account.md index ec26f9557..c48a65336 100644 --- a/cli/docs/generated/ente_account.md +++ b/cli/docs/generated/ente_account.md @@ -16,4 +16,4 @@ Manage account settings * [ente account list](ente_account_list.md) - list configured accounts * [ente account update](ente_account_update.md) - Update an existing account's export directory -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_account_add.md b/cli/docs/generated/ente_account_add.md index 74b2c23f9..1904ca370 100644 --- a/cli/docs/generated/ente_account_add.md +++ b/cli/docs/generated/ente_account_add.md @@ -16,4 +16,4 @@ ente account add [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_account_get-token.md b/cli/docs/generated/ente_account_get-token.md index 58ef3e7cd..d7ee77255 100644 --- a/cli/docs/generated/ente_account_get-token.md +++ b/cli/docs/generated/ente_account_get-token.md @@ -18,4 +18,4 @@ ente account get-token [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_account_list.md b/cli/docs/generated/ente_account_list.md index 3fc6fbc2e..cfc59bb8d 100644 --- a/cli/docs/generated/ente_account_list.md +++ b/cli/docs/generated/ente_account_list.md @@ -16,4 +16,4 @@ ente account list [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_account_update.md b/cli/docs/generated/ente_account_update.md index 04c4418e7..acb65412a 100644 --- a/cli/docs/generated/ente_account_update.md +++ b/cli/docs/generated/ente_account_update.md @@ -19,4 +19,4 @@ ente account update [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_admin.md b/cli/docs/generated/ente_admin.md index 91e70324d..aafe51b39 100644 --- a/cli/docs/generated/ente_admin.md +++ b/cli/docs/generated/ente_admin.md @@ -15,8 +15,10 @@ Commands for admin actions like disable or enabling 2fa, bumping up the storage ### SEE ALSO * [ente](ente.md) - CLI tool for exporting your photos from ente.io +* [ente admin delete-user](ente_admin_delete-user.md) - Delete a user * [ente admin disable-2fa](ente_admin_disable-2fa.md) - Disable 2fa for a user * [ente admin get-user-id](ente_admin_get-user-id.md) - Get user id -* [ente admin update-subscription](ente_admin_update-subscription.md) - Update subscription for the free user +* [ente admin list-users](ente_admin_list-users.md) - List all users +* [ente admin update-subscription](ente_admin_update-subscription.md) - Update subscription for user -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_admin_delete-user.md b/cli/docs/generated/ente_admin_delete-user.md new file mode 100644 index 000000000..56c96841e --- /dev/null +++ b/cli/docs/generated/ente_admin_delete-user.md @@ -0,0 +1,21 @@ +## ente admin delete-user + +Delete a user + +``` +ente admin delete-user [flags] +``` + +### Options + +``` + -a, --admin-user string The email of the admin user. + -h, --help help for delete-user + -u, --user string The email of the user to delete. (required) +``` + +### SEE ALSO + +* [ente admin](ente_admin.md) - Commands for admin actions + +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_admin_disable-2fa.md b/cli/docs/generated/ente_admin_disable-2fa.md index 19183fdfe..333f0912e 100644 --- a/cli/docs/generated/ente_admin_disable-2fa.md +++ b/cli/docs/generated/ente_admin_disable-2fa.md @@ -9,7 +9,7 @@ ente admin disable-2fa [flags] ### Options ``` - -a, --admin-user string The email of the admin user. (required) + -a, --admin-user string The email of the admin user. -h, --help help for disable-2fa -u, --user string The email of the user to disable 2FA for. (required) ``` @@ -18,4 +18,4 @@ ente admin disable-2fa [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_admin_get-user-id.md b/cli/docs/generated/ente_admin_get-user-id.md index 0151a3ec8..3d26f624a 100644 --- a/cli/docs/generated/ente_admin_get-user-id.md +++ b/cli/docs/generated/ente_admin_get-user-id.md @@ -9,7 +9,7 @@ ente admin get-user-id [flags] ### Options ``` - -a, --admin-user string The email of the admin user. (required) + -a, --admin-user string The email of the admin user. -h, --help help for get-user-id -u, --user string The email of the user to fetch details for. (required) ``` @@ -18,4 +18,4 @@ ente admin get-user-id [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_admin_list-users.md b/cli/docs/generated/ente_admin_list-users.md new file mode 100644 index 000000000..8841df57b --- /dev/null +++ b/cli/docs/generated/ente_admin_list-users.md @@ -0,0 +1,20 @@ +## ente admin list-users + +List all users + +``` +ente admin list-users [flags] +``` + +### Options + +``` + -a, --admin-user string The email of the admin user. + -h, --help help for list-users +``` + +### SEE ALSO + +* [ente admin](ente_admin.md) - Commands for admin actions + +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_admin_update-subscription.md b/cli/docs/generated/ente_admin_update-subscription.md index 30339acf2..cc1fa9623 100644 --- a/cli/docs/generated/ente_admin_update-subscription.md +++ b/cli/docs/generated/ente_admin_update-subscription.md @@ -1,6 +1,10 @@ ## ente admin update-subscription -Update subscription for the free user +Update subscription for user + +### Synopsis + +Update subscription for the free user. If you want to apply specific limits, use the `--no-limit False` flag ``` ente admin update-subscription [flags] @@ -9,7 +13,7 @@ ente admin update-subscription [flags] ### Options ``` - -a, --admin-user string The email of the admin user. (required) + -a, --admin-user string The email of the admin user. -h, --help help for update-subscription --no-limit string When true, sets 100TB as storage limit, and expiry to current date + 100 years (default "True") -u, --user string The email of the user to update subscription for. (required) @@ -19,4 +23,4 @@ ente admin update-subscription [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_auth.md b/cli/docs/generated/ente_auth.md index 4a64a944d..5770f36f3 100644 --- a/cli/docs/generated/ente_auth.md +++ b/cli/docs/generated/ente_auth.md @@ -13,4 +13,4 @@ Authenticator commands * [ente](ente.md) - CLI tool for exporting your photos from ente.io * [ente auth decrypt](ente_auth_decrypt.md) - Decrypt authenticator export -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_auth_decrypt.md b/cli/docs/generated/ente_auth_decrypt.md index 1203319e9..e573db2a3 100644 --- a/cli/docs/generated/ente_auth_decrypt.md +++ b/cli/docs/generated/ente_auth_decrypt.md @@ -16,4 +16,4 @@ ente auth decrypt [input] [output] [flags] * [ente auth](ente_auth.md) - Authenticator commands -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_export.md b/cli/docs/generated/ente_export.md index fb4cc6541..c5783236c 100644 --- a/cli/docs/generated/ente_export.md +++ b/cli/docs/generated/ente_export.md @@ -16,4 +16,4 @@ ente export [flags] * [ente](ente.md) - CLI tool for exporting your photos from ente.io -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/docs/generated/ente_version.md b/cli/docs/generated/ente_version.md index 0254e2ebd..b51055697 100644 --- a/cli/docs/generated/ente_version.md +++ b/cli/docs/generated/ente_version.md @@ -16,4 +16,4 @@ ente version [flags] * [ente](ente.md) - CLI tool for exporting your photos from ente.io -###### Auto generated by spf13/cobra on 13-Mar-2024 +###### Auto generated by spf13/cobra on 14-Mar-2024 diff --git a/cli/internal/api/admin.go b/cli/internal/api/admin.go index 1c0b2c50a..9e0bcb90a 100644 --- a/cli/internal/api/admin.go +++ b/cli/internal/api/admin.go @@ -25,6 +25,69 @@ func (c *Client) GetUserIdFromEmail(ctx context.Context, email string) (*models. } return &res, nil } + +func (c *Client) ListUsers(ctx context.Context) ([]models.User, error) { + var res struct { + Users []models.User `json:"users"` + } + r, err := c.restClient.R(). + SetContext(ctx). + SetQueryParam("sinceTime", "0"). + SetResult(&res). + Get("/admin/users/") + if err != nil { + return nil, err + } + if r.IsError() { + return nil, &ApiError{ + StatusCode: r.StatusCode(), + Message: r.String(), + } + } + return res.Users, nil +} + +func (c *Client) DeleteUser(ctx context.Context, email string) error { + + r, err := c.restClient.R(). + SetContext(ctx). + SetQueryParam("email", email). + Delete("/admin/user/delete") + if err != nil { + return err + } + if r.IsError() { + return &ApiError{ + StatusCode: r.StatusCode(), + Message: r.String(), + } + } + return nil +} + +func (c *Client) Disable2Fa(ctx context.Context, userID int64) error { + var res interface{} + + payload := map[string]interface{}{ + "userID": userID, + } + r, err := c.restClient.R(). + SetContext(ctx). + SetResult(&res). + SetBody(payload). + Post("/admin/user/disable-2fa") + if err != nil { + return err + } + if r.IsError() { + return &ApiError{ + StatusCode: r.StatusCode(), + Message: r.String(), + } + } + return nil +} + func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.UserDetails, storageInBytes int64, expiryTimeInMicro int64) error { var res interface{} if userDetails.Subscription.ProductID != "free" { diff --git a/cli/internal/api/models/user_details.go b/cli/internal/api/models/user_details.go index 259ff972b..6a5310d7d 100644 --- a/cli/internal/api/models/user_details.go +++ b/cli/internal/api/models/user_details.go @@ -1,9 +1,7 @@ package models type UserDetails struct { - User struct { - ID int64 `json:"id"` - } `json:"user"` + User User `json:"user"` Usage int64 `json:"usage"` Email string `json:"email"` @@ -14,3 +12,10 @@ type UserDetails struct { PaymentProvider string `json:"paymentProvider"` } `json:"subscription"` } + +type User struct { + ID int64 + Email string `json:"email"` + Hash string `json:"hash"` + CreationTime int64 `json:"creationTime"` +} diff --git a/cli/pkg/admin_actions.go b/cli/pkg/admin_actions.go index c9ec00667..0105cdc19 100644 --- a/cli/pkg/admin_actions.go +++ b/cli/pkg/admin_actions.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/ente-io/cli/internal" + "github.com/ente-io/cli/internal/api" "github.com/ente-io/cli/pkg/model" "github.com/ente-io/cli/utils" "log" @@ -24,6 +25,63 @@ func (c *ClICtrl) GetUserId(ctx context.Context, params model.AdminActionForUser return nil } +func (c *ClICtrl) ListUsers(ctx context.Context, params model.AdminActionForUser) error { + accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail) + if err != nil { + return err + } + users, err := c.Client.ListUsers(accountCtx) + if err != nil { + if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") { + fmt.Printf("Error: old admin token, please re-authenticate using `ente account add` \n") + return nil + } + return err + } + for _, user := range users { + fmt.Printf("Email: %s, ID: %d, Created: %s\n", user.Email, user.ID, time.UnixMicro(user.CreationTime).Format("2006-01-02")) + } + return nil +} + +func (c *ClICtrl) DeleteUser(ctx context.Context, params model.AdminActionForUser) error { + accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail) + if err != nil { + return err + } + err = c.Client.DeleteUser(accountCtx, params.UserEmail) + if err != nil { + if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") { + fmt.Printf("Error: old admin token, please re-authenticate using `ente account add` \n") + return nil + } + return err + } + fmt.Println("Successfully deleted user") + return nil +} + +func (c *ClICtrl) Disable2FA(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 + } + err = c.Client.Disable2Fa(accountCtx, userDetails.User.ID) + if err != nil { + if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") { + fmt.Printf("Error: Old admin token, please re-authenticate using `ente account add` \n") + return nil + } + return err + } + fmt.Println("Successfully disabled 2FA for user") + return nil +} + func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActionForUser, noLimit bool) error { accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail) if err != nil { @@ -82,6 +140,9 @@ func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (con if err != nil { return nil, err } + if len(accounts) == 0 { + return nil, fmt.Errorf("no accounts found, use `account add` to add an account") + } var acc *model.Account for _, a := range accounts { if a.Email == adminEmail { @@ -89,6 +150,14 @@ func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (con break } } + if (len(accounts) > 1) && (acc == nil) { + return nil, fmt.Errorf("multiple accounts found, specify the admin email using --admin-user") + } + if acc == nil && len(accounts) == 1 { + acc = &accounts[0] + fmt.Printf("Assuming %s as the Admin \n------------\n", acc.Email) + } + if acc == nil { return nil, fmt.Errorf("account not found for %s, use `account list` to list accounts", adminEmail) }