FIX (sign up): Return authorization token on sign up to avoid 2-step sign up

This commit is contained in:
Rostislav Dugin
2026-02-15 00:08:01 +03:00
parent c70ad82c95
commit 7b05bd8000
6 changed files with 61 additions and 45 deletions

View File

@@ -726,41 +726,28 @@ func Test_InviteUserToWorkspace_MembershipReceivedAfterSignUp(t *testing.T) {
assert.Equal(t, workspaces_dto.AddStatusInvited, inviteResponse.Status)
// 3. Sign up the invited user
// 3. Sign up the invited user (now returns token directly)
signUpRequest := users_dto.SignUpRequestDTO{
Email: inviteEmail,
Password: "testpassword123",
Name: "Invited User",
}
resp := test_utils.MakePostRequest(
var signInResponse users_dto.SignInResponseDTO
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/users/signup",
"",
signUpRequest,
http.StatusOK,
)
assert.Contains(t, string(resp.Body), "User created successfully")
// 4. Sign in the newly registered user
signInRequest := users_dto.SignInRequestDTO{
Email: inviteEmail,
Password: "testpassword123",
}
var signInResponse users_dto.SignInResponseDTO
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/users/signin",
"",
signInRequest,
http.StatusOK,
&signInResponse,
)
// 5. Verify user is automatically added as member to workspace
assert.NotEmpty(t, signInResponse.Token)
assert.Equal(t, inviteEmail, signInResponse.Email)
// 4. Verify user is automatically added as member to workspace
var membersResponse workspaces_dto.GetMembersResponseDTO
test_utils.MakeGetRequestAndUnmarshal(
t,

View File

@@ -52,7 +52,7 @@ func (c *UserController) RegisterProtectedRoutes(router *gin.RouterGroup) {
// @Accept json
// @Produce json
// @Param request body users_dto.SignUpRequestDTO true "User signup data"
// @Success 200
// @Success 200 {object} users_dto.SignInResponseDTO
// @Failure 400
// @Router /users/signup [post]
func (c *UserController) SignUp(ctx *gin.Context) {
@@ -84,13 +84,19 @@ func (c *UserController) SignUp(ctx *gin.Context) {
}
}
err := c.userService.SignUp(&request)
user, err := c.userService.SignUp(&request)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
response, err := c.userService.GenerateAccessToken(user)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
ctx.JSON(http.StatusOK, response)
}
// SignIn

View File

@@ -27,7 +27,20 @@ func Test_SignUpUser_WithValidData_UserCreated(t *testing.T) {
Name: "Test User",
}
test_utils.MakePostRequest(t, router, "/api/v1/users/signup", "", request, http.StatusOK)
var response users_dto.SignInResponseDTO
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/users/signup",
"",
request,
http.StatusOK,
&response,
)
assert.NotEmpty(t, response.Token)
assert.NotEqual(t, uuid.Nil, response.UserID)
assert.Equal(t, request.Email, response.Email)
}
func Test_SignUpUser_WithInvalidJSON_ReturnsBadRequest(t *testing.T) {

View File

@@ -44,19 +44,19 @@ func (s *UserService) SetEmailSender(sender users_interfaces.EmailSender) {
s.emailSender = sender
}
func (s *UserService) SignUp(request *users_dto.SignUpRequestDTO) error {
func (s *UserService) SignUp(request *users_dto.SignUpRequestDTO) (*users_models.User, error) {
existingUser, err := s.userRepository.GetUserByEmail(request.Email)
if err != nil {
return fmt.Errorf("failed to check existing user: %w", err)
return nil, fmt.Errorf("failed to check existing user: %w", err)
}
if existingUser != nil && existingUser.Status != users_enums.UserStatusInvited {
return errors.New("user with this email already exists")
return nil, errors.New("user with this email already exists")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
return nil, fmt.Errorf("failed to hash password: %w", err)
}
hashedPasswordStr := string(hashedPassword)
@@ -67,39 +67,45 @@ func (s *UserService) SignUp(request *users_dto.SignUpRequestDTO) error {
existingUser.ID,
hashedPasswordStr,
); err != nil {
return fmt.Errorf("failed to set password: %w", err)
return nil, fmt.Errorf("failed to set password: %w", err)
}
if err := s.userRepository.UpdateUserStatus(
existingUser.ID,
users_enums.UserStatusActive,
); err != nil {
return fmt.Errorf("failed to activate user: %w", err)
return nil, fmt.Errorf("failed to activate user: %w", err)
}
name := request.Name
if err := s.userRepository.UpdateUserInfo(existingUser.ID, &name, nil); err != nil {
return fmt.Errorf("failed to update name: %w", err)
return nil, fmt.Errorf("failed to update name: %w", err)
}
// Fetch updated user to ensure we have the latest data
updatedUser, err := s.userRepository.GetUserByID(existingUser.ID)
if err != nil {
return nil, fmt.Errorf("failed to get updated user: %w", err)
}
s.auditLogWriter.WriteAuditLog(
fmt.Sprintf("Invited user completed registration: %s", existingUser.Email),
&existingUser.ID,
fmt.Sprintf("Invited user completed registration: %s", updatedUser.Email),
&updatedUser.ID,
nil,
)
return nil
return updatedUser, nil
}
// Get settings to check registration policy for new users
settings, err := s.settingsService.GetSettings()
if err != nil {
return fmt.Errorf("failed to get settings: %w", err)
return nil, fmt.Errorf("failed to get settings: %w", err)
}
// Check if external registrations are allowed
if !settings.IsAllowExternalRegistrations {
return errors.New("external registration is disabled")
return nil, errors.New("external registration is disabled")
}
user := &users_models.User{
@@ -114,7 +120,7 @@ func (s *UserService) SignUp(request *users_dto.SignUpRequestDTO) error {
}
if err := s.userRepository.CreateUser(user); err != nil {
return fmt.Errorf("failed to create user: %w", err)
return nil, fmt.Errorf("failed to create user: %w", err)
}
s.auditLogWriter.WriteAuditLog(
@@ -123,7 +129,7 @@ func (s *UserService) SignUp(request *users_dto.SignUpRequestDTO) error {
nil,
)
return nil
return user, nil
}
func (s *UserService) SignIn(
@@ -258,6 +264,7 @@ func (s *UserService) GenerateAccessToken(
return &users_dto.SignInResponseDTO{
UserID: user.ID,
Email: user.Email,
Token: tokenString,
}, nil
}

View File

@@ -31,10 +31,18 @@ const notifyAuthListeners = () => {
};
export const userApi = {
async signUp(signUpRequest: SignUpRequest) {
async signUp(signUpRequest: SignUpRequest): Promise<SignInResponse> {
const requestOptions: RequestOptions = new RequestOptions();
requestOptions.setBody(JSON.stringify(signUpRequest));
return apiHelper.fetchPostRaw(`${getApplicationServer()}/api/v1/users/signup`, requestOptions);
return apiHelper
.fetchPostJson(`${getApplicationServer()}/api/v1/users/signup`, requestOptions)
.then((response: unknown): SignInResponse => {
const typedResponse = response as SignInResponse;
saveAuthorizedData(typedResponse.token, typedResponse.userId);
notifyAuthListeners();
return typedResponse;
});
},
async signIn(signInRequest: SignInRequest): Promise<SignInResponse> {

View File

@@ -92,11 +92,6 @@ export function SignUpComponent({ onSwitchToSignIn }: SignUpComponentProps): JSX
name,
cloudflareTurnstileToken: token,
});
await userApi.signIn({
email,
password,
cloudflareTurnstileToken: token,
});
} catch (e) {
setSignUpError(StringUtils.capitalizeFirstLetter((e as Error).message));
resetCloudflareTurnstile();