Building a Login System with Session Management
In this assignment, you will create a complete authentication system that allows users to log in with their registered accounts, maintains their login state across page visits, and provides secure access to protected content. This completes your user management trilogy by adding session-based authentication to the registration and contact systems you have already built.
You will implement session management using Express sessions stored in your PostgreSQL database, create middleware for protecting routes that require authentication, and build conditional navigation that adapts based on login status. This assignment emphasizes security best practices while maintaining the same MVC architecture patterns you have been using throughout this series.
AI tools could complete this assignment for you in just a few minutes. Do not give in to that temptation. If you let AI do the work for you, you will miss out on learning critical skills and put your future career at risk. The security and authentication skills you build here are fundamental to web development. Employers expect you to understand how authentication works, how to protect user data, and how to implement secure session management. If you rely on AI for these foundational skills, you may struggle to pass technical interviews or succeed in real-world development jobs. Take the time to learn and practice these concepts yourself because they are essential for your future as a developer.
Understanding Session Management
HTTP is a stateless protocol, meaning each request to your server is independent and contains no memory of previous requests. However, web applications need to remember information about users between requests, such as whether they are logged in, their preferences, or their shopping cart contents.
Sessions solve this problem by creating a temporary storage mechanism that persists across multiple requests from the same user. When a user logs in successfully, your server creates a session containing their user information. The server sends a session identifier to the user's browser as a cookie, and the browser automatically includes this identifier with every subsequent request.
Express sessions can store session data in various places: memory (which disappears when the server restarts), files, or databases. This assignment uses PostgreSQL for session storage because it provides persistence, scalability, and integration with your existing database infrastructure. The connect-pg-simple library automatically creates and manages the session table structure.
Session identifiers are essentially temporary passwords that grant access to user accounts. The express-session library automatically generates cryptographically secure session IDs and handles secure cookie transmission. Never attempt to create your own session management system, as it requires expertise in cryptography and security to implement safely.
Sessions have expiration times to limit the window of vulnerability if a session ID is compromised. This assignment configures 24-hour sessions, meaning users will need to log in again after a day of inactivity.
Preparation
1. Generate Session Secret
Before configuring session management, you need a secure session secret. This secret is used to sign and encrypt session data, protecting it from tampering. Session secrets must be cryptographically random and kept private.
Use the JWT Secrets Generator to create a random secret of at least 128 bits. Add it to your .env file:
SESSION_SECRET=your_generated_secret_here
Never commit your .env file to version control or share your session secret publicly. If your secret is compromised, all existing sessions become vulnerable to hijacking attacks. Treat your session secret with the same level of security as database passwords and API keys.
2. Install Session Packages
Install the required session management packages:
pnpm install express-session connect-pg-simple
These packages work together to provide session functionality: express-session handles the core session management, while connect-pg-simple stores session data in your PostgreSQL database and automatically creates the necessary session table.
3. Configure Session Middleware
Configure session middleware in your server.js file. Add these import statements at the top with your other imports:
import session from 'express-session';
import connectPgSimple from 'connect-pg-simple';
import { caCert } from './src/models/db.js';
Add the session configuration to your Express middleware setup. Place this code right after the app declaration const app = express(); but before your existing middleware configurations:
// Initialize PostgreSQL session store
const pgSession = connectPgSimple(session);
// Configure session middleware
app.use(session({
store: new pgSession({
conObject: {
connectionString: process.env.DB_URL,
// Configure SSL for session store connection (required by BYU-I databases)
ssl: {
ca: caCert,
rejectUnauthorized: true,
checkServerIdentity: () => { return undefined; }
}
},
tableName: 'session',
createTableIfMissing: true
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: NODE_ENV.includes('dev') !== true,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
}
}));
The session configuration includes several important security options. The httpOnly: true setting prevents JavaScript code from accessing session cookies, protecting against XSS attacks. The secure setting determines whether cookies require HTTPS; it uses HTTP during development but switches to HTTPS in production.
The maxAge setting controls how long sessions last before expiring; here it is set to 24 hours (24 * 60 * 60 * 1000 milliseconds). The resave: false prevents unnecessary session updates, and saveUninitialized: false prevents creating sessions for users who have not logged in.
4. Add Session Cleanup System
Database session storage requires maintenance to remove expired sessions. In production web applications, this is typically handled by cron jobs (scheduled tasks that run at the operating system level) or cloud-based schedulers. These external tools run independently of your application and can execute maintenance scripts on a fixed schedule.
For this course, setting up external cron jobs would require server management experience and infrastructure that is not freely available for student projects. Instead, you will implement an internal cleanup system that runs within your Node.js application. While this approach is not ideal for large-scale production systems (your cleanup stops when the server is offline), it provides a practical solution for learning environments and small-scale deployments, and it teaches you the fundamentals of automated maintenance tasks.
Create a new file at src/utils/session-cleanup.js with the following content:
import db from '../models/db.js';
/**
* Removes expired sessions from the database.
* In production, this would typically be handled by a cron job.
*/
const cleanupExpiredSessions = async () => {
try {
const result = await db.query(
`DELETE FROM session WHERE expire < NOW()`
);
if (result.rowCount > 0) {
console.log(`Cleaned up ${result.rowCount} expired sessions`);
}
} catch (error) {
// Check if the error is due to the session table not existing (PostgreSQL error code 42P01)
if (error.code === '42P01') {
console.log('Session table does not exist yet:\n→ It will be created when the first session is initialized.');
return;
}
// Log actual errors
console.error('Error cleaning up sessions:', error);
}
};
/**
* Starts automatic session cleanup that runs every 24 hours.
* Runs immediately on startup to handle any sessions that expired while server was offline.
*/
const startSessionCleanup = () => {
// Run cleanup immediately on startup (catches sessions that expired while offline)
cleanupExpiredSessions();
// Schedule cleanup to run every 12 hours
const twelveHours = 12 * 60 * 60 * 1000;
setInterval(cleanupExpiredSessions, twelveHours);
console.log('Session cleanup scheduled to run every 12 hours');
};
export { startSessionCleanup };
Now integrate the cleanup system into your server startup. In server.js, add this import at the top with your other imports:
import { startSessionCleanup } from './src/utils/session-cleanup.js';
Add this line right after your session middleware configuration:
// Start automatic session cleanup
startSessionCleanup();
This cleanup function deletes sessions that have passed their expiration time, preventing your session table from growing indefinitely. The cleanup runs immediately when your server starts (handling any sessions that expired while the server was offline) and then runs every 12 hours automatically. The connect-pg-simple library automatically sets expiration times based on your maxAge configuration (24 hours in this case).
Because the cleanup runs on startup, even if your server goes offline for days or weeks, expired sessions will be cleaned up as soon as the server comes back online. The interval timer then ensures regular maintenance while the server is running.
5. Update Global Middleware
Update your global middleware to include authentication state management. Open src/middleware/global.js and add this code in the addLocalVariables function:
// Convenience variable for UI state based on session state
res.locals.isLoggedIn = false;
if (req.session && req.session.user) {
res.locals.isLoggedIn = true;
}
The isLoggedIn variable is a convenience feature for displaying appropriate navigation links and UI elements. However, hiding navigation links does not provide security because users can still type URLs directly into their browser. Any route that truly requires authentication must be protected with the requireLogin middleware you will create later in this assignment. This middleware performs the actual security check and redirects unauthorized users to the login page.
6. Restart and Verify
Restart your server and check the console for the session cleanup scheduling message. The connect-pg-simple session table is created lazily on first use (when a session is stored), so don't be alarmed if the table doesn't appear immediately. The goal for now is that startup produces no errors. Later in this assignment, you will create login functionality that stores sessions and populates the session table.
Assignment Instructions
1. Set Up Dynamic CSS Loading
Add middleware to your routes configuration to load the login CSS. Open src/controllers/routes.js and add the following middleware with your other route-specific middleware:
// Add login-specific styles to all login routes
router.use('/login', (req, res, next) => {
res.addStyle('<link rel="stylesheet" href="/css/login.css">');
next();
});
This follows the same pattern you used in the contact form and registration assignments, ensuring the login CSS loads for all routes under /login.
2. Create Login Styles
Create public/css/login.css with comprehensive styling for the login system:
/* Login Form Styles */
.login-form {
max-width: 500px;
margin: 2rem auto;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
h2 {
text-align: center;
color: #2c5aa0;
margin-top: 0;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
font-family: inherit;
box-sizing: border-box;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
&:focus {
outline: none;
border-color: #2c5aa0;
box-shadow: 0 0 5px rgba(44, 90, 160, 0.3);
}
}
}
button[type="submit"] {
width: 100%;
padding: 1rem;
background-color: #2c5aa0;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #1e3a6f;
}
&:disabled {
background-color: #ccc;
cursor: not-allowed;
}
}
.form-footer {
margin-top: 1.5rem;
text-align: center;
font-size: 0.9rem;
a {
color: #2c5aa0;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
/* Dashboard Styles */
.dashboard {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
h1 {
color: #2c5aa0;
margin-bottom: 1rem;
}
& > p {
margin-bottom: 2rem;
color: #666;
}
.user-info,
.session-debug {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
h2,
h3 {
color: #2c5aa0;
margin-top: 0;
margin-bottom: 1rem;
}
.user-detail {
margin-bottom: 0.5rem;
font-size: 1rem;
.label {
font-weight: bold;
color: #495057;
display: inline-block;
width: 120px;
}
.value {
color: #333;
}
}
}
.session-debug {
pre {
background-color: #f1f3f4;
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 1rem;
margin: 0;
font-size: 0.9rem;
line-height: 1.4;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
}
.dashboard-actions {
text-align: center;
.logout-button {
display: inline-block;
background-color: #dc3545;
color: white;
padding: 0.75rem 2rem;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
background-color: #c82333;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-decoration: none;
color: white;
}
&:active {
transform: translateY(1px);
}
}
}
}
/* Error and success messages */
.auth-message {
max-width: 500px;
margin: 1rem auto;
padding: 1rem;
border-radius: 6px;
text-align: center;
font-weight: 500;
&.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
&.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
}
/* Responsive design */
@media (max-width: 768px) {
.login-form,
.dashboard {
margin: 1rem;
padding: 1rem;
}
.user-detail .label {
width: 100px;
}
}
3. Update Navigation with Conditional Display
Modify your site navigation to show different links based on authentication status. Open src/views/partials/header.ejs and replace your entire navigation section with this conditional structure:
<nav>
<ul>
<!-- Always visible links -->
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/catalog">Course Catalog</a></li>
<li><a href="/faculty">Faculty Directory</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/demo">Middleware Demo</a></li>
<!-- Only visible when logged in -->
<% if (isLoggedIn) { %>
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/register/list">All Users</a></li>
<li><a href="/contact/responses">Contact Responses</a></li>
<li><a href="/logout">Logout</a></li>
<% } %>
<!-- Only visible when NOT logged in -->
<% if (!isLoggedIn) { %>
<li><a href="/login">Login</a></li>
<li><a href="/register">Register</a></li>
<% } %>
</ul>
</nav>
This navigation structure uses the isLoggedIn variable from your global middleware to control which links are visible. Core site features like the course catalog and faculty directory remain accessible to all users, while administrative pages like the users list and contact responses are only visible to authenticated users. The authentication links (login and register) are hidden once users are logged in, replaced with the dashboard and logout options.
The isLoggedIn variable is a convenience feature for displaying appropriate navigation links and UI elements. However, hiding navigation links does not provide security because users can still type URLs directly into their browser.
Any route that truly requires authentication must be protected with the requireLogin middleware. This middleware performs the actual security check and redirects unauthorized users to the login page. For now, the dashboard is the only protected route. You will refactor other routes to use this middleware in future assignments.
4. Create Authentication Middleware
Create reusable middleware for protecting routes that require authentication. Create a new file at src/middleware/auth.js:
/**
* Middleware to require authentication for protected routes.
* Redirects to login page if user is not authenticated.
* Sets res.locals.isLoggedIn = true for authenticated requests.
*/
const requireLogin = (req, res, next) => {
// Check if user is logged in via session; we can beef this up later with roles and permissions
if (req.session && req.session.user) {
// User is authenticated - set UI state and continue
res.locals.isLoggedIn = true;
next();
} else {
// User is not authenticated - redirect to login
res.redirect('/login');
}
};
export { requireLogin };
This middleware function checks for a user session and either allows the request to continue (for authenticated users) or redirects to the login page (for unauthenticated users). By placing it in a separate middleware file, you can easily import and use it on any route that requires authentication.
Authentication middleware belongs in its own file because it provides functionality that many different parts of your application may need. Routes, controllers, and other middleware can all import and use authentication functions without creating circular dependencies or architectural confusion. This separation of concerns makes your code more maintainable and testable. If you need to modify authentication logic later, you only need to change it in one place.
Do not remove or alter the existing global middleware in src/middleware/global.js. The isLoggedIn variable set there is still needed for conditional UI rendering, even though the actual security checks are handled by the new requireLogin middleware.
5. Create the Login Model
Create the database interaction layer for login operations. Create a new file at src/models/forms/login.js with the following content:
import bcrypt from 'bcrypt';
import db from '../db.js';
/**
* Find a user by email address for login verification.
*
* @param {string} email - Email address to search for
* @returns {Promise<Object|null>} User object with password hash or null if not found
*/
const findUserByEmail = async (email) => {
// TODO: Write SELECT query for id, name, email, password, created_at
// TODO: Use LOWER() on both sides for case-insensitive email comparison
// TODO: Use $1 placeholder for email parameter
// TODO: Add LIMIT 1 to ensure only one result
// TODO: Execute query and return first row or null
};
/**
* Verify a plain text password against a stored bcrypt hash.
*
* @param {string} plainPassword - The password to verify
* @param {string} hashedPassword - The stored password hash
* @returns {Promise<boolean>} True if password matches, false otherwise
*/
const verifyPassword = async (plainPassword, hashedPassword) => {
// TODO: Use bcrypt.compare() to verify the password
// TODO: Return the result (true/false)
};
export { findUserByEmail, verifyPassword };
Complete the TODO items in each function. For findUserByEmail, use LOWER(email) = LOWER($1) to perform case-insensitive email matching. The verifyPassword function should use bcrypt.compare(plainPassword, hashedPassword) which returns a promise that resolves to a boolean.
Notice that these model functions do not include try-catch blocks. Your application uses a global error handler to catch and process all errors, including database query errors and bcrypt failures. This keeps your model layer clean and focused on data operations, while error handling logic remains centralized in your error handling middleware.
6. Create the Login Controller
Create the route handlers for authentication. Create a new file at src/controllers/forms/login.js with the following content:
import { body, validationResult } from 'express-validator';
import { findUserByEmail, verifyPassword } from '../../models/forms/login.js';
import { Router } from 'express';
const router = Router();
/**
* Validation rules for login form
*/
const loginValidation = [
body('email')
.trim()
.isEmail()
.withMessage('Please provide a valid email address')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('Password is required')
];
/**
* Display the login form.
*/
const showLoginForm = (req, res) => {
// TODO: Render the login form view (forms/login/form)
// TODO: Pass title: 'User Login'
};
/**
* Process login form submission.
*/
const processLogin = async (req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
// TODO: Log validation errors to console
// TODO: Redirect back to /login
return;
}
// TODO: Extract email and password from req.body
try {
// TODO: Find user by email using findUserByEmail()
// TODO: If not found, log "User not found" and redirect to /login
// TODO: Verify password using verifyPassword(password, user.password)
// TODO: If password incorrect, log "Invalid password" and redirect to /login
// SECURITY: Remove password from user object before storing in session
delete user.password;
// TODO: Store user in session: req.session.user = user
// TODO: Redirect to /dashboard
} catch (error) {
// Model functions do not catch errors, so handle them here
// TODO: Log error to console
// TODO: Redirect to /login
}
};
/**
* Handle user logout.
*
* NOTE: connect.sid is the default session cookie name since we did not
* specify a custom name when creating the session in server.js.
*/
const processLogout = (req, res) => {
// First, check if there is a session object on the request
if (!req.session) {
// If no session exists, there's nothing to destroy,
// so we just redirect the user back to the home page
return res.redirect('/');
}
// Call destroy() to remove this session from the store (PostgreSQL in our case)
req.session.destroy((err) => {
if (err) {
// If something goes wrong while removing the session from the database:
console.error('Error destroying session:', err);
/**
* Clear the session cookie from the browser anyway, so the client
* does not keep sending an invalid session ID.
*/
res.clearCookie('connect.sid');
/**
* Normally we would respond with a 500 error since logout did not fully succeed.
* Example: return res.status(500).send('Error logging out');
*
* Since this is a practice site, we will redirect to the home page anyway.
*/
return res.redirect('/');
}
// If session destruction succeeded, clear the session cookie from the browser
res.clearCookie('connect.sid');
// Redirect the user to the home page
res.redirect('/');
});
};
/**
* Display protected dashboard (requires login).
*/
const showDashboard = (req, res) => {
const user = req.session.user;
const sessionData = req.session;
// Security check! Ensure user and sessionData do not contain password field
if (user && user.password) {
console.error('Security error: password found in user object');
delete user.password;
}
if (sessionData.user && sessionData.user.password) {
console.error('Security error: password found in sessionData.user');
delete sessionData.user.password;
}
// TODO: Render the dashboard view (dashboard)
// TODO: Pass title: 'Dashboard', user, and sessionData to template
};
// Routes
router.get('/', showLoginForm);
router.post('/', loginValidation, processLogin);
// Export router as default, and specific functions for root-level routes
export default router;
export { processLogout, showDashboard };
Complete all the TODO items following these guidelines:
-
For session management, store user data without the password field:
{ id, name, email, created_at } - Use generic error messages for users ("Invalid email or password") but specific console logs for debugging
-
The
processLoginfunction includes try-catch because the model functions do not handle errors themselves. Your global error handler will catch unhandled errors, but the controller needs to catch expected errors (like user not found or invalid password) to provide appropriate user feedback. -
Ensure you understand what the
processLogoutfunction is doing by reading the comments carefully -
In
showDashboard, verify that password is not in the user or sessionData objects as a security check
7. Integrate Authentication Routes
Connect your authentication controller to the main routing system. Open src/controllers/routes.js and add these imports at the top with your other imports (remember that { Router } should be the last import):
import loginRoutes from './forms/login.js';
import { processLogout, showDashboard } from './forms/login.js';
import { requireLogin } from '../middleware/auth.js';
Add these routes to your router after your other route definitions:
// Login routes (form and submission)
router.use('/login', loginRoutes);
// Authentication-related routes at root level
router.get('/logout', processLogout);
router.get('/dashboard', requireLogin, showDashboard);
The login form and submission routes are grouped together under /login. However, logout and dashboard exist at the root level because they are not logically "under" the login path. The dashboard uses the requireLogin middleware to ensure only authenticated users can access it. This structure keeps things simple for now; in a future refactoring assignment, you will reorganize routes when you have multiple dashboard-related pages.
8. Create Login Views
Build the user interface for login and dashboard. Create the directory src/views/forms/login/ and create form.ejs in that directory:
<%- include('../../partials/header') %>
<main>
<h1><%= title %></h1>
<p>
Enter your email and password to access your account. Do not have an account? <a href="/register">Register here</a>.
</p>
<form method="POST" action="/login" class="login-form">
<!-- Email field -->
<div class="form-group">
<!-- TODO: Add label for email -->
<!-- TODO: Add input type="email" with id, name, and required -->
<!-- TODO: Add appropriate placeholder text -->
</div>
<!-- Password field -->
<!-- TODO: Create form-group div -->
<!-- TODO: Add label for password -->
<!-- TODO: Add input type="password" with id, name, minlength="8", and required -->
<!-- TODO: Add appropriate placeholder text -->
<!-- Submit button -->
<!-- TODO: Add button type="submit" with text "Sign In" or "Login" -->
<div class="form-footer">
<p>New user? <a href="/register">Create an account</a></p>
</div>
</form>
</main>
<%- include('../../partials/footer') %>
Create the form following the same structure as your registration form, but with only two fields: email and password. Include client-side validation attributes that match the server-side rules.
Now create src/views/dashboard.ejs for the protected page:
<%- include('partials/header') %>
<main>
<div class="dashboard">
<h1>Welcome to Your Dashboard</h1>
<p>
This is a protected page that requires authentication. Only logged-in users can access this content.
</p>
<div class="user-info">
<h2>Your Account Information</h2>
<!-- TODO: Display user.name in a div with class="user-detail" -->
<!-- TODO: Use span with class="label" for "Name:" and span with class="value" for the actual name -->
<!-- TODO: Display user.email in the same format -->
<!-- TODO: Display user.created_at formatted with toLocaleDateString() -->
</div>
<div class="session-debug">
<h3>Session Information (For Learning Purposes)</h3>
<p><em>This section shows your current session data for educational purposes.</em></p>
<pre><%= JSON.stringify(sessionData, null, 2) %></pre>
<p>
<strong>If you see a password property in the above object you have a security vulnerability, fix it!</strong>
</p>
</div>
<div class="dashboard-actions">
<a href="/logout" class="logout-button">Logout</a>
</div>
</div>
</main>
<%- include('partials/footer') %>
Complete the TODOs to display user information and session data. The user information should follow the CSS structure with user-detail, label, and value classes. The session debug section helps you verify that your session is working correctly and does not contain sensitive information like passwords.
9. Test Your Authentication System
Thoroughly test your complete authentication system:
- Visit http://127.0.0.1:3000/login and verify the login form displays correctly.
- Test invalid login attempts with non-existent emails and wrong passwords. Check your server console for appropriate error messages.
- Register a new user if needed, then log in with those credentials.
- After successful login, verify you are redirected to the dashboard and can see your user information and session data. Confirm there is no password property in the session debug output.
- Check that the navigation changes to show dashboard and logout options when logged in.
- Try accessing http://127.0.0.1:3000/dashboard in an incognito window. You should be redirected to the login page.
- Test the logout functionality and verify the session is destroyed. After logout, try accessing the dashboard again and confirm you are redirected to login.
- Close and reopen your browser, then return to your site to verify session persistence across browser sessions.
Use pgAdmin to examine the session table and verify that session data is being stored properly. The session table should contain entries with session IDs, encrypted session data, and expiration timestamps. You should also see console messages about session cleanup running on startup and periodically.
Security testing is crucial for authentication systems. Always test both valid and invalid scenarios, verify that protected routes are actually protected, and confirm that sessions are properly created and destroyed. Pay attention to console messages to ensure validation and authentication logic are working correctly. Most importantly, verify that passwords never appear in session data or logs.
Key Concepts Summary
This assignment completed your understanding of full-stack web authentication by implementing session management, route protection, and stateful user interactions. You learned how Express sessions bridge the gap between HTTP's stateless nature and the need for persistent user state in web applications.
The middleware patterns you implemented demonstrate separation of concerns in web security. The requireLogin middleware provides reusable authentication protection that can be applied to any route, while the global middleware manages UI state for a consistent user experience.
Your conditional navigation illustrates the difference between user interface convenience and actual security measures. While the isLoggedIn variable helps create appropriate user experiences, the requireLogin middleware provides the real security by verifying authentication on the server side.
The session storage integration with PostgreSQL provides persistent, scalable session management that survives server restarts and can handle multiple concurrent users. The automated cleanup system you implemented mimics production maintenance practices, teaching you about scheduled tasks and database maintenance even though you used an internal solution rather than external cron jobs.
Experiment and Extend
Try adding additional protected routes using the requireLogin middleware. Apply this middleware to the users list page (/register/list) and contact responses page (/contact/responses) to see how route protection scales across your application.
Experiment with different session duration settings to understand the trade-offs between user convenience and security. Try implementing "remember me" functionality by conditionally extending session duration based on a checkbox in the login form.