Skip to main content

Routing System

The central router (App/router.php) is “The Guard” of Zoo Arcadia. After the front controller parses the URL, the central router handles:
  • Session management and timeout
  • Authentication verification
  • Public vs. protected route enforcement
  • Domain whitelist validation
  • Delegation to domain-specific routers

Central Router Overview

The central router acts as a security gateway before any domain logic executes:
App/router.php (simplified)
<?php
session_start();

// 1. Disable browser cache
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");

$domain = $_GET["domain"] ?? "home";

// 2. Define public domains (no authentication required)
$public_domains = [
    "auth", "contact", "home", "about", 
    "habitats", "animals", "cms", "testimonials"
];

// 3. Session timeout check (11 hours)
if (isset($_SESSION["user"]["username"])) {
    $sessionTimeout = 39600; // 11 hours in seconds
    $lastActivity = $_SESSION["last_activity"] ?? time();
    
    if (time() - $lastActivity > $sessionTimeout) {
        // Session expired: destroy and redirect
        $_SESSION = array();
        session_destroy();
        header("Location: /auth/pages/login?msg=session_expired");
        exit();
    } else {
        // Update last activity time
        $_SESSION["last_activity"] = time();
    }
}

// 4. Main authentication check
if (!isset($_SESSION["user"]["username"]) && !in_array($domain, $public_domains)) {
    header("Location: /auth/pages/login");
    exit();
}

// 5. Load domain router
$allowed_domains = [
    "auth", "home", "animals", "habitats", "users",
    "employees", "testimonials", "contact", "roles",
    "permissions", "schedules", "reports", "vreports"
];

if (in_array($domain, $allowed_domains)) {
    $domainRouterPath = __DIR__ . "/{$domain}/{$domain}Router.php";
    
    if (file_exists($domainRouterPath)) {
        require_once $domainRouterPath;
    } else {
        http_response_code(500);
        header('Location: /public/error-500.php');
        exit();
    }
} else {
    http_response_code(404);
    header('Location: /public/error-404.php');
    exit();
}

Authentication Flow

Here’s how authentication is enforced:

Public vs. Protected Domains

The router distinguishes between public and protected domains:
$public_domains = [
    "auth",          // Login, logout, register
    "contact",       // Contact form
    "home",          // Homepage (except /home/pages/start dashboard)
    "about",         // About page
    "habitats",      // Public habitat listing
    "animals",       // Public animal listing
    "cms",           // CMS pages
    "testimonials"   // Public testimonials display
];
Some public domains have mixed routes. For example, animals is public, but /animals/gest/create (management) requires authentication. The router includes specific checks for these cases.

Session Timeout Management

The router implements an 11-hour session timeout:
if (isset($_SESSION["user"]["username"])) {
    $sessionTimeout = 39600; // 11 hours in seconds
    $lastActivity = $_SESSION["last_activity"] ?? time();
    
    if (time() - $lastActivity > $sessionTimeout) {
        // Session expired: destroy completely
        $_SESSION = array();
        
        // Also delete the session cookie
        if (ini_get("session.use_cookies")) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $params["path"], $params["domain"],
                $params["secure"], $params["httponly"]
            );
        }
        
        session_destroy();
        header("Location: /auth/pages/login?msg=session_expired");
        exit();
    } else {
        // Update last activity time
        $_SESSION["last_activity"] = time();
    }
    
    // Validate session data integrity
    if (!isset($_SESSION["user"]["id_user"]) || 
        !isset($_SESSION["loggedin"]) || 
        $_SESSION["loggedin"] !== true) {
        // Invalid session: destroy it
        $_SESSION = array();
        session_destroy();
        header("Location: /auth/pages/login?msg=invalid_session");
        exit();
    }
}
The 11-hour timeout represents a full working day plus buffer:
  • 8-hour workday: Standard shift for zoo employees
  • 3-hour buffer: Lunch breaks, meetings, idle time
  • Security balance: Long enough for usability, short enough for security
If a user is inactive for 11 hours, their session expires and they must log in again. However, each action they take resets the timer.

Granular Route Protection

Some domains are public but have protected controllers. The router includes specific checks:

Protected Routes in Public Domains

// Global protection for management controllers
$controller = $_GET['controller'] ?? '';
if ($controller === 'gest' && !isset($_SESSION["user"]["username"])) {
    header("Location: /auth/pages/login");
    exit();
}

// Animals domain: feeding and gest require auth
if ($domain === "animals") {
    $controller = $_GET['controller'] ?? '';
    if (in_array($controller, ['feeding', 'gest']) && 
        !isset($_SESSION["user"]["username"])) {
        header("Location: /auth/pages/login");
        exit();
    }
}

// Habitats domain: gest and suggestion require auth
if ($domain === "habitats") {
    $controller = $_GET['controller'] ?? '';
    if (in_array($controller, ['gest', 'suggestion']) && 
        !isset($_SESSION["user"]["username"])) {
        header("Location: /auth/pages/login");
        exit();
    }
}

// Home domain: dashboard (/home/pages/start) requires auth
if ($domain === "home" && $_GET["action"] === "start" && 
    !isset($_SESSION["user"]["username"])) {
    header("Location: /auth/pages/login");
    exit();
}
✅ /animals/pages/allanimals       (public listing)
✅ /animals/pages/animalpicked     (public detail page)

Preventing Login Loops

The router prevents authenticated users from accessing the login page:
// If already authenticated and trying to access login, redirect to dashboard
if (isset($_SESSION["user"]["username"]) && 
    $domain === "auth" && 
    $_GET["action"] === "login") {
    header("Location: /home/pages/start");
    exit();
}
This improves user experience and prevents infinite redirect loops.

Domain Whitelist Validation

The router only loads domains that are explicitly allowed:
$allowed_domains = [
    "hero", "medias", "habitat1", "cms", "about", 
    "auth", "home", "animals", "employees", "habitats", 
    "permissions", "reports", "roles", "schedules", 
    "testimonials", "users", "contact", "vreports"
];

if (in_array($domain, $allowed_domains)) {
    $domainRouterPath = __DIR__ . "/{$domain}/{$domain}Router.php";
    
    if (file_exists($domainRouterPath)) {
        require_once $domainRouterPath;
    } else {
        // Domain in whitelist but router file missing
        http_response_code(500);
        header('Location: /public/error-500.php');
        exit();
    }
} else {
    // Domain not in whitelist
    http_response_code(404);
    header('Location: /public/error-404.php');
    exit();
}
The whitelist is a security feature. Even if a domain folder exists, it won’t be accessible unless explicitly listed in $allowed_domains. This prevents access to development, test, or internal domains.

Cache Control Headers

The router disables browser caching to ensure users always see fresh content:
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
This is crucial for:
  • Session updates: Ensuring logout is immediate
  • Permission changes: New permissions apply immediately
  • Content updates: CMS changes visible right away
  • Security: Preventing cached authenticated pages

Domain Router Pattern

After the central router validates authentication, it loads the domain-specific router:
<?php
require_once __DIR__ . '/../../includes/functions.php';
handleDomainRouting('animals', __DIR__);
Each domain router is minimal - it just delegates to the handleDomainRouting() function, which handles the actual controller instantiation and action execution. This pattern provides:
  • Consistency: All domains route the same way
  • Maintainability: Routing logic is centralized in functions.php
  • Simplicity: Adding a new domain requires minimal boilerplate

Session Data Structure

When a user logs in, their session contains:
$_SESSION = [
    'loggedin' => true,
    'user' => [
        'id_user' => 123,
        'username' => '[email protected]',
        'full_name' => 'John Doe',
        'role_name' => 'Veterinarian',
        'permissions' => [
            'animals-view',
            'animals-edit',
            'vreports-create',
            'vreports-view'
        ]
    ],
    'last_activity' => 1709539200  // Unix timestamp
];
Controllers can access this data with:
$username = $_SESSION['user']['username'];
$permissions = $_SESSION['user']['permissions'];

if (hasPermission('animals-create')) {
    // Allow animal creation
}

Error Handling

The router handles errors appropriately:
if (!in_array($domain, $allowed_domains)) {
    http_response_code(404);
    header('Location: /public/error-404.php');
    exit();
}

Next Steps

Domain Structure

Learn how domains handle requests internally

Authentication

Implement authentication in your features

Build docs developers (and LLMs) love