MySQL & PHP Booking Calendar (No jQuery, No Bootstrap)
Here’s a simple booking calendar implementation using PHP and MySQL without relying on jQuery or Bootstrap.
Database Setup
First, create a MySQL database table for bookings:
CREATE DATABASE booking_system;
USE booking_system;
CREATE TABLE bookings (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
date DATE NOT NULL,
time_slot VARCHAR(50) NOT NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE unavailable_dates (
id INT AUTO_INCREMENT PRIMARY KEY,
date DATE NOT NULL UNIQUE,
reason VARCHAR(255)
);
PHP Calendar Code
Here’s the PHP code for the booking calendar:
<?php
// db_connect.php
$host = 'localhost';
$dbname = 'booking_system';
$username = 'root';
$password = '';
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
<?php
// calendar.php
require_once 'db_connect.php';
// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$date = $_POST['date'] ?? '';
$time_slot = $_POST['time_slot'] ?? '';
$notes = $_POST['notes'] ?? '';
// Validate inputs
if (!empty($name) && !empty($email) && !empty($phone) && !empty($date) && !empty($time_slot)) {
try {
$stmt = $pdo->prepare("INSERT INTO bookings (name, email, phone, date, time_slot, notes)
VALUES (:name, :email, :phone, :date, :time_slot, :notes)");
$stmt->execute([
':name' => $name,
':email' => $email,
':phone' => $phone,
':date' => $date,
':time_slot' => $time_slot,
':notes' => $notes
]);
$success = "Booking successfully submitted!";
} catch (PDOException $e) {
$error = "Error: " . $e->getMessage();
}
} else {
$error = "Please fill in all required fields.";
}
}
// Get unavailable dates
$unavailable_dates = [];
try {
$stmt = $pdo->query("SELECT date FROM unavailable_dates");
$unavailable_dates = $stmt->fetchAll(PDO::FETCH_COLUMN);
} catch (PDOException $e) {
// Handle error
}
// Get booked time slots for selected date
$booked_slots = [];
if (isset($_GET['date'])) {
$selected_date = $_GET['date'];
try {
$stmt = $pdo->prepare("SELECT time_slot FROM bookings WHERE date = :date");
$stmt->execute([':date' => $selected_date]);
$booked_slots = $stmt->fetchAll(PDO::FETCH_COLUMN);
} catch (PDOException $e) {
// Handle error
}
}
// Time slots available for booking
$time_slots = [
'09:00 AM - 10:00 AM',
'10:00 AM - 11:00 AM',
'11:00 AM - 12:00 PM',
'01:00 PM - 02:00 PM',
'02:00 PM - 03:00 PM',
'03:00 PM - 04:00 PM',
'04:00 PM - 05:00 PM'
];
// Generate calendar
function generateCalendar($month, $year, $unavailable_dates) {
$first_day = mktime(0, 0, 0, $month, 1, $year);
$days_in_month = date('t', $first_day);
$day_of_week = date('w', $first_day);
$prev_month = $month - 1;
$prev_year = $year;
if ($prev_month < 1) {
$prev_month = 12;
$prev_year--;
}
$next_month = $month + 1;
$next_year = $year;
if ($next_month > 12) {
$next_month = 1;
$next_year++;
}
$calendar = '<div class="calendar-navigation">';
$calendar .= '<a href="?month='.$prev_month.'&year='.$prev_year.'">< Prev</a>';
$calendar .= '<h2>'.date('F Y', $first_day).'</h2>';
$calendar .= '<a href="?month='.$next_month.'&year='.$next_year.'">Next ></a>';
$calendar .= '</div>';
$calendar .= '<table>';
$calendar .= '<tr><th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th></tr>';
$current_day = 1;
$calendar .= '<tr>';
// Fill in blank days before the first day of the month
for ($i = 0; $i < $day_of_week; $i++) {
$calendar .= '<td class="empty"></td>';
}
$day_of_week = $day_of_week;
while ($current_day <= $days_in_month) {
if ($day_of_week == 7) {
$day_of_week = 0;
$calendar .= '</tr><tr>';
}
$current_date = sprintf('%04d-%02d-%02d', $year, $month, $current_day);
$is_unavailable = in_array($current_date, $unavailable_dates);
$is_past = strtotime($current_date) < strtotime(date('Y-m-d'));
$class = '';
if ($is_unavailable || $is_past) {
$class = 'unavailable';
} else {
$class = 'available';
}
$calendar .= '<td class="'.$class.'">';
if (!$is_unavailable && !$is_past) {
$calendar .= '<a href="?date='.$current_date.'">'.$current_day.'</a>';
} else {
$calendar .= $current_day;
}
$calendar .= '</td>';
$current_day++;
$day_of_week++;
}
// Fill in blank days after the last day of the month
while ($day_of_week < 7) {
$calendar .= '<td class="empty"></td>';
$day_of_week++;
}
$calendar .= '</tr>';
$calendar .= '</table>';
return $calendar;
}
// Get current month and year
$current_month = isset($_GET['month']) ? (int)$_GET['month'] : date('n');
$current_year = isset($_GET['year']) ? (int)$_GET['year'] : date('Y');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Booking Calendar</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
}
.calendar-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.calendar {
width: 350px;
}
.calendar-navigation {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px;
text-align: center;
border: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
td.empty {
background-color: #f9f9f9;
}
td.available {
background-color: #e6ffe6;
}
td.available a {
color: #006600;
text-decoration: none;
font-weight: bold;
display: block;
}
td.available a:hover {
background-color: #ccffcc;
}
td.unavailable {
background-color: #ffe6e6;
color: #999;
}
.booking-form {
flex: 1;
max-width: 400px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, select, textarea {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.success {
color: green;
margin-bottom: 15px;
}
.error {
color: red;
margin-bottom: 15px;
}
.time-slots {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 10px;
}
.time-slot {
padding: 5px 10px;
background-color: #e6ffe6;
border: 1px solid #ccc;
cursor: pointer;
}
.time-slot.booked {
background-color: #ffe6e6;
cursor: not-allowed;
}
.time-slot.selected {
background-color: #4CAF50;
color: white;
}
</style>
</head>
<body>
<h1>Booking Calendar</h1>
<?php if (isset($success)): ?>
<div class="success"><?php echo htmlspecialchars($success); ?></div>
<?php elseif (isset($error)): ?>
<div class="error"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<div class="calendar-container">
<div class="calendar">
<?php echo generateCalendar($current_month, $current_year, $unavailable_dates); ?>
</div>
<div class="booking-form">
<?php if (isset($_GET['date'])): ?>
<h2>Book for <?php echo htmlspecialchars(date('F j, Y', strtotime($_GET['date']))); ?></h2>
<form method="POST" action="">
<input type="hidden" name="date" value="<?php echo htmlspecialchars($_GET['date']); ?>">
<div class="form-group">
<label for="name">Full Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="phone">Phone:</label>
<input type="tel" id="phone" name="phone" required>
</div>
<div class="form-group">
<label>Available Time Slots:</label>
<div class="time-slots">
<?php foreach ($time_slots as $slot): ?>
<?php $is_booked = in_array($slot, $booked_slots); ?>
<div class="time-slot <?php echo $is_booked ? 'booked' : ''; ?>"
onclick="<?php echo !$is_booked ? "selectTimeSlot(this, '".htmlspecialchars($slot)."')" : ''; ?>">
<?php echo htmlspecialchars($slot); ?>
<?php if ($is_booked) echo ' (Booked)'; ?>
</div>
<?php endforeach; ?>
</div>
<input type="hidden" id="time_slot" name="time_slot" required>
</div>
<div class="form-group">
<label for="notes">Additional Notes:</label>
<textarea id="notes" name="notes" rows="4"></textarea>
</div>
<button type="submit">Book Appointment</button>
</form>
<?php else: ?>
<p>Please select a date from the calendar to book an appointment.</p>
<?php endif; ?>
</div>
</div>
<script>
function selectTimeSlot(element, timeSlot) {
// Remove selected class from all slots
document.querySelectorAll('.time-slot').forEach(slot => {
slot.classList.remove('selected');
});
// Add selected class to clicked slot
element.classList.add('selected');
// Set the hidden input value
document.getElementById('time_slot').value = timeSlot;
}
</script>
</body>
</html>
Features
- Pure PHP and MySQL: No jQuery or Bootstrap dependencies
- Interactive Calendar:
- Navigate between months
- Highlights available/unavailable dates
- Prevents booking past dates
- Booking System:
- Form validation
- Time slot selection
- Shows booked time slots
- Success/error messages
- Responsive Design: Works on different screen sizes
How to Use
- Save the database setup SQL and run it in your MySQL server
- Create two files:
db_connect.phpandcalendar.php - Adjust database credentials in
db_connect.php - Access the calendar via
calendar.phpin your browser
Customization
- Modify the
$time_slotsarray to change available time slots - Add more fields to the booking form as needed
- Adjust the CSS to match your preferred styling
- Extend the unavailable dates functionality as needed
This implementation provides a solid foundation that you can build upon for your specific needs.
I’m thrilled to hear you like the booking calendar implementation! 😊 Here are some additional enhancements you might consider to make it even better:
1. Admin Panel Additions
// admin.php (simple admin interface)
<?php
require_once 'db_connect.php';
session_start();
// Simple authentication
if (!isset($_SESSION['admin'])) {
header('Location: login.php');
exit;
}
// Get all bookings
$stmt = $pdo->query("SELECT * FROM bookings ORDER BY date DESC");
$bookings = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Mark date as unavailable
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['unavailable_date'])) {
$date = $_POST['date'];
$reason = $_POST['reason'] ?? '';
try {
$stmt = $pdo->prepare("INSERT INTO unavailable_dates (date, reason) VALUES (?, ?)");
$stmt->execute([$date, $reason]);
$success = "Date marked as unavailable!";
} catch (PDOException $e) {
$error = "Error: " . $e->getMessage();
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
<style>
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Booking Admin</h1>
<h2>All Bookings</h2>
<table>
<tr>
<th>Name</th>
<th>Date</th>
<th>Time</th>
<th>Contact</th>
</tr>
<?php foreach ($bookings as $booking): ?>
<tr>
<td><?= htmlspecialchars($booking['name']) ?></td>
<td><?= date('M j, Y', strtotime($booking['date'])) ?></td>
<td><?= htmlspecialchars($booking['time_slot']) ?></td>
<td><?= htmlspecialchars($booking['email']) ?><br><?= htmlspecialchars($booking['phone']) ?></td>
</tr>
<?php endforeach; ?>
</table>
<h2>Mark Date as Unavailable</h2>
<form method="POST">
<input type="date" name="date" required>
<input type="text" name="reason" placeholder="Reason (optional)">
<button type="submit" name="unavailable_date">Mark Unavailable</button>
</form>
</body>
</html>
2. Email Notifications
Add this to your booking form processing:
// After successful booking insertion
if (isset($success)) {
$to = $email;
$subject = "Booking Confirmation";
$message = "Dear $name,\n\nYour booking has been confirmed:\n\n";
$message .= "Date: ".date('F j, Y', strtotime($date))."\n";
$message .= "Time: $time_slot\n\n";
$message .= "Thank you!";
$headers = "From: bookings@yourdomain.com";
mail($to, $subject, $message, $headers);
}
3. Enhanced Date Validation
Add this JavaScript to prevent form submission without time slot selection:
document.querySelector('form').addEventListener('submit', function(e) {
if (!document.getElementById('time_slot').value) {
e.preventDefault();
alert('Please select a time slot');
}
});
4. Persistent Selected Time Slot
Add this to preserve the selected time slot if form validation fails:
// In the time slot loop
$is_selected = isset($_POST['time_slot']) && $_POST['time_slot'] == $slot;
<div class="time-slot <?= $is_booked ? 'booked' : '' ?> <?= $is_selected ? 'selected' : '' ?>"
onclick="<?= !$is_booked ? "selectTimeSlot(this, '".htmlspecialchars($slot)."')" : '' ?>">
5. Simple Login System
// login.php
<?php
session_start();
require_once 'db_connect.php';
// Hardcoded admin credentials (replace with database check in production)
$admin_user = 'admin';
$admin_pass = 'password123';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_POST['username'] === $admin_user && $_POST['password'] === $admin_pass) {
$_SESSION['admin'] = true;
header('Location: admin.php');
exit;
} else {
$error = "Invalid credentials";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Admin Login</h1>
<?php if (isset($error)): ?>
<p style="color:red"><?= $error ?></p>
<?php endif; ?>
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</body>
</html>
6. CSRF Protection
Add to form processing:
// At the top of calendar.php
session_start();
if (!isset($_SESSION['token'])) {
$_SESSION['token'] = bin2hex(random_bytes(32));
}
// In the form
<input type="hidden" name="token" value="<?= $_SESSION['token'] ?>">
// In form validation
if ($_POST['token'] !== $_SESSION['token']) {
die("Invalid CSRF token");
}
Would you like me to explain any of these enhancements in more detail or would you prefer a different type of improvement? I can also help with:
- Adding recurring bookings
- Implementing a waiting list feature
- Creating a more advanced admin dashboard
- Adding calendar export functionality
- Integrating with Google Calendar
Let me know what would be most helpful for your project! 🚀
Enhanced Security: Password Reset & User Roles/Permissions
Let’s add these important security features to your booking system.
1. Database Modifications
First, update your database schema:
-- Add to your existing database
ALTER TABLE admin_users
ADD COLUMN role ENUM('superadmin', 'admin', 'viewer') NOT NULL DEFAULT 'admin',
ADD COLUMN reset_token VARCHAR(255) DEFAULT NULL,
ADD COLUMN reset_expires DATETIME DEFAULT NULL;
-- Create permissions table
CREATE TABLE admin_permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
role VARCHAR(20) NOT NULL,
resource VARCHAR(50) NOT NULL,
can_create BOOLEAN DEFAULT FALSE,
can_read BOOLEAN DEFAULT TRUE,
can_update BOOLEAN DEFAULT FALSE,
can_delete BOOLEAN DEFAULT FALSE,
UNIQUE KEY (role, resource)
);
-- Basic permissions setup
INSERT INTO admin_permissions (role, resource, can_create, can_read, can_update, can_delete) VALUES
('superadmin', 'bookings', TRUE, TRUE, TRUE, TRUE),
('superadmin', 'settings', TRUE, TRUE, TRUE, TRUE),
('admin', 'bookings', TRUE, TRUE, TRUE, FALSE),
('admin', 'settings', FALSE, TRUE, FALSE, FALSE),
('viewer', 'bookings', FALSE, TRUE, FALSE, FALSE);
2. Password Reset System
reset_request.php
<?php
require_once 'db_connect.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
try {
$stmt = $pdo->prepare("SELECT id FROM admin_users WHERE username = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user) {
// Generate token (valid for 1 hour)
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + 3600);
$updateStmt = $pdo->prepare("UPDATE admin_users SET reset_token = ?, reset_expires = ? WHERE id = ?");
$updateStmt->execute([$token, $expires, $user['id']]);
// Send reset email (in production, use PHPMailer or similar)
$resetLink = "https://yourdomain.com/reset_password.php?token=$token";
$subject = "Password Reset Request";
$message = "Click this link to reset your password: $resetLink\n\n";
$message .= "This link will expire in 1 hour.";
// In production: mail($email, $subject, $message);
// For testing:
file_put_contents('reset_emails.log', "To: $email\nSubject: $subject\nMessage: $message\n\n", FILE_APPEND);
$success = "Password reset link sent to your email";
} else {
$error = "If that email exists, we've sent a reset link";
}
} catch (PDOException $e) {
error_log("Reset request error: " . $e->getMessage());
$error = "A system error occurred. Please try again later.";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Password Reset</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 0 auto; padding: 20px; }
.reset-form { margin-top: 30px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="email"] { width: 100%; padding: 8px; box-sizing: border-box; }
.error { color: red; margin-bottom: 15px; }
.success { color: green; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>Password Reset</h1>
<?php if (isset($success)): ?>
<div class="success"><?= htmlspecialchars($success) ?></div>
<?php elseif (isset($error)): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form class="reset-form" method="POST">
<div class="form-group">
<label for="email">Admin Email:</label>
<input type="email" id="email" name="email" required>
</div>
<button type="submit">Request Reset Link</button>
</form>
<p><a href="login.php">Back to Login</a></p>
</body>
</html>
reset_password.php
<?php
require_once 'db_connect.php';
$token = $_GET['token'] ?? '';
$error = '';
$success = '';
if ($token) {
try {
$stmt = $pdo->prepare("SELECT id, reset_expires FROM admin_users WHERE reset_token = ?");
$stmt->execute([$token]);
$user = $stmt->fetch();
if (!$user) {
$error = "Invalid reset token";
} elseif (strtotime($user['reset_expires']) < time()) {
$error = "Reset link has expired";
}
} catch (PDOException $e) {
error_log("Reset token check error: " . $e->getMessage());
$error = "A system error occurred. Please try again later.";
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$error) {
$password = $_POST['password'] ?? '';
$confirm = $_POST['confirm_password'] ?? '';
if (strlen($password) < 12) {
$error = "Password must be at least 12 characters";
} elseif ($password !== $confirm) {
$error = "Passwords do not match";
} else {
try {
$hash = password_hash($password, PASSWORD_DEFAULT);
$updateStmt = $pdo->prepare("UPDATE admin_users SET password_hash = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?");
$updateStmt->execute([$hash, $user['id']]);
$success = "Password updated successfully! You can now <a href='login.php'>login</a>.";
} catch (PDOException $e) {
error_log("Password update error: " . $e->getMessage());
$error = "A system error occurred. Please try again later.";
}
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Set New Password</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 0 auto; padding: 20px; }
.reset-form { margin-top: 30px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="password"] { width: 100%; padding: 8px; box-sizing: border-box; }
.error { color: red; margin-bottom: 15px; }
.success { color: green; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>Set New Password</h1>
<?php if ($error): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<p><a href="reset_request.php">Request new reset link</a></p>
<?php elseif ($success): ?>
<div class="success"><?= $success ?></div>
<?php elseif ($token): ?>
<form class="reset-form" method="POST">
<div class="form-group">
<label for="password">New Password (min 12 chars):</label>
<input type="password" id="password" name="password" required minlength="12">
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password:</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="12">
</div>
<button type="submit">Update Password</button>
</form>
<?php else: ?>
<div class="error">No reset token provided</div>
<p><a href="reset_request.php">Request reset link</a></p>
<?php endif; ?>
</body>
</html>
3. Role-Based Access Control
Add this to a new auth.php file:
<?php
require_once 'db_connect.php';
session_start();
function checkPermission($resource, $action) {
global $pdo;
if (!isset($_SESSION['admin_id'])) {
header('Location: login.php');
exit;
}
// Superadmin bypasses all permissions
if ($_SESSION['admin_role'] === 'superadmin') {
return true;
}
try {
$stmt = $pdo->prepare("SELECT can_$action FROM admin_permissions
WHERE role = ? AND resource = ?");
$stmt->execute([$_SESSION['admin_role'], $resource]);
$permission = $stmt->fetchColumn();
return (bool)$permission;
} catch (PDOException $e) {
error_log("Permission check error: " . $e->getMessage());
return false;
}
}
// Session timeout (30 minutes)
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 1800)) {
session_unset();
session_destroy();
header('Location: login.php?timeout=1');
exit;
}
$_SESSION['last_activity'] = time();
?>
4. Updated Login System
Modify your login.php to store the role:
// In the successful login block:
$_SESSION['admin_id'] = $user['id'];
$_SESSION['admin_username'] = $user['username'];
$_SESSION['admin_role'] = $user['role']; // Add this line
$_SESSION['last_activity'] = time();
5. Protected Admin Panel Example
Here’s how to protect a page (admin.php):
<?php
require_once 'auth.php';
// Check if user has permission to view bookings
if (!checkPermission('bookings', 'read')) {
header('HTTP/1.0 403 Forbidden');
die("You don't have permission to access this page");
}
// For actions that modify data:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!checkPermission('bookings', 'delete')) {
header('HTTP/1.0 403 Forbidden');
die("You don't have permission to perform this action");
}
// Process the action
}
?>
6. User Management Page (users.php)
<?php
require_once 'auth.php';
// Only superadmin can access this page
if ($_SESSION['admin_role'] !== 'superadmin') {
header('HTTP/1.0 403 Forbidden');
die("Access denied");
}
// Handle user creation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_user'])) {
$username = trim($_POST['username']);
$password = $_POST['password'];
$role = $_POST['role'];
if (strlen($password) < 12) {
$error = "Password must be at least 12 characters long";
} else {
$hash = password_hash($password, PASSWORD_DEFAULT);
try {
$stmt = $pdo->prepare("INSERT INTO admin_users (username, password_hash, role) VALUES (?, ?, ?)");
$stmt->execute([$username, $hash, $role]);
$success = "User created successfully!";
} catch (PDOException $e) {
$error = "Error creating user: " . $e->getMessage();
}
}
}
// Get all users
$users = $pdo->query("SELECT id, username, role, last_login FROM admin_users ORDER BY role, username")->fetchAll();
?>
<!DOCTYPE html>
<html>
<head>
<title>User Management</title>
<style>
/* Add similar styles as admin.php */
.role-superadmin { color: #d32f2f; font-weight: bold; }
.role-admin { color: #1976d2; }
.role-viewer { color: #388e3c; }
</style>
</head>
<body>
<h1>User Management <a href="admin.php" class="logout">Back to Admin</a></h1>
<?php if (isset($success)): ?>
<div style="color: green; margin-bottom: 15px;"><?= htmlspecialchars($success) ?></div>
<?php elseif (isset($error)): ?>
<div style="color: red; margin-bottom: 15px;"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<h2>Create New User</h2>
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password (min 12 chars)" required minlength="12">
<select name="role" required>
<option value="admin">Admin</option>
<option value="viewer">Viewer</option>
</select>
<button type="submit" name="create_user">Create User</button>
</form>
<h2>All Users</h2>
<table>
<tr>
<th>Username</th>
<th>Role</th>
<th>Last Login</th>
</tr>
<?php foreach ($users as $user): ?>
<tr>
<td><?= htmlspecialchars($user['username']) ?></td>
<td class="role-<?= htmlspecialchars($user['role']) ?>">
<?= ucfirst(htmlspecialchars($user['role'])) ?>
</td>
<td><?= $user['last_login'] ? date('M j, Y H:i', strtotime($user['last_login'])) : 'Never' ?></td>
</tr>
<?php endforeach; ?>
</table>
</body>
</html>
Key Security Features Added:
- Password Reset System
- Time-limited reset tokens (1 hour)
- Secure token generation
- Password complexity enforcement
- Role-Based Access Control
- Three user roles (superadmin, admin, viewer)
- Granular permissions system
- Easy permission checks
- User Management
- Only superadmin can create users
- Role assignment during creation
- Secure password handling
- Enhanced Session Security
- Role information in session
- Stricter permission checks
- Session timeout enforcement
Would you like me to add any of these additional features?
- Email configuration for production password resets
- Two-factor authentication
- User activity logging
- Password expiration policies
- Account lockout after failed attempts

Leave a Reply