This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6402-22e21c4ac179965f4b40b86b0c0a550c1879f5a1 in repository https://gitbox.apache.org/repos/asf/texera.git
commit bf25dd2d47ab7ea7bcbd4bb7b7e9e1f41ec195eb Author: Mrudhulraj <[email protected]> AuthorDate: Wed Jul 29 17:39:09 2026 -0700 fix(user): Add user email as part of registration in authentication (#6402) <!-- Thanks for sending a pull request (PR)! Here are some tips for you: 1. If this is your first time, please read our contributor guidelines: [Contributing to Texera](https://github.com/apache/texera/blob/main/CONTRIBUTING.md) 2. Ensure you have added or run the appropriate tests for your PR 3. If the PR is work in progress, mark it a draft on GitHub. 4. Please write your PR title to summarize what this PR proposes, we are following Conventional Commits style for PR titles as well. 5. Be sure to keep the PR description updated to reflect all changes. --> ### What changes were proposed in this PR? PR fixes a bug local-account registration where the user's email was silently being dropped. Two layers were silently dropping the email: | Layer | Before | After | | --- | --- | --- | | Frontend Sign Up form | No email input | New required email input with format validation | | `auth.service.ts → POST /auth/register` | Sent `{ username, password }` | Sends `{ username, email, password }` | | `UserRegistrationRequest` (Scala case class) | `(username, password)` | `(username, email, password)` | | `AuthResource.register` | `user.setEmail(username)` | `user.setEmail(request.email)` | Before, every locally-registered user had `email = username` in the database, because the request DTO had no `email` field and the resource fell back to `setEmail(username)`. After this PR, the user-entered email is what lands in the `user.email` column. Frontend-side, the new `registerEmail` form control uses `[Validators.required, Validators.email]`. A new `UserService.validateEmail` does a defensive non-empty + regex check before the request is sent. A new `#emailErrorTip` template renders "Please input your email." messages on the Sign Up tab. Before-After user registration: UI changes before: <img width="1500" height="800" alt="sign up page" src="https://github.com/user-attachments/assets/d8b0b2e4-117b-4bb4-8a28-162155358b6c" /> After entering the user-email as part of the sign-up in the forms: <img width="1500" height="800" alt="User Registration post fix" src="https://github.com/user-attachments/assets/b8cff507-d907-47b8-af95-3d3e8c32d046" /> `newUser` Registered with email post-fix; `testUser` (pre-fix) was registered without an email and the email field is filled with the Username : <img width="1913" height="286" alt="admin page post fix" src="https://github.com/user-attachments/assets/e3dca870-bf72-4e9b-a23f-46e64f8870ca" /> ### Any related issues, documentation, discussions? Fixes #6559 ### How was this PR tested? Unit tests (frontend, `local-login.component.spec.ts`): - `requires registerEmail and enforces email format` — covers the `Validators.required` and `Validators.email` behavior on `registerEmail`. - `register › sets registerErrorMessage when the email is empty` - `register › sets registerErrorMessage when the email is malformed` ### Was this PR authored or co-authored using generative AI tooling? <!-- Choose one and delete the other. Per ASF guidance, if generative AI tooling helped author this PR, include `Generated-by: <tool> <version>`. TODO: replace with one of the two lines below. --> Yes for frontend forms and testing. Backend was fixed manually. Co-authored-by: Yicong Huang <[email protected]> --- .../request/auth/UserRegistrationRequest.scala | 2 +- .../texera/web/resource/auth/AuthResource.scala | 34 ++++++--- bin/single-node/examples/load-examples.sh | 5 +- .../app/common/service/user/auth.service.spec.ts | 12 ++- .../src/app/common/service/user/auth.service.ts | 4 +- .../app/common/service/user/stub-auth.service.ts | 2 +- .../app/common/service/user/stub-user.service.ts | 2 +- .../app/common/service/user/user.service.spec.ts | 4 +- .../src/app/common/service/user/user.service.ts | 22 +++++- .../about/local-login/local-login.component.html | 17 +++++ .../local-login/local-login.component.spec.ts | 89 +++++++++++++++++++++- .../about/local-login/local-login.component.ts | 19 +++-- 12 files changed, 180 insertions(+), 32 deletions(-) diff --git a/amber/src/main/scala/org/apache/texera/web/model/http/request/auth/UserRegistrationRequest.scala b/amber/src/main/scala/org/apache/texera/web/model/http/request/auth/UserRegistrationRequest.scala index f28b27c87d..a36646d1b2 100644 --- a/amber/src/main/scala/org/apache/texera/web/model/http/request/auth/UserRegistrationRequest.scala +++ b/amber/src/main/scala/org/apache/texera/web/model/http/request/auth/UserRegistrationRequest.scala @@ -19,4 +19,4 @@ package org.apache.texera.web.model.http.request.auth -case class UserRegistrationRequest(username: String, password: String) +case class UserRegistrationRequest(username: String, email: String, password: String) diff --git a/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala index 7739c4baa0..277180d2d5 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala @@ -102,22 +102,36 @@ class AuthResource { @POST @Path("/register") def register(request: UserRegistrationRequest): TokenIssueResponse = { - val username = request.username - if (username == null) throw new NotAcceptableException("Username cannot be null.") - if (username.trim.isEmpty) throw new NotAcceptableException("Username cannot be empty.") - userDao.fetchByName(username).size() match { - case 0 => + val username = Option(request.username).getOrElse("").trim + val useremail = Option(request.email).getOrElse("").trim + val userpassword = request.password + if (username.isEmpty) + throw new NotAcceptableException("Username cannot be empty") + if (useremail.isEmpty) + throw new NotAcceptableException("Email cannot be empty") + if (!useremail.matches("""^[^\s@]+@[^\s@]+\.[^\s@]+$""")) + throw new NotAcceptableException("Email format is invalid.") + if (userpassword == null || userpassword.isEmpty) + throw new NotAcceptableException("Password cannot be empty") + + // Check if email already exists + val usernameExists = !userDao.fetchByName(username).isEmpty + val emailExists = userDao.fetchOneByEmail(useremail) != null + + (usernameExists, emailExists) match { + case (true, _) => + throw new NotAcceptableException("Username exists already.") + case (_, true) => + throw new NotAcceptableException("Email exists already.") + case (false, false) => val user = new User user.setName(username) - user.setEmail(username) + user.setEmail(useremail) user.setRole(UserRoleEnum.RESTRICTED) // hash the plain text password - user.setPassword(new StrongPasswordEncryptor().encryptPassword(request.password)) + user.setPassword(new StrongPasswordEncryptor().encryptPassword(userpassword)) userDao.insert(user) TokenIssueResponse(jwtToken(jwtClaims(user, TOKEN_EXPIRE_TIME_IN_MINUTES))) - case _ => - // the username exists already - throw new NotAcceptableException("Username exists already.") } } diff --git a/bin/single-node/examples/load-examples.sh b/bin/single-node/examples/load-examples.sh index 899f78c4bb..e6452d1152 100755 --- a/bin/single-node/examples/load-examples.sh +++ b/bin/single-node/examples/load-examples.sh @@ -27,8 +27,7 @@ TEXERA_DASHBOARD_SERVICE_URL=${TEXERA_DASHBOARD_SERVICE_URL:-"http://dashboard-s TEXERA_FILE_SERVICE_URL=${TEXERA_FILE_SERVICE_URL:-"http://file-service:9092/api"} USERNAME=${TEXERA_EXAMPLE_USERNAME:-"texera"} PASSWORD=${TEXERA_EXAMPLE_PASSWORD:-"texera"} -# In Texera, registration sets email = username -OWNER_EMAIL="$USERNAME" +OWNER_EMAIL="[email protected]" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" DATASET_DIR="$SCRIPT_DIR/datasets" @@ -93,7 +92,7 @@ authenticate() { print_status "User doesn't exist, attempting to register..." REGISTER_RESPONSE=$(curl -s -X POST "$TEXERA_DASHBOARD_SERVICE_URL/auth/register" \ -H "Content-Type: application/json" \ - -d "{\"username\": \"$USERNAME\", \"password\": \"$PASSWORD\"}") + -d "{\"username\": \"$USERNAME\", \"email\": \"$OWNER_EMAIL\", \"password\": \"$PASSWORD\"}") if echo "$REGISTER_RESPONSE" | grep -q '"accessToken"'; then TOKEN=$(echo "$REGISTER_RESPONSE" | grep -o '"accessToken":"[^"]*' | cut -d'"' -f4) diff --git a/frontend/src/app/common/service/user/auth.service.spec.ts b/frontend/src/app/common/service/user/auth.service.spec.ts index 922894c0f9..2901a1a084 100644 --- a/frontend/src/app/common/service/user/auth.service.spec.ts +++ b/frontend/src/app/common/service/user/auth.service.spec.ts @@ -104,11 +104,11 @@ describe("AuthService", () => { }); describe("HTTP auth endpoints", () => { - it("register() POSTs username/password to the register endpoint", () => { - service.register("alice", "pw").subscribe(); + it("register() POSTs username/email/password to the register endpoint", () => { + service.register("alice", "[email protected]", "pw").subscribe(); const req = httpMock.expectOne(`${api}/${AuthService.REGISTER_ENDPOINT}`); expect(req.request.method).toEqual("POST"); - expect(req.request.body).toEqual({ username: "alice", password: "pw" }); + expect(req.request.body).toEqual({ username: "alice", email: "[email protected]", password: "pw" }); req.flush({ accessToken: "t" }); }); @@ -130,7 +130,11 @@ describe("AuthService", () => { }); const errorCases = [ - { name: "register", call: () => service.register("alice", "pw"), endpoint: AuthService.REGISTER_ENDPOINT }, + { + name: "register", + call: () => service.register("alice", "[email protected]", "pw"), + endpoint: AuthService.REGISTER_ENDPOINT, + }, { name: "auth", call: () => service.auth("alice", "pw"), endpoint: AuthService.LOGIN_ENDPOINT }, { name: "googleAuth", call: () => service.googleAuth("cred"), endpoint: AuthService.GOOGLE_LOGIN_ENDPOINT }, ]; diff --git a/frontend/src/app/common/service/user/auth.service.ts b/frontend/src/app/common/service/user/auth.service.ts index 9c9d0587cc..a99508394e 100644 --- a/frontend/src/app/common/service/user/auth.service.ts +++ b/frontend/src/app/common/service/user/auth.service.ts @@ -62,13 +62,15 @@ export class AuthService { * This method will handle the request for user registration. * It will automatically login, save the user account inside and trigger userChangeEvent when success * @param username + * @param email * @param password */ - public register(username: string, password: string): Observable<Readonly<{ accessToken: string }>> { + public register(username: string, email: string, password: string): Observable<Readonly<{ accessToken: string }>> { return this.http.post<Readonly<{ accessToken: string }>>( `${AppSettings.getApiEndpoint()}/${AuthService.REGISTER_ENDPOINT}`, { username, + email, password, } ); diff --git a/frontend/src/app/common/service/user/stub-auth.service.ts b/frontend/src/app/common/service/user/stub-auth.service.ts index 2bb5f91534..8557b2fcbe 100644 --- a/frontend/src/app/common/service/user/stub-auth.service.ts +++ b/frontend/src/app/common/service/user/stub-auth.service.ts @@ -65,7 +65,7 @@ export class StubAuthService implements PublicInterfaceOf<AuthService> { return undefined; } - register(username: string, password: string): Observable<Readonly<{ accessToken: string }>> { + register(username: string, email: string, password: string): Observable<Readonly<{ accessToken: string }>> { if (username !== "existing_user") { return of(MOCK_TOKEN); } else { diff --git a/frontend/src/app/common/service/user/stub-user.service.ts b/frontend/src/app/common/service/user/stub-user.service.ts index fba46bd007..3f2fc82a23 100644 --- a/frontend/src/app/common/service/user/stub-user.service.ts +++ b/frontend/src/app/common/service/user/stub-user.service.ts @@ -71,7 +71,7 @@ export class StubUserService implements PublicInterfaceOf<UserService> { logout(): void {} - register(username: string, password: string): Observable<void> { + register(username: string, email: string, password: string): Observable<void> { return of(); } diff --git a/frontend/src/app/common/service/user/user.service.spec.ts b/frontend/src/app/common/service/user/user.service.spec.ts index b1e2afe35f..f4792d4e65 100644 --- a/frontend/src/app/common/service/user/user.service.spec.ts +++ b/frontend/src/app/common/service/user/user.service.spec.ts @@ -59,7 +59,7 @@ describe("UserService", () => { .userChanged() .pipe(skip(1)) .subscribe(user => expect(user).toBeTruthy()); - service.register("test", "password").subscribe(() => { + service.register("test", "[email protected]", "password").subscribe(() => { expect((service as any).currentUser).toBeTruthy(); }); }); @@ -81,7 +81,7 @@ describe("UserService", () => { .userChanged() .pipe(skip(1)) .subscribe(user => expect(user).toBeFalsy()); - service.register("existing_user", "password").subscribe(() => { + service.register("existing_user", "[email protected]", "password").subscribe(() => { expect((service as any).currentUser).toBeFalsy(); }); }); diff --git a/frontend/src/app/common/service/user/user.service.ts b/frontend/src/app/common/service/user/user.service.ts index 9a55df6487..2905c2e892 100644 --- a/frontend/src/app/common/service/user/user.service.ts +++ b/frontend/src/app/common/service/user/user.service.ts @@ -81,9 +81,9 @@ export class UserService { this.changeUser(undefined); } - public register(username: string, password: string): Observable<void> { + public register(username: string, email: string, password: string): Observable<void> { return this.authService - .register(username, password) + .register(username, email, password) .pipe(switchMap(({ accessToken }) => this.handleAccessToken(accessToken))); } @@ -134,6 +134,24 @@ export class UserService { return { result: true, message: "Username frontend validation success." }; } + /** + * check the given parameter is a syntactically valid email address for registration + * @param email + */ + static validateEmail(email: string): { result: boolean; message: string } { + const trimmed = (email ?? "").trim(); + if (trimmed.length === 0) { + return { result: false, message: "Email should not be empty." }; + } + // Pragmatic email regex: non-whitespace + @ + non-whitespace + . + non-whitespace. + // Matches what most users expect; we leave authoritative validation to the backend. + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(trimmed)) { + return { result: false, message: "Email format is invalid." }; + } + return { result: true, message: "Email frontend validation success." }; + } + getAvatar(googleAvatar: string): Observable<string | undefined> { if (!googleAvatar) return of(undefined); diff --git a/frontend/src/app/hub/component/about/local-login/local-login.component.html b/frontend/src/app/hub/component/about/local-login/local-login.component.html index 9820fc3fe0..4234ab8520 100644 --- a/frontend/src/app/hub/component/about/local-login/local-login.component.html +++ b/frontend/src/app/hub/component/about/local-login/local-login.component.html @@ -34,6 +34,12 @@ <ng-container *ngIf="control.hasError('required')">Please confirm your password. </ng-container> <ng-container *ngIf="control.hasError('confirm')">Two passwords are inconsistent. </ng-container> </ng-template> + <ng-template + #emailErrorTip + let-control> + <ng-container *ngIf="control.hasError('required')">Please input your email. </ng-container> + <ng-container *ngIf="control.hasError('email')">Please input a valid email. </ng-container> + </ng-template> <nz-tab nzTitle="Sign In"> <form @@ -94,6 +100,17 @@ </nz-input-group> </nz-form-control> </nz-form-item> + <nz-form-item> + <nz-form-control [nzErrorTip]="emailErrorTip"> + <nz-input-group nzPrefixIcon="mail"> + <input + type="email" + nz-input + formControlName="registerEmail" + placeholder="Email" /> + </nz-input-group> + </nz-form-control> + </nz-form-item> <nz-form-item> <nz-form-control [nzErrorTip]="passwordErrorTip"> <nz-input-group nzPrefixIcon="lock"> diff --git a/frontend/src/app/hub/component/about/local-login/local-login.component.spec.ts b/frontend/src/app/hub/component/about/local-login/local-login.component.spec.ts index f969f117a3..bc39ac7fd1 100644 --- a/frontend/src/app/hub/component/about/local-login/local-login.component.spec.ts +++ b/frontend/src/app/hub/component/about/local-login/local-login.component.spec.ts @@ -93,6 +93,7 @@ describe("LocalLoginComponent", () => { "loginPassword", "loginUsername", "registerConfirmationPassword", + "registerEmail", "registerPassword", "registerUsername", ].sort() @@ -108,6 +109,20 @@ describe("LocalLoginComponent", () => { expect(registerUsername.hasError("required")).toBe(true); }); + it("requires registerEmail and enforces email format", () => { + const registerEmail = component.allForms.get("registerEmail")!; + registerEmail.setValue(""); + expect(registerEmail.hasError("required")).toBe(true); + + registerEmail.setValue("not-an-email"); + expect(registerEmail.hasError("email")).toBe(true); + + registerEmail.setValue("[email protected]"); + expect(registerEmail.valid).toBe(true); + expect(registerEmail.hasError("email")).toBe(false); + expect(registerEmail.hasError("required")).toBe(false); + }); + it("requires passwords and enforces minLength(6)", () => { const loginPassword = component.allForms.get("loginPassword")!; const registerPassword = component.allForms.get("registerPassword")!; @@ -264,6 +279,7 @@ describe("LocalLoginComponent", () => { const validateSpy = vi.spyOn(UserService, "validateUsername").mockReturnValue({ result: true, message: "ok" }); component.allForms.patchValue({ registerUsername: "alice", + registerEmail: "[email protected]", registerPassword: "abc", registerConfirmationPassword: "abc", }); @@ -279,6 +295,7 @@ describe("LocalLoginComponent", () => { vi.spyOn(UserService, "validateUsername").mockReturnValue({ result: true, message: "ok" }); component.allForms.patchValue({ registerUsername: "alice", + registerEmail: "[email protected]", registerPassword: "abcdef", registerConfirmationPassword: "ghijkl", }); @@ -294,8 +311,10 @@ describe("LocalLoginComponent", () => { result: false, message: "Username should not be empty.", }); + vi.spyOn(UserService, "validateEmail").mockReturnValue({ result: true, message: "ok" }); component.allForms.patchValue({ registerUsername: "", + registerEmail: "[email protected]", registerPassword: "abcdef", registerConfirmationPassword: "abcdef", }); @@ -306,17 +325,79 @@ describe("LocalLoginComponent", () => { expect(userServiceMock.register).not.toHaveBeenCalled(); }); - it("calls UserService.register with the trimmed username and surfaces a success notification", () => { + it("sets registerErrorMessage when the email is empty", () => { + vi.spyOn(UserService, "validateEmail").mockReturnValue({ + result: false, + message: "Email should not be empty.", + }); + component.allForms.patchValue({ + registerUsername: "alice", + registerEmail: "", + registerPassword: "abcdef", + registerConfirmationPassword: "abcdef", + }); + + component.register(); + + expect(component.registerErrorMessage).toBe("Email should not be empty."); + expect(userServiceMock.register).not.toHaveBeenCalled(); + }); + + it("sets registerErrorMessage when the email is malformed", () => { + vi.spyOn(UserService, "validateEmail").mockReturnValue({ + result: false, + message: "Email format is invalid.", + }); + component.allForms.patchValue({ + registerUsername: "alice", + registerEmail: "not-an-email", + registerPassword: "abcdef", + registerConfirmationPassword: "abcdef", + }); + + component.register(); + + expect(component.registerErrorMessage).toBe("Email format is invalid."); + expect(userServiceMock.register).not.toHaveBeenCalled(); + }); + + it("checks email validity before username validity", () => { + // Email validation runs before username validation in register(), so a + // bad email must short-circuit the flow even if username is also bad. + const validateUsernameSpy = vi + .spyOn(UserService, "validateUsername") + .mockReturnValue({ result: false, message: "Username should not be empty." }); + const validateEmailSpy = vi + .spyOn(UserService, "validateEmail") + .mockReturnValue({ result: false, message: "Email format is invalid." }); + component.allForms.patchValue({ + registerUsername: "", + registerEmail: "not-an-email", + registerPassword: "abcdef", + registerConfirmationPassword: "abcdef", + }); + + component.register(); + + expect(component.registerErrorMessage).toBe("Email format is invalid."); + expect(validateUsernameSpy).not.toHaveBeenCalled(); + expect(validateEmailSpy).toHaveBeenCalledWith("not-an-email"); + expect(userServiceMock.register).not.toHaveBeenCalled(); + }); + + it("calls UserService.register with the trimmed username,email and surfaces a success notification", () => { vi.spyOn(UserService, "validateUsername").mockReturnValue({ result: true, message: "ok" }); + vi.spyOn(UserService, "validateEmail").mockReturnValue({ result: true, message: "ok" }); component.allForms.patchValue({ registerUsername: " alice ", + registerEmail: " [email protected] ", registerPassword: "abcdef", registerConfirmationPassword: "abcdef", }); component.register(); - expect(userServiceMock.register).toHaveBeenCalledWith("alice", "abcdef"); + expect(userServiceMock.register).toHaveBeenCalledWith("alice", "[email protected]", "abcdef"); expect(notificationServiceMock.success).toHaveBeenCalledWith( "Your account has been created. Please contact the Texera administrator to activate your account." ); @@ -325,9 +406,11 @@ describe("LocalLoginComponent", () => { it("surfaces the error's message via NotificationService.error on failure", () => { vi.spyOn(UserService, "validateUsername").mockReturnValue({ result: true, message: "ok" }); + vi.spyOn(UserService, "validateEmail").mockReturnValue({ result: true, message: "ok" }); vi.mocked(userServiceMock.register!).mockReturnValueOnce(throwError(() => new Error("nope"))); component.allForms.patchValue({ registerUsername: "alice", + registerEmail: "[email protected]", registerPassword: "abcdef", registerConfirmationPassword: "abcdef", }); @@ -340,9 +423,11 @@ describe("LocalLoginComponent", () => { it("falls back to 'Registration failed' when the error has no message", () => { vi.spyOn(UserService, "validateUsername").mockReturnValue({ result: true, message: "ok" }); + vi.spyOn(UserService, "validateEmail").mockReturnValue({ result: true, message: "ok" }); vi.mocked(userServiceMock.register!).mockReturnValueOnce(throwError(() => ({}))); component.allForms.patchValue({ registerUsername: "alice", + registerEmail: "[email protected]", registerPassword: "abcdef", registerConfirmationPassword: "abcdef", }); diff --git a/frontend/src/app/hub/component/about/local-login/local-login.component.ts b/frontend/src/app/hub/component/about/local-login/local-login.component.ts index 0096807439..bc5bee933e 100644 --- a/frontend/src/app/hub/component/about/local-login/local-login.component.ts +++ b/frontend/src/app/hub/component/about/local-login/local-login.component.ts @@ -77,6 +77,7 @@ export class LocalLoginComponent implements OnInit { this.allForms = this.formBuilder.group({ loginUsername: new FormControl("", [Validators.required]), registerUsername: new FormControl("", [Validators.required]), + registerEmail: new FormControl("", [Validators.required, Validators.email]), loginPassword: new FormControl("", [Validators.required, Validators.minLength(6)]), registerPassword: new FormControl("", [Validators.required, Validators.minLength(6)]), registerConfirmationPassword: new FormControl("", [Validators.required, this.confirmationValidator]), @@ -143,8 +144,14 @@ export class LocalLoginComponent implements OnInit { this.registerErrorMessage = undefined; const registerPassword = this.allForms.get("registerPassword")?.value; const registerConfirmationPassword = this.allForms.get("registerConfirmationPassword")?.value; - const registerUsername = this.allForms.get("registerUsername")?.value.trim(); - const validation = UserService.validateUsername(registerUsername); + const registerEmail = (this.allForms.get("registerEmail")?.value ?? "").trim(); + const registerUsername = (this.allForms.get("registerUsername")?.value ?? "").trim(); + + const validateEmail = UserService.validateEmail(registerEmail); + if (!validateEmail.result) { + this.registerErrorMessage = validateEmail.message; + return; + } if (registerPassword.length < 6) { this.registerErrorMessage = "Password length should be greater than 5"; return; @@ -153,13 +160,15 @@ export class LocalLoginComponent implements OnInit { this.registerErrorMessage = "Passwords do not match"; return; } - if (!validation.result) { - this.registerErrorMessage = validation.message; + + const validateUsername = UserService.validateUsername(registerUsername); + if (!validateUsername.result) { + this.registerErrorMessage = validateUsername.message; return; } // register the credentials with backend this.userService - .register(registerUsername, registerPassword) + .register(registerUsername, registerEmail, registerPassword) .pipe( catchError((e: unknown) => { const errorMessage = (e as Error)?.message || "Registration failed";
