Module 04: Admin Panel

The admin panel lets owners and admins manage dashboard user accounts — create new users, edit roles, and remove access. This module builds the user management interface and its supporting API endpoints in both Node.js and PHP.

Demo Files

1. User Management Requirements

The dashboard supports three roles, each with a distinct set of permissions:

Role View Data Manage Viewers Manage Admins Manage Owners
owner Yes Yes Yes Yes
admin Yes Yes No No
viewer Yes No No No

The owner role can do everything, including managing other owners. An admin can manage viewer accounts and view all collected data. A viewer can only view data — no user management access.

Only users with the owner or admin role can access the admin panel. Viewers who attempt to reach the admin page are redirected or shown an error.

2. The Admin Page

The admin page (admin.html) is a self-contained HTML file with two main sections: a user list table and an "Add User" form.

The user list table displays all dashboard accounts with four columns:

The table is populated on page load by fetching GET /api/users. Each row is built dynamically using textContent rather than innerHTML to prevent XSS from stored user data.

Below the table, the Add User form collects four fields:

<form id="add-user-form">
    <input type="email" id="new-email" placeholder="Email" required>
    <input type="text" id="new-name" placeholder="Display Name" required>
    <input type="password" id="new-password" placeholder="Password" required>
    <select id="new-role">
        <option value="viewer">Viewer</option>
        <option value="admin">Admin</option>
        <option value="owner">Owner</option>
    </select>
    <button type="submit">Add User</button>
</form>

When submitted, the form calls POST /api/users with the form data as JSON. On success, the table is refreshed to include the new user.

3. User CRUD API

The admin panel relies on four API endpoints that follow standard REST conventions. All endpoints require the caller to have an admin or owner role (verified via session):

Method Endpoint Description Access
GET /api/users List all users admin+ only
POST /api/users Create a new user admin+ only
PUT /api/users/:id Update user role or display name admin+ only
DELETE /api/users/:id Delete a user (cannot delete self) admin+ only

The GET response returns an array of user objects. Notice that password_hash is never included — the SELECT query explicitly lists only the safe columns:

SELECT id, email, display_name, role, created_at, last_login
FROM users
ORDER BY created_at

The POST endpoint hashes the password before storing it. The DELETE endpoint includes a self-deletion guard — it compares the target user ID against the session user ID and returns a 400 error if they match.

4. Node.js Implementation

The Node.js implementation (users-api.js) uses Express routes with a middleware function to enforce admin access:

function requireAdmin(req, res, next) {
    if (!req.session.user ||
        !['owner', 'admin'].includes(req.session.user.role)) {
        return res.status(403).json({
            success: false,
            error: 'Admin access required'
        });
    }
    next();
}

This middleware is applied to every route in the file. It checks two things: that a session exists (req.session.user is truthy) and that the user's role is either owner or admin. If either check fails, it short-circuits the request with a 403 Forbidden response.

For creating users, the password is hashed with bcrypt before storage:

const passwordHash = await bcrypt.hash(password, 10);

The second argument (10) is the cost factor — the number of rounds of hashing. Higher values are slower but more resistant to brute-force attacks. A cost of 10 takes roughly 100ms on modern hardware, which is fast enough for account creation but far too slow for an attacker to try millions of passwords.

The route also validates the role against a whitelist of valid values:

const validRoles = ['owner', 'admin', 'viewer'];
const userRole = validRoles.includes(role) ? role : 'viewer';

If the client sends an invalid role (or no role at all), it defaults to viewer — the least-privileged role. This ensures that a malicious or buggy client cannot assign arbitrary role strings.

5. PHP Implementation

The PHP implementation (users-api.php) follows the same logic using PDO for database access. The admin check happens at the top of the file, before any routing:

if (empty($_SESSION['user']) ||
    !in_array($_SESSION['user']['role'] ?? '', ['owner', 'admin'])) {
    http_response_code(403);
    echo json_encode(['success' => false, 'error' => 'Admin access required']);
    exit;
}

Because PHP scripts are typically one-file-per-endpoint (or use a simple router), the access check runs once at the top. If it fails, exit stops execution immediately — none of the routing logic below will execute.

For password hashing, PHP uses its built-in function:

$hash = password_hash($password, PASSWORD_BCRYPT);

This is equivalent to bcrypt.hash(password, 10) in Node.js. PHP's password_hash() automatically generates a salt and uses a default cost of 10. The resulting hash string contains the algorithm identifier, cost factor, salt, and hash — all in one string.

URL routing is handled by extracting the user ID from the request path:

preg_match('#/api/users/(\d+)$#', $path, $matches);
$userId = $matches[1] ?? null;

If a numeric ID is present in the URL (e.g., /api/users/42), the request is routed to the PUT or DELETE handler. If no ID is present, it goes to GET (list) or POST (create).

6. Security Considerations

User management endpoints are high-value targets. Several precautions are built into both implementations:

Duplicate email handling: Both implementations catch the database's unique constraint violation (MySQL error code ER_DUP_ENTRY / SQLSTATE 23000) and return a 409 Conflict response. This is better than checking for existence first and then inserting, which creates a race condition where two simultaneous requests could both pass the existence check.
Role escalation risk: In the simplified implementation shown here, any admin can create another admin or even an owner. In a production system, you would typically restrict role assignment — for example, only owners can create admins, and only admins or higher can create viewers. This prevents a compromised viewer account from being escalated by a compromised admin.