// Copyright (C) 2020 Storj Labs, Inc. // See LICENSE for copying information. package admin import ( "database/sql" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "github.com/gorilla/mux" "golang.org/x/crypto/bcrypt" "storj.io/common/memory" "storj.io/common/uuid" "storj.io/storj/satellite/console" ) func (server *Server) addUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() body, err := io.ReadAll(r.Body) if err != nil { sendJSONError(w, "failed to read body", err.Error(), http.StatusInternalServerError) return } var input console.CreateUser err = json.Unmarshal(body, &input) if err != nil { sendJSONError(w, "failed to unmarshal request", err.Error(), http.StatusBadRequest) return } user := console.CreateUser{ Email: input.Email, FullName: input.FullName, Password: input.Password, SignupPromoCode: input.SignupPromoCode, } err = user.IsValid() if err != nil { sendJSONError(w, "user data is not valid", err.Error(), http.StatusBadRequest) return } existingUser, err := server.db.Console().Users().GetByEmail(ctx, input.Email) if err != nil && !errors.Is(sql.ErrNoRows, err) { sendJSONError(w, "failed to check for user email", err.Error(), http.StatusInternalServerError) return } if existingUser != nil { sendJSONError(w, fmt.Sprintf("user with email already exists %s", input.Email), "", http.StatusConflict) return } hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), 0) if err != nil { sendJSONError(w, "unable to save password hash", "", http.StatusInternalServerError) return } userID, err := uuid.New() if err != nil { sendJSONError(w, "unable to create UUID", "", http.StatusInternalServerError) return } newUser, err := server.db.Console().Users().Insert(ctx, &console.User{ ID: userID, FullName: user.FullName, ShortName: user.ShortName, Email: user.Email, PasswordHash: hash, ProjectLimit: server.console.DefaultProjectLimit, ProjectStorageLimit: server.console.UsageLimits.Storage.Free.Int64(), ProjectBandwidthLimit: server.console.UsageLimits.Bandwidth.Free.Int64(), SignupPromoCode: user.SignupPromoCode, }) if err != nil { sendJSONError(w, "failed to insert user", err.Error(), http.StatusInternalServerError) return } _, err = server.payments.Setup(ctx, newUser.ID, newUser.Email, newUser.SignupPromoCode) if err != nil { sendJSONError(w, "failed to create payment account for user", err.Error(), http.StatusInternalServerError) return } // Set User Status to be activated, as we manually created it newUser.Status = console.Active err = server.db.Console().Users().Update(ctx, userID, console.UpdateUserRequest{ Status: &newUser.Status, }) if err != nil { sendJSONError(w, "failed to activate user", err.Error(), http.StatusInternalServerError) return } data, err := json.Marshal(newUser) if err != nil { sendJSONError(w, "json encoding failed", err.Error(), http.StatusInternalServerError) return } sendJSONData(w, http.StatusOK, data) } func (server *Server) userInfo(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } if err != nil { sendJSONError(w, "failed to get user", err.Error(), http.StatusInternalServerError) return } user.PasswordHash = nil projects, err := server.db.Console().Projects().GetOwn(ctx, user.ID) if err != nil { sendJSONError(w, "failed to get user projects", err.Error(), http.StatusInternalServerError) return } type User struct { ID uuid.UUID `json:"id"` FullName string `json:"fullName"` Email string `json:"email"` ProjectLimit int `json:"projectLimit"` } type Project struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Description string `json:"description"` OwnerID uuid.UUID `json:"ownerId"` } var output struct { User User `json:"user"` Projects []Project `json:"projects"` } output.User = User{ ID: user.ID, FullName: user.FullName, Email: user.Email, ProjectLimit: user.ProjectLimit, } for _, p := range projects { output.Projects = append(output.Projects, Project{ ID: p.ID, Name: p.Name, Description: p.Description, OwnerID: p.OwnerID, }) } data, err := json.Marshal(output) if err != nil { sendJSONError(w, "json encoding failed", err.Error(), http.StatusInternalServerError) return } sendJSONData(w, http.StatusOK, data) } func (server *Server) userLimits(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } if err != nil { sendJSONError(w, "failed to get user", err.Error(), http.StatusInternalServerError) return } user.PasswordHash = nil var limits struct { Storage int64 `json:"storage"` Bandwidth int64 `json:"bandwidth"` Segment int64 `json:"segment"` } limits.Storage = user.ProjectStorageLimit limits.Bandwidth = user.ProjectBandwidthLimit limits.Segment = user.ProjectSegmentLimit data, err := json.Marshal(limits) if err != nil { sendJSONError(w, "json encoding failed", err.Error(), http.StatusInternalServerError) return } sendJSONData(w, http.StatusOK, data) } func (server *Server) updateUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } if err != nil { sendJSONError(w, "failed to get user", err.Error(), http.StatusInternalServerError) return } body, err := io.ReadAll(r.Body) if err != nil { sendJSONError(w, "failed to read body", err.Error(), http.StatusInternalServerError) return } type UserWithPaidTier struct { console.User PaidTierStr string `json:"paidTierStr"` } var input UserWithPaidTier err = json.Unmarshal(body, &input) if err != nil { sendJSONError(w, "failed to unmarshal request", err.Error(), http.StatusBadRequest) return } updateRequest := console.UpdateUserRequest{} if input.FullName != "" { updateRequest.FullName = &input.FullName } if input.ShortName != "" { shortNamePtr := &input.ShortName updateRequest.ShortName = &shortNamePtr } if input.Email != "" { existingUser, err := server.db.Console().Users().GetByEmail(ctx, input.Email) if err != nil && !errors.Is(sql.ErrNoRows, err) { sendJSONError(w, "failed to check for user email", err.Error(), http.StatusInternalServerError) return } if existingUser != nil { sendJSONError(w, fmt.Sprintf("user with email already exists %s", input.Email), "", http.StatusConflict) return } updateRequest.Email = &input.Email } if len(input.PasswordHash) > 0 { updateRequest.PasswordHash = input.PasswordHash } if input.ProjectLimit > 0 { updateRequest.ProjectLimit = &input.ProjectLimit } if input.ProjectStorageLimit > 0 { updateRequest.ProjectStorageLimit = &input.ProjectStorageLimit } if input.ProjectBandwidthLimit > 0 { updateRequest.ProjectBandwidthLimit = &input.ProjectBandwidthLimit } if input.ProjectSegmentLimit > 0 { updateRequest.ProjectSegmentLimit = &input.ProjectSegmentLimit } if input.PaidTierStr != "" { status, err := strconv.ParseBool(input.PaidTierStr) if err != nil { sendJSONError(w, "failed to parse paid tier status", err.Error(), http.StatusBadRequest) return } updateRequest.PaidTier = &status } err = server.db.Console().Users().Update(ctx, user.ID, updateRequest) if err != nil { sendJSONError(w, "failed to update user", err.Error(), http.StatusInternalServerError) return } } // updateLimits updates user limits and all project limits for that user (future and existing). func (server *Server) updateLimits(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } if err != nil { sendJSONError(w, "failed to get user", err.Error(), http.StatusInternalServerError) return } body, err := io.ReadAll(r.Body) if err != nil { sendJSONError(w, "failed to read body", err.Error(), http.StatusInternalServerError) return } var input struct { Storage memory.Size `json:"storage"` Bandwidth memory.Size `json:"bandwidth"` Segment int64 `json:"segment"` } err = json.Unmarshal(body, &input) if err != nil { sendJSONError(w, "failed to unmarshal request", err.Error(), http.StatusBadRequest) return } newLimits := console.UsageLimits{ Storage: user.ProjectStorageLimit, Bandwidth: user.ProjectBandwidthLimit, Segment: user.ProjectSegmentLimit, } if input.Storage > 0 { newLimits.Storage = input.Storage.Int64() } if input.Bandwidth > 0 { newLimits.Bandwidth = input.Bandwidth.Int64() } if input.Segment > 0 { newLimits.Segment = input.Segment } if newLimits.Storage == user.ProjectStorageLimit && newLimits.Bandwidth == user.ProjectBandwidthLimit && newLimits.Segment == user.ProjectSegmentLimit { sendJSONError(w, "no limits to update", "new values are equal to old ones", http.StatusBadRequest) return } err = server.db.Console().Users().UpdateUserProjectLimits(ctx, user.ID, newLimits) if err != nil { sendJSONError(w, "failed to update user limits", err.Error(), http.StatusInternalServerError) return } userProjects, err := server.db.Console().Projects().GetOwn(ctx, user.ID) if err != nil { sendJSONError(w, "failed to get user's projects", err.Error(), http.StatusInternalServerError) return } for _, p := range userProjects { err = server.db.Console().Projects().UpdateUsageLimits(ctx, p.ID, newLimits) if err != nil { sendJSONError(w, "failed to update project limits", err.Error(), http.StatusInternalServerError) } } } func (server *Server) disableUserMFA(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } if err != nil { sendJSONError(w, "failed to get user details", err.Error(), http.StatusInternalServerError) return } user.MFAEnabled = false user.MFASecretKey = "" mfaSecretKeyPtr := &user.MFASecretKey var mfaRecoveryCodes []string err = server.db.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{ MFAEnabled: &user.MFAEnabled, MFASecretKey: &mfaSecretKeyPtr, MFARecoveryCodes: &mfaRecoveryCodes, }) if err != nil { sendJSONError(w, "failed to disable mfa", err.Error(), http.StatusInternalServerError) return } } func (server *Server) freezeUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } u, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if err != nil { if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } sendJSONError(w, "failed to get user details", err.Error(), http.StatusInternalServerError) return } err = server.freezeAccounts.FreezeUser(ctx, u.ID) if err != nil { sendJSONError(w, "failed to freeze user", err.Error(), http.StatusInternalServerError) } } func (server *Server) unfreezeUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } u, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if err != nil { if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } sendJSONError(w, "failed to get user details", err.Error(), http.StatusInternalServerError) return } if err = server.freezeAccounts.UnfreezeUser(ctx, u.ID); err != nil { sendJSONError(w, "failed to unfreeze user", err.Error(), http.StatusInternalServerError) return } } func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) userEmail, ok := vars["useremail"] if !ok { sendJSONError(w, "user-email missing", "", http.StatusBadRequest) return } user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) if errors.Is(err, sql.ErrNoRows) { sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), "", http.StatusNotFound) return } if err != nil { sendJSONError(w, "failed to get user details", err.Error(), http.StatusInternalServerError) return } // Ensure user has no own projects any longer projects, err := server.db.Console().Projects().GetByUserID(ctx, user.ID) if err != nil { sendJSONError(w, "unable to list projects", err.Error(), http.StatusInternalServerError) return } if len(projects) > 0 { sendJSONError(w, "some projects still exist", fmt.Sprintf("%v", projects), http.StatusConflict) return } // Delete memberships in foreign projects members, err := server.db.Console().ProjectMembers().GetByMemberID(ctx, user.ID) if err != nil { sendJSONError(w, "unable to search for user project memberships", err.Error(), http.StatusInternalServerError) return } if len(members) > 0 { for _, project := range members { err := server.db.Console().ProjectMembers().Delete(ctx, user.ID, project.ProjectID) if err != nil { sendJSONError(w, "unable to delete user project membership", err.Error(), http.StatusInternalServerError) return } } } // ensure no unpaid invoices exist. invoices, err := server.payments.Invoices().List(ctx, user.ID) if err != nil { sendJSONError(w, "unable to list user invoices", err.Error(), http.StatusInternalServerError) return } if len(invoices) > 0 { for _, invoice := range invoices { if invoice.Status == "draft" || invoice.Status == "open" { sendJSONError(w, "user has unpaid/pending invoices", "", http.StatusConflict) return } } } hasItems, err := server.payments.Invoices().CheckPendingItems(ctx, user.ID) if err != nil { sendJSONError(w, "unable to list pending invoice items", err.Error(), http.StatusInternalServerError) return } if hasItems { sendJSONError(w, "user has pending invoice items", "", http.StatusConflict) return } emptyName := "" emptyNamePtr := &emptyName deactivatedEmail := fmt.Sprintf("deactivated+%s@storj.io", user.ID.String()) status := console.Deleted err = server.db.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{ FullName: &emptyName, ShortName: &emptyNamePtr, Email: &deactivatedEmail, Status: &status, }) if err != nil { sendJSONError(w, "unable to delete user", err.Error(), http.StatusInternalServerError) return } err = server.payments.CreditCards().RemoveAll(ctx, user.ID) if err != nil { sendJSONError(w, "unable to delete credit card(s) from stripe account", err.Error(), http.StatusInternalServerError) } }