The dashboard is for authorized users only. This module builds a login page that authenticates against the reporting API, stores the session, and redirects to the dashboard overview.
Authentication follows a straightforward cookie-based session pattern. The browser sends credentials once, the server validates them and sets a session cookie, and every subsequent request includes that cookie automatically.
Key points:
users table.Set-Cookie header. The browser stores this cookie and includes it in every subsequent request to the same origin.The login page (login.html) is a self-contained HTML file with no framework dependencies. It presents a centered card with two inputs and a submit button:
<form id="login-form">
<h1>Analytics Dashboard</h1>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email"
required autocomplete="email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password"
required autocomplete="current-password">
</div>
<div id="error-message" class="error" hidden></div>
<button type="submit">Sign In</button>
</form>
Design decisions:
type="email" — Triggers email-specific keyboard on mobile and provides built-in browser validation.autocomplete attributes — Tells the browser which credential manager fields to fill. Using email and current-password enables password managers to autofill correctly.required — Prevents empty submissions without any JavaScript.hidden — The error message area is invisible by default. JavaScript removes the hidden attribute and sets the text content when authentication fails.The card is centered using a simple CSS technique: max-width: 400px on the form with margin: 0 auto, placed inside a flex container that centers vertically.
The form is submitted via JavaScript fetch() rather than a traditional form POST. This gives us control over the request format and error handling:
const form = document.getElementById('login-form');
const errorDiv = document.getElementById('error-message');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (res.ok && data.success) {
window.location.href = '../02-overview-page/dashboard.html';
} else {
errorDiv.textContent = data.error || 'Login failed';
errorDiv.hidden = false;
}
} catch (err) {
errorDiv.textContent = 'Network error. Please try again.';
errorDiv.hidden = false;
}
});
Line-by-line walkthrough:
e.preventDefault() — Stops the browser from performing a traditional form submission (which would navigate to a new page). We handle the request ourselves.credentials: 'include' — Tells fetch() to include cookies in the request and accept Set-Cookie headers in the response. Without this, the session cookie from the server would be silently ignored.Content-Type: application/json — The server expects a JSON body, not URL-encoded form data.res.ok — A shorthand that is true when the HTTP status is in the 200-299 range.catch block handles network failures (server down, no connectivity). The else branch handles application-level errors (wrong password, missing fields).<form method="POST"> submission navigates to the action URL. If the login fails, the server would need to render an HTML error page or redirect back with query parameters. Using fetch() keeps the user on the same page and lets us show inline error messages without a full page reload.
After a successful login, the server sets a session cookie (e.g., connect.sid in Express, PHPSESSID in PHP). The browser sends this cookie automatically with every subsequent request to the same origin — no JavaScript required.
To protect dashboard pages from unauthorized access, each page checks authentication status on load:
// Run on every dashboard page load
async function checkAuth() {
try {
const res = await fetch('/api/dashboard', {
credentials: 'include'
});
if (res.status === 401) {
window.location.href = '/project/dashboard/01-login/login.html';
return null;
}
return await res.json();
} catch (err) {
window.location.href = '/project/dashboard/01-login/login.html';
return null;
}
}
// Usage: on page load
checkAuth().then(data => {
if (data) {
// Populate dashboard with data
}
});
This pattern works as follows:
GET /api/dashboard request. The browser includes the session cookie automatically.The dashboard header includes a "Sign Out" button. Clicking it calls the logout endpoint and redirects to the login page:
document.getElementById('logout-btn').addEventListener('click', async () => {
await fetch('/api/logout', {
method: 'POST',
credentials: 'include'
});
window.location.href = '/project/dashboard/01-login/login.html';
});
On the server side, the logout handler destroys the session:
req.session.destroy() removes the session from the store and invalidates the cookie.session_destroy() deletes the session data on the server. The cookie remains in the browser but refers to a session that no longer exists.The POST method is intentional. Logout modifies server state (destroying a session), so it should be a POST, not a GET. Using GET for logout creates a vulnerability: an attacker could embed <img src="/api/logout"> on any page to forcibly log users out (a form of CSRF).
The reference file login-api.js shows the Express routes for login and logout, extracted from the full reporting API. Here is the login handler:
router.post('/api/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password required'
});
}
const pool = req.app.get('pool');
const [rows] = await pool.execute(
'SELECT id, email, password_hash, display_name, role FROM users WHERE email = ?',
[email]
);
if (rows.length === 0 || !(await bcrypt.compare(password, rows[0].password_hash))) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
const user = rows[0];
req.session.user = {
id: user.id,
email: user.email,
displayName: user.display_name,
role: user.role
};
res.json({ success: true, data: req.session.user });
});
Key details:
? placeholder in the SQL prevents SQL injection. The email value is passed as a parameter, not concatenated into the query string.bcrypt.compare() — Compares the plaintext password against the stored bcrypt hash. This function is intentionally slow (by design) to resist brute-force attacks.req.session.user — Express-session stores this object server-side (in memory, Redis, or a database) and associates it with the session cookie.The PHP equivalent (login-api.php) uses built-in PHP functions for session management and password verification:
session_start();
$stmt = $pdo->prepare(
'SELECT id, email, password_hash, display_name, role FROM users WHERE email = ?'
);
$stmt->execute([$email]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Invalid credentials']);
exit;
}
session_regenerate_id(true);
$_SESSION['user'] = [
'email' => $user['email'],
'displayName' => $user['display_name'],
'role' => $user['role']
];
echo json_encode(['success' => true, 'data' => $_SESSION['user']]);
PHP-specific notes:
session_start() — Must be called before accessing $_SESSION. It reads the session cookie from the request, loads the session data from the server's session store (files by default), and makes it available as a superglobal array.password_verify() — PHP's built-in bcrypt verification function. It extracts the algorithm and cost factor from the stored hash automatically.session_regenerate_id(true) — Generates a new session ID after successful login. The true parameter deletes the old session file. This prevents session fixation attacks, where an attacker sets a known session ID before the user logs in.? placeholder prevents SQL injection.session_regenerate_id(), an attacker could set a known PHPSESSID cookie in the victim's browser (via XSS or a subdomain). When the victim logs in, the attacker's known session ID would now be associated with an authenticated session. Regenerating the ID on login invalidates any pre-set session IDs.