W05 Team Activity: Admin Role
Overview
For this team activity, you will meet for a 1-hour synchronous team meeting using Microsoft Teams video sharing.
Please make every effort to attend this meeting. If you cannot attend, you must complete the Absent from Meeting Checklist.
In this activity you will implement an admin role and restrict access to certain pages based on that role.
Activity Instructions
Before the Meeting
Before the meeting begins, each person should individually do the following:
- Complete all of the Learning Activities for this week.
For the meeting, follow these steps.
Determine the Leader for the Meeting
Choose one person that will be the leader for this meeting. Their role will be to help guide the rest of the team through the steps of the meeting. Try to rotate so that each person gets a chance to be the leader of at least one meeting.
Begin with Prayer
One person on the team should begin the meeting with a prayer.
Code Review (10 minutes)
Select one person to share their screen and share some of their code from the last week. (Try to rotate so that each person gets a chance to share at least once during the course.)
- What was particularly well done or interesting?
- Do you have any suggestions for improvement?
Understand the Requirements
In this activity you will add the following new features:
- Create a middleware function called
requireRolethat checks if a logged-in user has a specific role. - Protect admin-only routes using the
requireRolemiddleware. - Update the UI to only show admin links to admin users.
Group Discussion
- What is the difference between authentication and authorization?
Add the Admin Role to an Existing User
Before you can test role-based authorization, you need at least one user account that has the admin role. You will use a SQL statement to update an existing user account in your database.
Complete the following:
- Open pgAdmin and connect to your application database.
- Verify that you have a user account to update and an admin role in the database.
- You can use a SELECT statement to view all users:
SELECT * FROM users; - You can use a SELECT statement to view all roles:
SELECT * FROM roles;
- You can use a SELECT statement to view all users:
- Determine the user account that you want to give admin permissions.
- Run an UPDATE statement to set the role_id to the id of the admin role by using a subquery. The statement should look similar to this (replace the
user_idwith the one that you want to update):UPDATE users SET role_id = (SELECT role_id FROM roles WHERE role_name = 'admin') WHERE user_id = 1; - Verify that the update was successful by running the SELECT statement again.
Note About Role Management
For simplicity, you will not implement a full role management system in the application at this point. In a real application, you would create admin pages where authorized users could assign roles to other users. For now, you will manage roles directly in the database.
Sample Code (click to expand)
-- View all users and roles
SELECT * FROM users;
SELECT * FROM roles;
-- Update a specific user to have admin role
UPDATE users SET role_id = (SELECT role_id FROM roles WHERE role_name = 'admin') WHERE user_id = 1;
-- Verify the update by listing all users and their roles
SELECT users.user_id, users.email, roles.role_name FROM users JOIN roles ON users.role_id = roles.role_id;
Update Session Data to Include User Role
Previously, when a user logged in, you added the user data to the session, but it did not include the user's role. You will now update the session data to include the user's role to make it easier to user later.
Complete the following:
- Open the
src/models/users.jsusers model file. - Locate the
findUserByEmailfunction. - Update the SQL statement so that instead of returning the role_id, it returns the role_name by joining the roles table. The new SQL should look something like the following:
const query = ` SELECT u.user_id, u.email, u.password_hash, r.role_name FROM users u JOIN roles r ON u.role_id = r.role_id WHERE u.email = $1 `; - Now when the user object is return and placed on the session, it should include the
role_nameproperty.
Create the RequireRole Middleware Function
Now you will create a middleware function that checks if a logged-in user has a specific role. This function will be used to protect admin-only routes.
This function is a little different than a typical middleware function, because it is actually a middleware function factory, or function that returns a middleware function.
Complete the following:
- Open the
src/controllers/users.jsusers controller file. - Create a new function called
requireRole. This function should accept a parameter calledrolethat specifies which role is required. - Inside the function, return another function that has the standard middleware parameters:
req,res, andnext. - In the inner function, check if
req.session.userexists and ifreq.session.user.role_namematches the required role. - If the user has the required role, call
next()to allow the request to continue. - If the user does not have the required role, set an error flash message and redirect to the root
/page. - Export the
requireRolefunction so it can be used in your route files.
Sample Code (click to expand)
/**
* Middleware factory to require specific role for route access
* Returns middleware that checks if user has the required role
*
* @param {string} role - The role name required (e.g., 'admin', 'user')
* @returns {Function} Express middleware function
*/
const requireRole = (role) => {
return (req, res, next) => {
// Check if user is logged in first
if (!req.session || !req.session.user) {
req.flash('error', 'You must be logged in to access this page.');
return res.redirect('/login');
}
// Check if user's role matches the required role
if (req.session.user.role_name !== role) {
req.flash('error', 'You do not have permission to access this page.');
return res.redirect('/');
}
// User has required role, continue
next();
};
};
Group Discussion
- Why can the requireRole function not be a standard middleware function (why must it be a function factory)?
Protect Admin-Only Routes
Now you will apply the requireRole middleware to routes that should only be accessed by admin users. This includes all routes for managing organizations, projects, and categories.
Complete the following:
- Open the
src/controllers/routes.jsroute file. - Import
requireRolemiddleware function at the top of the file. - For each route that should be admin-only, add
requireRole('admin')to the route middleware chain. - For example, the
/new-organizationroutes should now look as follows:// Route for new organization page router.get('/new-organization', requireRole('admin'), newOrganizationPage); // Route to handle new organization form submission router.post('/new-organization', requireRole('admin'), organizationValidation, processNewOrganizationForm); - Apply this protection to all routes related to creating or updating organizations, projects, and categories. This should include:
- Creating a new organization
- Editing an existing organization
- Creating a new project
- Editing an existing project
- Creating a new category
- Editing an existing category
- Assigning categories to projects
For each of these actions, make sure to add
requireRole('admin')to the route middleware chain for both the GET and POST routes. - Test the routes by trying to access them without logging in, then by logging in as a regular user, and finally by logging in as an admin user.
Update Navigation to Show Admin Options Conditionally
The final step is to update your navigation menu so that admin options are only visible to users with the admin role. This provides a better user experience because regular users will not see links to pages they cannot access.
Complete the following:
- Open your
server.jsfile. - Locate the middleware that sets the
res.locals.isLoggedInvariable. - Add code to this function to set a
res.locals.uservariable that contains the current user's session data if they are logged in. This will make the user's role available in all templates.Sample Code (click to expand)
// Middleware to set res.locals variables for to all templates app.use((req, res, next) => { res.locals.isLoggedIn = false; if (req.session && req.session.user) { res.locals.isLoggedIn = true; } res.locals.user = req.session.user || null; res.locals.NODE_ENV = NODE_ENV; next(); }); - Open your main organization list View:
views/organizations.ejs. - Find the link to the new organization page.
- Add code around it that conditionally displays the link only if the user has the admin role. It should now look like the following:
<% if (user && user.role_name === 'admin') { %> <a href="/new-organization">Add New Organization</a> <% } %> - Repeat this process for other admin-only links in your navigation including:
Page Action to Restrict /organization/[id]Edit organization /projectsAdd new project /project/[id]Edit project /project/[id]Edit project categories /categoriesAdd new category /category/[id]Edit category
Group Discussion
- Why is it not sufficient to hide admin links in the navigation for security (why must they also be protected on the server)?
Deploy and test
Once you have everything working, deploy your code to your hosting server and verify that it works there.
Conclude the meeting
You have now completed all the steps of the activity.
Make sure to finish on your own if needed
You need to complete all the steps in this team activity before submitting your quiz. If you are not able to finish some of the steps during your meeting, you will need to finish them on your own after the meeting is over.
Submission
After you have completed all the steps for this team activity, return to Canvas to submit the associated quiz.
Other Links:
- Return to: Week Overview | Course Home