Skip to content

RaccoonCodes/WatchNest-Main-Application

Repository files navigation

WatchNest - Main Application

This Project contains the main application. To use this, you will need to download this project and the API for this project. The explanation for how each action in the API work is explained in the README for the API repository. This READMEwill focus on the front-end and back-end of this application.

Overview

This project is developed in .NET 6 and its main focus is to provider frendly user experience in storing user's watchlist, such as movies, series, and videos that they have seen and search for their series in their own list when they want to review certain titles, genre, etc.

There are two different host that can be used in this application,

https://localhost:44301

http://localhost:5001

when using the http host, it will redirect to https host.

Note: If you are using this project for the first time, or locally, make sure to inject the seed that is available in the api when logged in as Administrator or disable the authorization for admin-only in case there is no admin credentials available.

Packages Used

This project uses the following packages, so please ensure they are installed when using this project:

  • Microsoft.Web.LibraryManager.Cli 2.1.175
  • System.IdentityModel.Tokens.Jwt 8.2.0
  • Caching SqlServer 8.0.6
  • Bootstrap 5.1.3
  • Google fonts
  • Jquery-validate 1.19.3 (through libman)
  • Jquery-validation-unobtrusive 3.2.12 (through libman)
  • Jquery 3.6.0 (through libman)

Program.cs

The following describes a little bit about what has been implemented

Distributed SQL Caching

I have set up caching to be store in SQL database. Table is name as AppCache. This will store results produced by the API calls to improve performance and reduce the number of redundant API calls

Cookie Authentication

I have implemented cookie authentication, which manages user's session. Its scheme consist of: A cookie that will hold the cookie produce by the API. The API will return a cookie that holds JWT. More info on my README in WatchNest - API.

A Login Path that if unaunthenticated user attempts to access an endpoint or resource, they will be redirected to the login page.

A Logout Path when a user logs out, they are redirected back to the login page.

Access Denied Path: If a user tries to access a resource they are not authorized to view, they are directed to a Access Denied Page.

builder.Services.AddAuthentication("CookieAuth").AddCookie("CookieAuth",opts =>
{
    opts.Cookie.Name = "AuthToken";
    opts.LoginPath = "/Home/Index";
    opts.LogoutPath = "/Home/Logout";
    opts.AccessDeniedPath = "/Home/AccessDenied";
});

HTTP Configuration

This application uses client named "APIClient" for making API calls. It contains the following:

Base Address: The base address for the HTTP client is set to https://localhost:44350/. This will be the default address used for all outgoing requests from this client.

Accept: This header is set to application/json, ensuring the client expects JSON responses from the API.

Lifetime handler: The handler lifetime is set to 30 minutes. This means the HttpClient handler will be reused for 30 minutes before being disposed and replaced. This improves performance by reducing the overhead of creating new handlers for each request.

builder.Services.AddHttpClient("APIClient",client =>
{
    client.BaseAddress = new Uri("https://localhost:44350/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
}).SetHandlerLifetime(TimeSpan.FromMinutes(30));

Middleware: JwtClaimsMiddleware

This Middleware uses a class to extract information from JWT stored in the cookie and attach the claims to the current HttpContext.User.

Here is the General Process:

  1. JWT Retrieval: Retrieves the JWT token stored in a cookie named AuthToken
  2. JWT Decoding: Uses JwtSecurityTokenHandler to decode the token. Reads the claims from the token to extract information.
  3. Claims Handling: Creates a ClaimsIdentity using the claims from the decoded JWT. This way, it assigns the identity to HttpContext.User, enabling claims-based authentication throughout the application.

Once the process is done, it moves to the next middleware in the pipeline.

public async Task InvokeAsync(HttpContext context)
{
    var jwt = context.Request.Cookies["AuthToken"];

    if (!string.IsNullOrEmpty(jwt))
    {
        try
        {
            //Decode
            var handler = new JwtSecurityTokenHandler();
            var token = handler.ReadJwtToken(jwt);

            // ClaimsIdentity from the token claims
            var claims = new ClaimsIdentity(token.Claims, "Bearer");

            // Attach claims to the HttpContext.User
            context.User = new ClaimsPrincipal(claims);
        }
        catch
        {
            _logger.LogError("There was an error with jwt Token. Please look into it!");
            context.User = new ClaimsPrincipal();
        }
    }
    await _next(context);
}

Models

Most Model classes are pretty straight forward, so I will be explaining the implementations for Account, Admin, and WatchList Services. These classes maintain Separation of Concerns by keeping business logic encapsulated within the service while letting the controllers focus on handling HTTP request.

Account Services

This class inherits IAccountService interface and handles account related operations such as login, logout, and registration. It uses IHttpClientFactory to send and recieve request to and from the API.

Methods

LoginAsync: Send a POST request to Account/Login endpoint with loginDTO object which contains user's username and password. Once Successful, it recieves a cookie containing JWT and is used for authentication in this application. This method returns a tuple, a bool flag, error message, and JWT cookie.

LogoutAsync: Sends a DELETE request to Account/Logout endpoint and it returns a bool whether the opperations was a success.

RegisterAsync: Sends a POST request to the Account/Register endpoint with object RegisterDTO that contains user details. It return a tuple that contains a bool flag and any error messages.

ExtractErrorMessageAsync: A Helper method that parses error response from the API, and attempts to deserialize the response into an ErrorDetails object.

public async Task<(bool IsSuccess, string? ErrorMessage, string? JwtCookie)> LoginAsync(string username, string password);

public async Task<(bool IsSuccess, string? ErrorMessage)> RegisterAsync(string username, string password, string email);

public async Task<bool> LogoutAsync();

Admin Services

This is reponsivle for implementing administrative actions by interacting with API endpoints, similiar to the previous method. .

NOTE ApiResponse is described in later section.

Methods GetAllUsersAsync: Fetches all users in the database with a GET request to the API and returns an ApiResponse and its type is tied to UserModel.

GetAllSeriesAsync: Fetches all series from the database with a GET request to the API and returns ApiResponse and its type is SeriesDTO.

DeleteUserAsync: Deletes user by their Id with a DELETE request to the API and return a bool determining if it was sucessful or not.

GetCachedUserListAsync: Tries to retrieved cached user list, if it exist. If it is empty, expired, or null, it cahces a new user list from the API call and set the expiration to 1 hour and 30 min.

RefreshCacheAsync: Invalidates cache based on cache key.

public async Task<ApiResponse<UserModel>> GetAllUsersAsync();
public async Task<ApiResponse<SeriesDTO>> GetAllSeriesAsync();
public async Task<ApiResponse<SeriesDTO>> GetFilteredSeriesAsync(FilterDTO filterDTO);
public async Task<ApiResponse<UserModel>> GetCachedUserListAsync(string cacheKey);
public async Task RefreshCacheAsync(string cacheKey);
public async Task<bool> DeleteUserAsync(string userID);

WatchList Service

This class provides management for dealing with users watchlist with CRUD operations, filtering and caching.

Methods

GetSeriesAsync: retrieves series for specific user and includes a refresh for caching. The api results gets cached for 3 min and if there is no series, then it will return empty ApiResponse.

FilterSeriesAsync: retreives filter series based on FilterDTO which contains sort order, sort column, filter query, and UserID. It returns an ApiResponse object.

GetSeriesByIdAsync: Fetches details of a single series by its series ID by calling a GET request. and returns a UpdatedModel for the method.

AddSeriesAsync: Adds a new series using a POST request and returns a bool that represents success.

DeleteSeriesAsync: Deletes a series by its ID via a DELETE request and returns a boolean indicating the operation's success.

UpdateSeriesAsync: Updates an existing series using a PUT request and returns a bool determining the success of the API call.

Controllers

Work in Progress