Module 01: Login & Authentication

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.

Demo Files

1. Login Flow

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.

Browser Server ┌──────────────────┐ ┌──────────────────┐ │ User enters │ POST /api/login │ │ │ email + password │ ──────────────────▶ │ Validate creds │ │ │ {email, password} │ against database │ │ │ │ │ │ │ Set-Cookie: sid=… │ Create session │ │ │ ◀────────────────── │ in store │ │ Redirect to │ │ │ │ /dashboard │ │ │ └──────────────────┘ └──────────────────┘ ┌──────────────────┐ ┌──────────────────┐ │ Subsequent │ GET /api/dashboard │ │ │ requests include │ ──────────────────▶ │ Validate session │ │ Cookie: sid=… │ (cookie sent │ from cookie │ │ automatically │ automatically) │ │ │ │ 200 OK + data │ │ │ │ ◀────────────────── │ Return dashboard │ └──────────────────┘ └──────────────────┘

Key points:

2. The Login Page

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:

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.

3. Form Submission

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:

Why not a traditional form POST? A traditional <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.

4. Session Persistence

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:

This is not security. Client-side redirects are a convenience, not a protection. The real security boundary is the server: the API endpoint must verify the session and return 401 if it is invalid. A user could easily disable JavaScript or modify the redirect logic. The server must never trust the client.

5. Logout

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:

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).

6. Node.js Auth Routes

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:

7. PHP Auth Routes

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 fixation prevention: Without 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.