- Published on
Resource Booking App
Contents
👋 Introduction
🔑 Key Features
🛠️ Tech Stack
🏗️ System Architecture
🔒 Seamless Auth0 Integration for User Authentication and Authorization
🚫 Securing Pages from Unauthorized Access via URL Manipulation
✨ Additional Exciting Features
🔗 Links
🏁 Conclusion
Introduction
Have you ever found yourself in a big apartment complex with tons of amenities like gyms, pools, and shared spaces? It sounds awesome, but there's a catch – it's often a hassle to actually use these resources because everyone's competing for them simultaneously. It hit me one day – there's got to be a simpler way to sort this out. That's when I got the idea for something I believe could change the game: a Resource Booking App.
Key Features
As I began outlining the project's requirements and architecting its structure, I aimed to incorporate specific features and platforms to enhance my skill set. Here are the project's key objectives that I set out to accomplish:
- Three-Tier Architecture - Frontend, RESTful API, Backend
- Login and different views for two types of users - Client and Admin
- Admin’s functionalities - Create, read, update, and delete (CRUD) resources
- Client’s functionalities - Book resources and view current and previous bookings
Tech Stack
Frontend: React, Framer Motion, React Router, React Hot Toast, and Vite, deployed on Vercel.
Backend: Node.js and Express.js for the RESTful API, tested with Postman and Apache JMeter, deployed on Vercel.
Database: Designed a database consisting of three distinct collections. This database was securely hosted on a cluster within MongoDB Atlas.
Authentication and Authorization: Implemented Auth0 for authentication and authorization - creating secure endpoints and Role-Based Access Control.
System Architecture
Presentation Layer
Within this layer, you'll find the frontend accessible to two user types which include both clients and admins. The user's access is directed to specific views based on their assigned role.
For clients, the user interface offers three main options: logging out, accessing the ‘My Bookings’ page to view current and previous bookings, or making reservations on the ‘Available Resources’ page.
Admins have a trio of choices as well: logging out, viewing client bookings on the ‘Resource Overview’ page, or engaging in CRUD operations within the ‘Resource Management’ page.
Application Layer
This layer consists of the RESTful API designed with several secure endpoints as detailed in the image.
1. checkUser
- This endpoint is used to check if this is the first login of the user and assigns an appropriate role to the user.
2. createUser
- If a newly registered user is detected, it activates a modal to gather the user's flat number and subsequently stores the user's data in the database's user collection. For instance, consider a new user named ‘Anusha’ residing in flat C602. In this case, the following details are added to the collection:
{ name: "Anusha", flat: "C602" }
3. createNewResource
- When an admin completes the resource details, this endpoint is invoked to add the resource to the allResources collection. The admin provides information including the resource's name, start time, and end time. This endpoint generates multiple resource records, each spanning one hour, commencing at the specified start time and ending at the resource's designated end time. Suppose an admin wants to create a new resource named ‘Conference Room A’ available from 9:00 AM to 11:00 AM. When they call the createNewResource endpoint with these details, the system generates multiple records as follows:
{ resource: "Conference Room A", startTime: "9", endTime: "10", date: "09/09/2023", available: "1" }
{ resource: "Conference Room A", startTime: "10", endTime: "11", date: "09/09/2023", available: "1" }
- This allows users to book ‘Conference Room A’ for one-hour slots within the specified time frame.
4. deleteResource
- This endpoint is used by the admin to delete an existing resource from the ‘allResources’ collection. It deletes all the records of the resource by matching the field ‘resource’.
5. insertBooking
- This endpoint is evoked when a client books a resource from the available set of resources. The following sample record is generated and pushed into the ‘bookings’ collection:
{ resource: "Badminton", flat: "B-501", name: "Anusha", startTime: "11", endTime: "12", bookingTimeStamp: "2023-08-31T06:30:37.003Z" }
- Subsequently, the record with the same resource name and time slot is then altered in the ‘allResources’ collection by changing the value of the ‘available’ field from "1" to "0". The following is the updated record for the same example:
{ resource: "Badminton", startTime: "11", endTime: "12", date: "31/08/2023", available: "0" }
6. deleteBookings
- This feature allows admins to clear out all bookings associated with a resource when they decide to delete that resource. As a result, any previous bookings made by clients for that resource will be erased.
7. getAllResources
- This endpoint is used to fetch all the records from the ‘allResources’ collection.
8. getAvailableResources
- This endpoint retrieves all records from the 'allResources' collection where the 'available' field is set to '1'.
9. getUniqueAvailableResources
- On the client's page, this endpoint retrieves a list of unique available resources, which are then displayed on separate cards. This card can be tapped to book that resource with the desired time slot from the available set of time slots.
- If all slots for a resource are booked, it won't be shown to the client on an card.
10. getUniqueExistingResources
This endpoint serves a dual purpose:
Admin Page: It displays all created resources on separate cards. These cards provide options to either edit or delete the resource.
Client Page: It checks for the availability of existing resources that can be booked. If no unique existing resources are found, the client is informed that no resources currently exist.
It's important to note that getUniqueExistingResources differs from getUniqueAvailableResources. The endpoint getUniqueAvailableResources is only called if there are any existing resources which is determined by getUniqueExistingResources.
11. getAllBookings
- On the admin page, this endpoint is utilized to access and view all the bookings made by clients.
12. getUniqueResourcesBooked
- This endpoint serves the purpose of retrieving all the unique resources that have been booked. Each resource is then presented in a separate accordion on the admin page. When the admin interacts with an accordion, they can view the bookings specifically made for that resource. These bookings are filtered using the data retrieved by getAllBookings for that particular resource.
13. userBookings
- This endpoint is used to retrieve the bookings of a specific user, which are subsequently displayed in a table on the user's ‘My Bookings’ page.
14. getFlatNumber
- This endpoint is triggered upon user login to fetch their flat details. However, it is not invoked if the user has just signed up. The retrieved flat information is used when the user is booking resources.
15. mainController
- The main controller plays a crucial role in the system, ensuring that API calls are seamlessly routed to the appropriate helper functions listed above, all based on the specified route.
- To add an extra layer of security, I engineered a router middleware to verify the user's access token and check if the user has the necessary permissions to access a given path before invoking the main controller function.
Database Layer
As the name suggests, this layer consists of the database, ‘resourceBooking’, with 3 different collections:
- allResources
- bookings
- users
Seamless Auth0 Integration for User Authentication and Authorization
Securing Endpoints
- To enhance the security of the endpoints, each one is safeguarded by the 'checkJwt' middleware, which is executed before calling the relevant function within the main controller.
- This security measure validates that the access token provided in the API call header is issued by Auth0 for the specific domain and audience designated for the API.
const checkJwt = auth({
issuerBaseURL: process.env.AUTH0_BASE_URL,
audience: process.env.AUTH0_AUDIENCE,
algorithm: ['RS256'],
jwks_uri: 'https://dev-1k4isffw1z8aw3io.us.auth0.com/.well-known/jwks.json',
});
checkJwt
serves as a middleware function designed to protect specific routes or endpoints in the application.- The
auth()
function, provided by Auth0, configures thecheckJwt
middleware. - Within the configuration object passed to
auth()
, you'll find properties such as:issuerBaseURL
, which specifies the authentication service's base URL for issuer verification i.e. the base URL of my tenant which issues these tokens.audience
, which defines the intended audience for JWT validation i.e. nothing but the API URL which consumes these tokens.- The
algorithm
property specifies the cryptographic algorithm for JWT signature verification, employing RS256 (RSA Signature with SHA-256) in this case. - The
jwks_uri
property points to the URL where the JSON Web Key Set (JWKS) resides. JWKS verifies the digital signatures of the JWTs.
In summary, this code sets up a middleware (checkJwt
) applied to protect the API endpoints within the application using JSON Web Tokens (JWTs). This ensures that only authenticated users can access secured resources.
Role-Based Access Control
- As previously mentioned, the application adopts Role-Based Access Control (RBAC), defining two distinct user roles: Client and Admin.
- While
checkJwt
is responsible for verifying the authentication and authorization of users to access API endpoints,checkScopes
is a middleware function used to confirm if the user has the necessary role to access that endpoint. - In simpler terms, an admin can solely execute admin-related tasks, while a client is restricted to client-specific functions.
const checkScopes = requiredScopes('read:admin');
- The above code snippet showcases the middleware which is then used within the router middleware, ensuring that API calls are appropriately directed to the main controller function.
- The
requiredScopes()
function, provided by Auth0, checks whether the access token contains the specified scope provided as an argument to the function. This functionality is made possible as I configured the access tokens to include embedded roles. - Illustrated below, are the endpoints accessible exclusively to clients, those exclusive to admins, and those accessible to both clients and admins.
This meticulous process safeguards against unauthorized access to endpoints through direct URL manipulation, effectively preventing users from performing admin or client specific functions without proper privileges.
Integrating Secure Endpoints with RBAC
- Now that we have seen the independent functionalities of the
checkScopes
andcheckJwt
middleware functions, here's a code snippet that showcases how both of these security layers are integrated within the context of API requests.
router.route('/createNewResource').post(checkJwt, checkScopes, controller.createNewResource);
As demonstrated above, these middleware functions are combined into the router middleware. In the above code snippet, the endpoint that is being invoked is the
createNewResource
endpoint, a function exclusively accessible to admins.The sequence in which these middleware functions are arranged is important:
- Initially, it verifies user authentication via the
checkJwt
middleware function, - Subsequently, it verifies user authorization as it checks whether the user has the required scope through the
checkScopes
middleware function, - Finally, it redirects the flow to the
createNewResource
function, utilizing the main controller object denoted ascontroller
.
- Initially, it verifies user authentication via the
Integrating Auth0 User Login
When users access the Resource Booking app's frontend via the URL, the initial screen greets them and asks the user to log in by clicking the 'Go to Login' button.
I set up the frontend of the application using Auth0's platform, specifically utilizing the Universal Login feature, and seamlessly incorporated login options through two social connections: Google and Gmail.
Within the UserContext.jsx file, I destructured the following objects by invoking the useAuth0()
hook imported from the @auth0/auth0-react
package:
const { isAuthenticated, loginWithRedirect, logout, user, isLoading } = useAuth0();
isAuthenticated
is a boolean that evaluates to true when the user has completed the login process and false when the user has not. This distinction is vital as it prevents unauthorized users from accessing the rest of the application via URL manipulation.A state object called 'myUser' is also created. Its value is determined on the basis of the value of
isAuthenticated
and is set using thesetMyUser(user)
function in cases where the user has successfully logged in (isAuthenticated===true
), and it is set tosetMyUser(null)
when the user has not yet logged in (isAuthenticated===false
).isLoading
is a boolean flag that signifies whether data is currently being fetched from Auth0 and is also used to manage any potential errors during this process.The
logout()
function is utilized when an authenticated user wants to log out of the web application.The
UserContext.Provider
exports the following values, which play a pivotal role in several sections of the application:
value={{ myUser, loginWithRedirect, logout, isLoading, }}
Within the Login.jsx file, the following values are destructured from the UserContext:
const { myUser, loginWithRedirect, logout, isLoading } = useUserContext();
- The login page dynamically displays either the 'Logout' button when the user is already authenticated or the 'Go to Login' button when the user is not logged in. This behavior is determined by checking the value of the
myUser
state as shown in the code snippet below. This can also be achieved using theisAuthenticated
state.
{
myUser ? (
<button className="go" onClick={() => logout({ returnTo: window.location.origin })}>
Logout
</button>
) : (
<button className="go" onClick={loginPage}>
Go to Login
</button>
);
}
- When a user clicks the 'Go to Login' button, it triggers the
loginPage()
function, which in turn invokes theloginWithRedirect()
function. This function redirects the user to the login/sign-up page configured using Universal Login on the Auth0 platform as shown in the code snippet below.
const loginPage = () => {
loginWithRedirect();
};
- A logged-in user can initiate the logout process by clicking the 'Logout' button. This action invokes the
logout
function, subsequently redirecting the user to the domain specified in the URL. An option to logout is also present in the navbar for ease of access.
Securing Pages from Unauthorized Access via URL Manipulation
A vital aspect of ensuring the security of a web application is to block attempts by unauthorized users to access sensitive pages through URL manipulation. When anyone with the correct URL and path can gain entry to a page, it can lead to a lot of issues, including clients gaining unwarranted access to admin-only pages or making unauthorized API calls.
Although the backend is secured using the methods discussed previously, the frontend too, has to be secure and prohibit access to unauthenticated or unauthorized users.
To tackle this challenge, I have implemented a page routing mechanism using React Router. Within this routing framework, I've established a RootLayout responsible for rendering components based on the user's role.
Components designed for clients are rendered within the
Client.jsx
file, whereas those designed for admins reside in theAdmin.jsx
file. These client and admin components are subsequently integrated into theRootLayout.jsx
file via theOutlet
component, imported from thereact-router-dom
package as shown below:
function RootLayout() {
const { myUser } = useUserContext();
return (
<>
{myUser != null ? <NavBar /> : null}
<Toaster />
<Outlet></Outlet>
</>
);
}
- Within the
Client.jsx
file, content is dynamically rendered based on the user's role, which is extracted from therole
state that is deconstructed from thecontext.jsx
file.
if (role === 'client') {
return <>/*Client Page*/</>;
} else {
return (
<>
<p className="accessDenied">Sorry, you don't have access to this page!</p>
</>
);
}
If an unauthenticated or unauthorized user tries to access the client/admin page then the user is informed that they don't have access to that page as seen in the above code snippet.
Similarly, the admin page's security is upheld by checking whether the user's role corresponds to that of an admin and subsequently executing conditional content rendering.
Lastly, as seen in the above RootLayout function, the navigation bar's visibility depends on the user's authentication status, determined by the
myUser
state. This approach effectively prevents any blatant redirection attempts to other pages by unauthenticated users. Furthermore, the navigation bar is dynamically set based on the user's role when the user is authenticated, as shown below:
const navItems = role === 'admin' ? adminNavItems : clientNavItems;
Additional Exciting Features
Skeleton Loading Animation
React Hot Toast Notifications
Button Disabling During Database Operations
Cool animations using Framer Motion
The following video depicts all the enlisted features in action:
Links
Feel free to give it a spin:
- Access the frontend at https://resource-booking-frontend.vercel.app/
- If you think you can outsmart the secure RESTful API, try your luck at https://resource-booking-api.vercel.app/
For all the behind-the-scenes action, explore the GitHub Repository at https://github.com/anusha-c18/resource-booking-app
Conclusion
The Resource Booking App combines robust security with user-friendly features, offering a seamless experience for both clients and admins. It's a step towards simplifying resource management in shared spaces. With some modifications, this project can easily adapt to create booking systems for a wide range of applications, from reserving movie tickets to powering an e-commerce platform. Its flexibility opens doors to endless possibilities in the world of web applications.