A Full-Stack Web App Using Angular and GraphQL: Adding User Registration Functionality (Part 4)

Jollen Moyani - Mar 4 - - Dev Community

Welcome to our journey of building a full-stack web application using Angular and GraphQL.

In the previous article of this series, we learned how to add the edit and delete features to our application. We also configured the home page and provided the sort and filter options for the list of movies.

In this article, we will add the feature for user registration in our application.

Adding the required model classes

First, let’s create a class named UserRoles.cs within the Models folder and define the user roles in our app.

public static class UserRoles
{
    public const string Admin = "Admin";
    public const string User = "User";
}
Enter fullscreen mode Exit fullscreen mode

This static class specifies the allowed user roles in our application.

Next, add another class named UserRegistration.cs in the Models folder with the following code.

public class UserRegistration
{
        [Required]
        public string FirstName { get; set; }
        [Required]
        public string LastName { get; set; }
        [Required]
        public string Username { get; set; }
        [Required]
        [RegularExpression(@"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$")]
        public string Password { get; set; }
        [Required]
        [Compare("Password")]
        public string ConfirmPassword { get; set; }
        [Required]
        public string Gender { get; set; }
        public UserRegistration()
        {
            FirstName = string.Empty;
            LastName = string.Empty;
            Gender = string.Empty;
            Username = string.Empty;
            Password = string.Empty;
            ConfirmPassword = string.Empty;
        }
}
Enter fullscreen mode Exit fullscreen mode

This class is used to capture user registration details. All the fields of this class are marked optional. The [RegularExpression] attribute applied to the Password field is used to enforce password complexity rules: the password should have a minimum of 8 characters, at least 1 uppercase letter, 1 lowercase letter, and 1 number.

Next, add another class named RegistrationResponse.cs inside the Models folder.

public class RegistrationResponse
{
    public bool IsRegistrationSuccess { get; set; }
    public string? ErrorMessage { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

This class returns the registration response to the client.

Create the IUser interface

In the MovieApp\Interfaces folder, add a new file called IUser.cs with the following method declarations.

public interface IUser
{
    Task<bool> RegisterUser(UserMaster userData);
    Task<bool> IsUserExists(int userId);
    bool CheckUserNameAvailability(string username);
}
Enter fullscreen mode Exit fullscreen mode

Creating a UserDataAccessLayer for the application

Add a class named UserDataAccessLayer.cs inside the MovieApp\DataAccess folder with the following code.

public class UserDataAccessLayer: IUser
{
    readonly MovieDbContext _dbContext;
    public UserDataAccessLayer(IDbContextFactory<MovieDbContext> dbContext)
    {
        _dbContext = dbContext.CreateDbContext();
    }
    public async Task<bool> IsUserExists(int userId)
    {
        UserMaster? user = await _dbContext.UserMasters.FirstOrDefaultAsync(x => x.UserId == userId);
        return user != null;
    }
    public async Task<bool> RegisterUser(UserMaster userData)
    {
        bool isUserNameAvailable = CheckUserNameAvailability(userData.Username);
        try
        {
            if (isUserNameAvailable)
            {
                await _dbContext.UserMasters.AddAsync(userData);
                await _dbContext.SaveChangesAsync();
                return true;
            }
            else
            {
                return false;
            }
        }
        catch
        {
            throw;
        }
    }
    public bool CheckUserNameAvailability(string userName)
    {
        string? user = _dbContext.UserMasters.FirstOrDefault(x => x.Username == userName)?.ToString();
        return user == null;
    }
}
Enter fullscreen mode Exit fullscreen mode

We have implemented the IUser interface and added the definition for all the required methods:

  • The IsUserExists method checks whether a user exists in the database based on the provided user ID.
  • The RegisterUser method is used to register a new user. It first checks if the username is available, and if it is, it adds the new user to the UserMasters DbSet and saves the changes to the database.
  • The CheckUserNameAvailability method checks whether a username is available in the database. It returns true if the username is found, indicating it is available.

Add a GraphQL mutation resolver for authentication

In the MovieApp\GraphQL folder, add a class named AuthMutationResolver.cs.

[ExtendObjectType(typeof(MovieMutationResolver))]
public class AuthMutationResolver
{
    readonly IUser _userService;
    public AuthMutationResolver(IUser userService)
    {
        _userService = userService;
    }
    [GraphQLDescription("Register a new user.")]
    public async Task<RegistrationResponse> UserRegistration([FromBody] UserRegistration registrationData)
    {
        UserMaster user = new()
        {
            FirstName = registrationData.FirstName,
            LastName = registrationData.LastName,
            Username = registrationData.Username,
            Password = registrationData.Password,
            Gender = registrationData.Gender,
            UserTypeName = UserRoles.User
        };
        bool userRegistrationStatus = await _userService.RegisterUser(user);
        if (userRegistrationStatus)
        {
            return new RegistrationResponse { IsRegistrationSuccess = true };
        }
        else
        {
            return new RegistrationResponse { IsRegistrationSuccess = false, ErrorMessage = "This username is not available." };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The AuthMutationResolver class allows registering a new user in the app, using the IUser service to interact with the user data.

The UserRegistration asynchronous method is used to handle the user registration. It creates a new UserMaster object from the provided registration data and then uses the \userService to register the new user. A RegistrationResponse instance is returned with IsRegistrationSuccess set to true if the registration is successful. If the registration was unsuccessful, a RegistrationResponse instance is returned with IsRegistrationSuccess set to false and an error message indicating that the username is unavailable.

The ExtendObjectType attribute tells the GraphQL schema that the AuthMutationResolver class is extending the MovieMutationResolver type. This means that the fields and methods defined in AuthMutationResolver will be added to the MovieMutationResolver type in the GraphQL schema. This is a feature of the Hot Chocolate library that allows for a more modular and organized structure of your GraphQL schema.

Note: GraphQL restricts the creation of more than one mutation type.

Register the mutation resolver

As a new mutation resolver has been added, it must be registered in our middleware. Update the Program.cs file with the following code.

builder.Services.AddGraphQLServer()
    .AddQueryType<MovieQueryResolver>()
    .AddMutationType<MovieMutationResolver>()
    .AddTypeExtension<AuthMutationResolver>()
    .AddFiltering()
    .AddErrorFilter(error =>
    {
        return error;
    });
Enter fullscreen mode Exit fullscreen mode

We use the AddTypeExtension method to register the new mutation resolver type MovieMutationResolver. Then, register the transient lifetime of the IUser service using the following code.

builder.Services.AddTransient<IUser, UserDataAccessLayer>();
Enter fullscreen mode Exit fullscreen mode

We are done with the server configuration. Let’s move to the client side of the app.

Add the GraphQL query and mutation

Add the following GraphQL mutation in the src\app\GraphQL\mutation.ts file. It will allow us to register a new user in the app.

export const REGISTER_USER = gql`
  mutation register($registrationData: UserRegistrationInput!) {
    userRegistration(registrationData: $registrationData) {
      isRegistrationSuccess
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Create the client-side model

Create a new file named userRegistration.ts under the src\app\models folder and include the following code.

export interface UserRegistration {
  firstName: string;
  lastName: string;
  username: string;
  password: string;
  confirmPassword: string;
  gender: string;
}
export interface RegistrationResponse {
  isRegistrationSuccess: boolean;
  errorMessage: string;
}
export type RegistrationType = {
  userRegistration: RegistrationResponse;
};
Enter fullscreen mode Exit fullscreen mode

In the previous code example:

  • The UserRegistration interface is used to capture the details a user provides during registration.
  • The RegistrationResponse is used to provide feedback on whether the registration was successful or not.
  • The RegistrationType is used to define the structure of an object that includes a RegistrationResponse.

Then, create a new file named userRegistrationForm.ts in the Models folder and add the following code to it.

import { FormControl } from '@angular/forms';
export interface UserRegistrationForm {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
  userName: FormControl<string>;
  password: FormControl<string>;
  confirmPassword: FormControl<string>;
  gender: FormControl<string>;
}
Enter fullscreen mode Exit fullscreen mode

This interface creates a strongly typed reactive form for capturing user registration details.

Create the GraphQL service

Run the following command in the ClientApp folder to generate a service file.

ng g s services\registration
Enter fullscreen mode Exit fullscreen mode

Add the following code to the registration.service.ts file.

import { Injectable } from '@angular/core';
import { Mutation } from 'apollo-angular';
import { REGISTER_USER } from '../GraphQL/mutation';
import { RegistrationType } from '../models/userRegistration';
@Injectable({
  providedIn: 'root',
})
export class RegistrationService extends Mutation<RegistrationType> {
  document = REGISTER_USER;
}
Enter fullscreen mode Exit fullscreen mode

This service is used to register a new user using the GraphQL mutation.

Create the custom form validator service

Let’s create a custom form validator service. First, run the following command to generate a service file.

ng g s services\custom-form-validator
Enter fullscreen mode Exit fullscreen mode

Then, add the following code to the custom-form-validator.service.ts file.

import { Injectable } from '@angular/core';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
@Injectable({
  providedIn: 'root',
})
export class CustomFormValidatorService {
  passwordPatternValidator(): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (!control.value) {
        return null;
      }
      const regex = new RegExp('^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$');
      const validPattern = regex.test(control.value);
      return validPattern ? null : { invalidPassword: true };
    };
  }
  matchPasswordValidator(password: string, confirmPassword: string) {
    return (formGroup: AbstractControl): ValidationErrors | null => {
      const passwordControl = formGroup.get(password);
      const confirmPasswordControl = formGroup.get(confirmPassword);
      if (!passwordControl || !confirmPasswordControl) {
        return null;
      }
      if (
        confirmPasswordControl.errors &&
        !confirmPasswordControl.errors['passwordMismatch']
      ) {
        return null;
      }
      if (passwordControl.value !== confirmPasswordControl.value) {
        confirmPasswordControl.setErrors({ passwordMismatch: true });
        return { passwordMismatch: true };
      } else {
        confirmPasswordControl.setErrors(null);
        return null;
      }
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

The custom form validator service provides two methods for validating form inputs, specifically for password fields:

  • The passwordPatternValidator method returns a ValidatorFn function that takes an AbstractControl as an argument. The AbstractControl represents a specific form control, in this case, a password field. The function checks if the control’s value matches a specific regular expression pattern. The pattern requires at least one uppercase letter, one lowercase letter, one number, and a minimum length of 8 characters. If the control’s value doesn’t match this pattern, the function returns an object with the property invalidPassword set to true. If the control’s value is null or matches the pattern, the function returns null, indicating no validation errors.
  • The matchPasswordValidator method returns a ValidatorFn function that takes an AbstractControl as an argument. The AbstractControl, in this case, represents a form group that includes the password and confirm password fields. The function checks if the values of these two fields match. If they don’t match, the function sets a passwordMismatch error on the confirm password control and returns an object with a passwordMismatch property set to true. If the values match, the function clears any errors on the confirm password control and returns null, indicating no validation errors.

Create the user registration component

Run the following command to create the user registration component.

ng g c components\user-registration
Enter fullscreen mode Exit fullscreen mode

Update the UserRegistrationComponent class in the src\app\components\user-registration\user-registration.component.ts file as follows.

export class UserRegistrationComponent {
  protected userRegistrationForm!: FormGroup<UserRegistrationForm>;
  private destroyed$ = new ReplaySubject<void>(1);
  protected submitted = false;
  public data = ['Male', 'Female'];
  constructor(
    private readonly router: Router,
    private readonly formBuilder: NonNullableFormBuilder,
    private readonly customFormValidator: CustomFormValidatorService,
    private readonly registrationService: RegistrationService
  ) {
    this.initializeForm();
  }
  private initializeForm(): void {
    this.userRegistrationForm = this.formBuilder.group(
      {
        firstName: this.formBuilder.control('', Validators.required),
        lastName: this.formBuilder.control('', Validators.required),
        userName: this.formBuilder.control('', Validators.required),
        password: this.formBuilder.control('', [
          Validators.required,
          this.customFormValidator.passwordPatternValidator(),
        ]),
        confirmPassword: this.formBuilder.control('', Validators.required),
        gender: this.formBuilder.control('', Validators.required),
      },
      {
        validators: [
          this.customFormValidator.matchPasswordValidator(
            'password',
            'confirmPassword'
          ),
        ],
      }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The initializeForm method is called in the constructor to initialize the userRegistrationForm. It creates a form group with form controls for the first name, last name, username, password, confirm password, and gender. Each form control is initialized with an empty string and is required.

The password form control has an additional custom validator, the passwordPatternValidator, that validates the password pattern. The form group has a custom validator, matchPasswordValidator , that ensures the password and confirms that the password fields match.

Add the following functions to the UserRegistrationComponent class.

registerUser(): void {
this.submitted = true;
const userRegistrationData: UserRegistration = {
  firstName: this.userRegistrationForm.value.firstName ?? '',
  lastName: this.userRegistrationForm.value.lastName ?? '',
  username: this.userRegistrationForm.value.userName ?? '',
  password: this.userRegistrationForm.value.password ?? '',
  confirmPassword: this.userRegistrationForm.value.confirmPassword ?? '',
  gender: this.userRegistrationForm.value.gender ?? '',
};
if (this.userRegistrationForm.valid) {
  this.registrationService
    .mutate({
      registrationData: userRegistrationData,
    })
    .pipe(takeUntil(this.destroyed$))
    .subscribe({
      next: (response) => {
        if (response.data?.userRegistration.isRegistrationSuccess) {
          ToastUtility.show({
            content: 'User registration successful.',
            position: { X: 'Right', Y: 'Top' },
            cssClass: 'e-toast-success',
          });
          this. router.navigate(['/']);
        } else {
          this.userRegistrationForm.controls.userName.setErrors({
            userNameNotAvailable: true,
          });
          console.error(
            'Error occurred during registration: ',
            response.data?.userRegistration.errorMessage
          );
        }
      },
    });
 }
}

get registrationFormControl() {
    return this.userRegistrationForm.controls;
}

ngOnDestroy() {
    this.destroyed$.next();
    this.destroyed$.complete();
}
Enter fullscreen mode Exit fullscreen mode

The registerUser method is called to register a new user. It first sets the submitted property to true , which triggers validation messages in the template. Then, it creates a userRegistrationData object from the form values. The nullish coalescing operator (??) ensures that the object’s properties are not null or undefined.

If the form is valid , it calls the mutate method on the registrationService to register the new user. If the registration is successful , it displays a success toast notification and navigates to the root route. If the registration is unsuccessful , an error is set on the username form control indicating that the username is unavailable, and an error message is logged to the console.

The registrationFormControl returns the controls of the userRegistrationForm. This is used to access the form controls in the template.

Add the following code to the user-registration.component.html file.

<div class="row justify-content-center">
 <div class="col-md-6 col-lg-6 col-sm-12">
  <div class="title-container p-2 d-flex align-items-center justify-content-between">
   <h2 class="m-0">User Registration</h2>
   <div>
    <span class="mat-h4">Already Registered? </span>
    <button ejs-button cssClass="e-primary" [routerLink]="['/login']"> Login </button>
   </div>
  </div>
  <div class="e-card">
   <div class="e-card-content row g-0">
    <form [formGroup]="userRegistrationForm" (ngSubmit)="registerUser()">
     <div class="e-input-section">
      <ejs-textbox
           placeholder="First name"
           cssClass="e-outline"
           floatLabelType="Auto"
           formControlName="firstName"></ejs-textbox>
     </div>
     <div *ngIf="(registrationFormControl.firstName.touched || submitted) && registrationFormControl.firstName.errors?.['required']" class="e-error"> First Name is required. </div>
     <div class="e-input-section">
      <ejs-textbox
           placeholder="Last Name"
           cssClass="e-outline"
           floatLabelType="Auto"
           formControlName="lastName"></ejs-textbox>
     </div>
     <div *ngIf="(registrationFormControl.lastName.touched || submitted) && registrationFormControl.lastName.errors?.['required']" class="e-error"> Last Name is required. </div>
     <div class="e-input-section">
      <ejs-textbox
           placeholder="User Name"
           cssClass="e-outline"
           floatLabelType="Auto"
           formControlName="userName"></ejs-textbox>
     </div>
     <div *ngIf="(registrationFormControl.userName.touched || submitted) && registrationFormControl.userName.errors?.['required']" class="e-error" > Usern Name is required. </div>
     <div *ngIf="(registrationFormControl.userName.touched || submitted) && registrationFormControl.userName.errors?.['userNameNotAvailable']" class="e-error" > Usern Name is not available. </div>
     <div class="e-input-section">
      <ejs-textbox
           type="password"
           placeholder="Password"
           cssClass="e-outline"
           floatLabelType="Auto"
           formControlName="password"></ejs-textbox>
     </div>
     <div *ngIf="(registrationFormControl.password.touched || submitted) && registrationFormControl.password.errors?.['required']" class="e-error" > Password is required. </div>
     <div *ngIf="registrationFormControl.password.touched && registrationFormControl.password.errors?.['invalidPassword']" class="e-error"> Password should have minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, and 1 number. </div>
     <div class="e-input-section">
      <ejs-textbox
           type="password"
           placeholder="Confirm Password"
           cssClass="e-outline"
           floatLabelType="Auto"
           formControlName="confirmPassword"></ejs-textbox>
     </div>
     <div *ngIf="(registrationFormControl.confirmPassword.touched || submitted) && registrationFormControl.confirmPassword.errors?.['required']" class="e-error"> Confirm password is required.</div>
     <div *ngIf=" registrationFormControl.confirmPassword.touched && registrationFormControl.confirmPassword.errors?.['passwordMismatch']" class="e-error"> Passwords do not match. </div>
     <div class="e-input-section">
      <ejs-dropdownlist
           [dataSource]="data"
           placeholder="Select gender"
           cssClass="e-outline"
           floatLabelType="Auto"
           formControlName="gender"></ejs-dropdownlist>
     </div>
     <div *ngIf="(registrationFormControl.gender.touched || submitted) && registrationFormControl.gender.errors?.['required']" class="e-error"> Gender is required.</div>
     <div class="e-card-actions d-flex justify-content-end">
      <button type="submit" ejs-button cssClass="e-info">Register</button>
     </div>
    </form>
   </div>
  </div>
 </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The form element represents the user registration form. The formGroup attribute is bound to the userRegistrationForm property, and the ngSubmit event is bound to the registerUser method. The ejs-textbox components represent the form controls. The formControlName attribute binds each ejs-textbox to a form control in the userRegistrationForm.

On selecting the form controls or when the form is submitted, it will validate the details. If anything in the form control is empty, it will display an error message. If the password provided in the password form control is not in a valid pattern, then it will display an Invalid Password error message. If the password provided in the confirm password form control is wrong, it will display a Password Mismatch error message. If the username form control is empty, it will display a Username is Not A vailable error message.

The button element with the type=”submit” attribute is used to submit the form. The ejs-button directive is used to style the button.

The ejs-button directive with the routerLink attribute is used to navigate to the /login route, where a user can log in.

Note:

  • Make sure you add the import for the ReactiveFormsModule in the app.module.ts file.
  • The login component and the corresponding route will be created in the next part of this article. However, we have added a navigation link to the login page for consistency.

Configure app routing

Finally, open the src\app\app-routing.module.ts file and add the route for the UserRegistration component under the appRoutes array.

const appRoutes: Routes = [
  { path: '', component: HomeComponent, pathMatch: 'full' },
  { path: 'register', component: UserRegistrationComponent },
  // Existing code
];
Enter fullscreen mode Exit fullscreen mode

Execution demo

After executing the previous code examples, we will get output like in the following image. Implementing User Registration in a Full-Stack Web App with Angular and GraphQL

GitHub reference

For more details, refer to the complete source code for the full-stack web app with Angular and GraphQL on GitHub.

Summary

Thanks for reading! In this article, we learned how to add the user registration feature to our full-stack web app using Angular and GraphQL.

In the next article of this series, we’ll learn to implement the login functionality and add role-based authorization to our app.

Whether you’re already a valued Syncfusion user or new to our platform, we extend an invitation to explore our Angular components with the convenience of a complimentary trial. This trial allows you to experience the full potential of our components to enhance your app’s user interface and functionality.

Our dedicated support system is readily available if you need guidance or have any inquiries. Contact us through our support forum, support portal, or feedback portal. Your success is our priority, and we’re always delighted to assist you on your development journey!

Related blogs

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .