(w http.ResponseWriter, r *http.Request, params *EnrollFactorParams)
| 135 | } |
| 136 | |
| 137 | func (a *API) enrollPhoneFactor(w http.ResponseWriter, r *http.Request, params *EnrollFactorParams) error { |
| 138 | ctx := r.Context() |
| 139 | config := a.config |
| 140 | user := getUser(ctx) |
| 141 | session := getSession(ctx) |
| 142 | db := a.db.WithContext(ctx) |
| 143 | if params.Phone == "" { |
| 144 | return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Phone number required to enroll Phone factor") |
| 145 | } |
| 146 | |
| 147 | phone, err := validatePhone(params.Phone) |
| 148 | if err != nil { |
| 149 | return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Invalid phone number format (E.164 required)") |
| 150 | } |
| 151 | |
| 152 | var factorsToDelete []models.Factor |
| 153 | for _, factor := range user.Factors { |
| 154 | if factor.IsPhoneFactor() && factor.Phone.String() == phone { |
| 155 | if factor.IsVerified() { |
| 156 | return apierrors.NewUnprocessableEntityError( |
| 157 | apierrors.ErrorCodeMFAVerifiedFactorExists, |
| 158 | "A verified phone factor already exists, unenroll the existing factor to continue", |
| 159 | ) |
| 160 | } else if factor.IsUnverified() { |
| 161 | factorsToDelete = append(factorsToDelete, factor) |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | if err := db.Destroy(&factorsToDelete); err != nil { |
| 167 | return apierrors.NewInternalServerError("Database error deleting unverified phone factors").WithInternalError(err) |
| 168 | } |
| 169 | |
| 170 | if err := validateFactors(db, user, params.FriendlyName, a.config, session); err != nil { |
| 171 | return err |
| 172 | } |
| 173 | |
| 174 | factor := models.NewPhoneFactor(user, phone, params.FriendlyName) |
| 175 | err = db.Transaction(func(tx *storage.Connection) error { |
| 176 | if terr := tx.Create(factor); terr != nil { |
| 177 | return terr |
| 178 | } |
| 179 | if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.EnrollFactorAction, utilities.GetIPAddress(r), map[string]interface{}{ |
| 180 | "factor_id": factor.ID, |
| 181 | "factor_type": factor.FactorType, |
| 182 | }); terr != nil { |
| 183 | return terr |
| 184 | } |
| 185 | return nil |
| 186 | }) |
| 187 | if err != nil { |
| 188 | return err |
| 189 | } |
| 190 | return sendJSON(w, http.StatusOK, &EnrollFactorResponse{ |
| 191 | ID: factor.ID, |
| 192 | Type: models.Phone, |
| 193 | FriendlyName: factor.FriendlyName, |
| 194 | Phone: params.Phone, |
no test coverage detected