From 760de35e10ec83439d4e1f4945fb33fb7f65f931 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Thu, 17 Sep 2026 13:45:57 +0800 Subject: [PATCH] fix: allow username-only registration without invite --- server/session_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++ server/user.go | 2 +- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/server/session_test.go b/server/session_test.go index fde068f..3105016 100644 --- a/server/session_test.go +++ b/server/session_test.go @@ -1443,6 +1443,95 @@ func TestDispatchAccNew(t *testing.T) { } } +func TestDispatchAccNewUsernameOnlyDoesNotRequireInvite(t *testing.T) { + ctrl := gomock.NewController(t) + ss := mock_store.NewMockPersistentStorageInterface(ctrl) + uu := mock_store.NewMockUsersPersistenceInterface(ctrl) + aa := mock_auth.NewMockAuthHandler(ctrl) + + uid := types.Uid(2) + oldSecurity := store.Security + oldInvites := store.RegistrationInvites + oldAuthValidators := globals.authValidators + store.Store = ss + store.Users = uu + store.Security = &testSecurityStore{version: 1} + store.RegistrationInvites = nil + globals.authValidators = map[auth.Level][]string{} + setRegistrationSMSConfigForTest(true) + defer func() { + store.Store = nil + store.Users = nil + store.Security = oldSecurity + store.RegistrationInvites = oldInvites + globals.authValidators = oldAuthValidators + resetRegistrationSMSConfigForTest() + ctrl.Finish() + }() + + remoteAddr := "192.168.0.1" + secret := "alice:password" + authRec := &auth.Rec{ + TenantID: 1, + Uid: uid, + AuthLevel: auth.LevelAuth, + State: types.StateOK, + } + ss.EXPECT().GetLogicalAuthHandler("basic").Return(aa) + aa.EXPECT().IsUnique(auth.AuthContext{TenantID: 1, RemoteAddr: remoteAddr}, []byte(secret)).Return(true, nil) + uu.EXPECT().Create(types.TenantID(1), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ types.TenantID, user *types.User, private any) (*types.User, error) { + if user.Username != "alice" || user.Phone != "" { + t.Fatalf("unexpected registration user: username=%q phone=%q", user.Username, user.Phone) + } + user.SetUid(uid) + return user, nil + }) + aa.EXPECT().AddRecord(auth.AuthContext{TenantID: 1, RemoteAddr: remoteAddr}, gomock.Any(), + []byte(secret)).Return(authRec, nil) + ss.EXPECT().GetLogicalAuthHandler("token").Return(aa) + aa.EXPECT().GenSecret(gomock.Any()).Return([]byte("<==auth-token==>"), time.Now(), nil) + + s := &Session{ + send: make(chan any, 10), + authLvl: auth.LevelAuth, + ver: 16, + tenantID: 1, + tenantCode: "test", + remoteAddr: remoteAddr, + } + wg := sync.WaitGroup{} + r := responses{} + wg.Add(1) + go s.testWriteLoop(&r, &wg) + + msg := &ClientComMessage{ + Acc: &MsgClientAcc{ + Id: "username-only", + User: "new", + Scheme: "basic", + Username: "alice", + Secret: []byte(secret), + Desc: &MsgSetDesc{Public: map[string]any{"fn": "Alice"}}, + }, + } + + s.dispatch(msg) + close(s.send) + wg.Wait() + + if len(r.messages) != 1 { + t.Fatalf("responses: expected 1, received %d", len(r.messages)) + } + resp := r.messages[0].(*ServerComMessage) + if resp.Ctrl == nil || resp.Ctrl.Code != 201 { + t.Fatalf("expected successful username registration, got %#v", resp.Ctrl) + } + if username := resp.Ctrl.Params.(map[string]any)["username"].(string); username != "alice" { + t.Fatalf("username: got %q", username) + } +} + func TestDispatchNoMessage(t *testing.T) { remoteAddr := "192.168.0.1" s := &Session{ diff --git a/server/user.go b/server/user.go index 789bd4f..138717f 100644 --- a/server/user.go +++ b/server/user.go @@ -179,7 +179,7 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) { map[string]any{"creds": []string{"tel"}, "reason": "PHONE_VERIFICATION_REQUIRED"})) return } - inviteRequired := !phoneRegistration || (!smsEnabled && !registrationTelCredential) + inviteRequired := phoneRegistration && !smsEnabled && !registrationTelCredential if inviteRequired && inviteCode == "" { s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp, map[string]any{"what": "invite_code", "reason": "INVITE_CODE_REQUIRED"}))