GateArch
Student portal and admin system with course management
Overview
GateArch is a full-stack web application providing a student information portal backed by a PHP 8 and MySQL server. Students log in with hashed credentials, view their enrolled courses, check grades, and access course details from a personalised dashboard. An admin panel provides CRUD operations for managing students, courses, and enrollments. The application is structured around a PHP routing layer served through Apache, with PDO prepared statements used for all database interactions to prevent SQL injection. Sessions handle authentication state, and role-based access control at the PHP level restricts admin routes to users with the admin role. The frontend is a responsive HTML/CSS/JavaScript interface that uses form-based interactions with server-side processing — no single-page framework, demonstrating a solid grasp of traditional multi-page application architecture.
Architecture
Tech Stack
- PHP 8 — Server-side routing, session management, role-based access control, form processing
- MySQL — Relational database with four tables: users, courses, enrollments, grades; foreign key constraints enforce referential integrity
- PDO (PHP Data Objects) — All database queries use prepared statements with bound parameters, eliminating SQL injection risk entirely
- Apache — HTTP server;
.htaccessused to restrict direct access to PHP include files - PHP Sessions —
session_start()on every protected page; session data stores user ID and role; session is destroyed on logout - HTML5 / CSS3 / JavaScript — Responsive frontend; CSS Grid/Flexbox for dashboard layout; JavaScript for client-side form validation before POST submission
Build Process
Designed the relational schema to support the core features: user authentication and roles, course catalog, student-course enrollment, and grade recording. Foreign keys with ON DELETE CASCADE on the enrollments and grades tables ensure that deleting a student or course cleans up related records automatically.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(80) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role ENUM('student','admin') NOT NULL DEFAULT 'student'
);
CREATE TABLE courses (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
credits TINYINT NOT NULL,
description TEXT
);
CREATE TABLE enrollments (
id INT AUTO_INCREMENT PRIMARY KEY,
student_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
course_id INT NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
UNIQUE KEY uq_enrollment (student_id, course_id)
);
CREATE TABLE grades (
id INT AUTO_INCREMENT PRIMARY KEY,
enrollment_id INT NOT NULL REFERENCES enrollments(id) ON DELETE CASCADE,
grade DECIMAL(5,2),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
User passwords are never stored in plaintext. Registration uses password_hash($password, PASSWORD_DEFAULT) to store a bcrypt hash. Login uses password_verify($input, $stored_hash) to check the submission against the stored hash without ever decoding it. On successful verification, the user's ID and role are written to the session.
// auth.php — login processing
session_start();
$stmt = $pdo->prepare('SELECT id, password_hash, role FROM users WHERE username = ?');
$stmt->execute([$_POST['username']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($_POST['password'], $user['password_hash'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];
header('Location: ' . ($user['role'] === 'admin' ? 'admin.php' : 'dashboard.php'));
exit;
}
// fallthrough: show login error
The student dashboard queries enrolled courses and grades using a JOIN across three tables. All values are bound via PDO to prevent injection, including the session user ID that scopes the query to the authenticated student.
// dashboard.php — fetch enrolled courses with grades
$stmt = $pdo->prepare('
SELECT c.name, c.credits, g.grade
FROM enrollments e
JOIN courses c ON c.id = e.course_id
LEFT JOIN grades g ON g.enrollment_id = e.id
WHERE e.student_id = ?
ORDER BY c.name
');
$stmt->execute([$_SESSION['user_id']]);
$courses = $stmt->fetchAll(PDO::FETCH_ASSOC);
Every admin-facing PHP file begins with a session role check. If the check fails, the user is redirected to the login page — there is no route that an unauthenticated or non-admin user can reach admin functionality through.
// admin.php — role guard (top of every admin file)
session_start();
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
header('Location: login.php');
exit;
}
Admin CRUD operations use the same PDO prepared statement pattern. Form submissions are processed on POST, and the page redirects after successful operations (Post/Redirect/Get pattern) to prevent duplicate submissions on refresh.
The dashboard uses a CSS Grid layout with a sidebar for navigation and a main content area for course/grade tables. JavaScript validates form fields (non-empty, numeric grades in range 0–100) before submission, providing immediate feedback without a round-trip to the server. Server-side validation also runs on every POST as the authoritative check.
Beyond PDO prepared statements, additional security measures were applied: CSRF tokens on all state-changing forms (generated with bin2hex(random_bytes(32)), stored in session, verified on POST), htmlspecialchars() wrapping all user-supplied data rendered into HTML output to prevent XSS, and .htaccess rules to deny direct HTTP access to PHP include and config files.
// CSRF token generation and embedding
$_SESSION['csrf'] = bin2hex(random_bytes(32));
// In form HTML:
// <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
// CSRF verification on POST
if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
http_response_code(403); exit('CSRF validation failed');
}
Authentication & Request Flow
Challenges & Solutions
Role-Based Routing Without a Framework. Without a routing framework like Laravel, every protected PHP file needed its own role check at the top. The pattern was standardised into a single require 'includes/auth-guard.php' include that accepts a required role parameter. This reduced the guard to a single line at the top of each protected file, making it easy to audit which files are protected and ensuring consistent behaviour.
SQL Injection Prevention. The most critical security concern in any PHP/MySQL application is SQL injection. Every query in the application uses PDO prepared statements with bound parameters — no string concatenation of user input into SQL anywhere in the codebase. This was enforced as a code review rule: any query using string interpolation was rejected and rewritten. PDO also enables proper error handling via exceptions rather than relying on the return value of mysql_query.
What I Learned
- The full PHP session lifecycle:
session_start(), writing to$_SESSION, reading it on subsequent requests, and destroying it on logout password_hash()withPASSWORD_DEFAULTuses bcrypt with an automatically generated salt — the hash includes the algorithm identifier and salt, sopassword_verify()needs only the input and the stored hash- PDO prepared statements are the correct, non-negotiable approach to database queries with user input; string concatenation into SQL is never acceptable
- CSRF protection requires generating a random token per session, embedding it in every state-changing form, and verifying it on POST before processing the request
- The Post/Redirect/Get (PRG) pattern prevents duplicate form submissions on page refresh by redirecting after every successful POST operation
- Relational schema design with foreign keys and
ON DELETE CASCADEmaintains referential integrity at the database level, independent of application logic