Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions internal/api/otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ func (a *API) Otp(w http.ResponseWriter, r *http.Request) error {
params.Data = make(map[string]interface{})
}

// 🔥 Fix: If session user exists and email is provided, treat as email change request
if currentUser := getUserFromContext(r.Context()); currentUser != nil {
if params.Email != "" && !params.CreateUser {
return a.startEmailChangeVerification(currentUser, params.Email)
}
}

if ok, err := a.shouldCreateUser(r, params); !ok {
return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeOTPDisabled, "Signups not allowed for otp")
} else if err != nil {
Expand Down Expand Up @@ -236,3 +243,25 @@ func (a *API) shouldCreateUser(r *http.Request, params *OtpParams) (bool, error)
}
return true, nil
}

// startEmailChangeVerification initiates email change confirmation flow for phone-first users
func (a *API) startEmailChangeVerification(user *models.User, newEmail string) error {
db := a.db.WithContext(user.Context())
config := a.config

normalizedEmail, err := a.validateEmail(newEmail)
if err != nil {
return err
}

// Generate confirmation token for email change
if err := user.GenerateEmailChange(db, normalizedEmail, config.Security.EmailMaxFrequency); err != nil {
return apierrors.NewInternalServerError("Could not generate email change token").WithInternalError(err)
}

if err := a.sendEmailChange(db, user, normalizedEmail); err != nil {
return err
}

return nil
}
25 changes: 24 additions & 1 deletion internal/api/otp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,31 @@ func TestOtp(t *testing.T) {

func (ts *OtpTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
}

// ✅ New test for attaching email to phone-first user
func (ts *OtpTestSuite) TestAttachEmailToPhoneUser() {
// Create a phone-only user
user := &models.User{Phone: "1234567890"}
require.NoError(ts.T(), ts.API.db.Create(user))

// Simulate logged-in request with this user
token := ts.API.createAccessTokenForUser(user)
body := map[string]interface{}{
"email": "[email protected]",
"create_user": false,
}
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(body))

req := httptest.NewRequest(http.MethodPost, "/otp", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)

w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)

require.Equal(ts.T(), http.StatusOK, w.Code)
}

func (ts *OtpTestSuite) TestOtpPKCE() {
Expand Down Expand Up @@ -139,7 +163,6 @@ func (ts *OtpTestSuite) TestOtpPKCE() {
require.Equal(ts.T(), c.expected.code, w.Code)
data := make(map[string]interface{})
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))

})
}
}
Expand Down