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.
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.
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)
The following describes a little bit about what has been implemented
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
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";
});
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));
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:
- JWT Retrieval: Retrieves the JWT token stored in a cookie named
AuthToken
- JWT Decoding: Uses JwtSecurityTokenHandler to decode the token. Reads the claims from the token to extract information.
- 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);
}
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.
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();
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);
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.
Work in Progress