diff --git a/DB_existing schema.sql b/DB_existing schema.sql new file mode 100644 index 00000000..a6e4ee02 --- /dev/null +++ b/DB_existing schema.sql @@ -0,0 +1,680 @@ +-- phpMyAdmin SQL Dump +-- version 5.2.2 +-- https://www.phpmyadmin.net/ +-- +-- Host: db +-- Generation Time: Dec 02, 2025 at 07:32 PM +-- Server version: 8.0.41 +-- PHP Version: 8.2.27 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Database: `4wdcsa` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `bar_items` +-- + +DROP TABLE IF EXISTS `bar_items`; +CREATE TABLE `bar_items` ( + `item_id` int NOT NULL, + `price` decimal(10,2) DEFAULT NULL, + `description` varchar(64) DEFAULT NULL, + `image` varchar(255) DEFAULT NULL, + `qty` int DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `bar_tabs` +-- + +DROP TABLE IF EXISTS `bar_tabs`; +CREATE TABLE `bar_tabs` ( + `tab_id` int NOT NULL, + `user_id` int DEFAULT NULL, + `image` varchar(255) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `bar_transactions` +-- + +DROP TABLE IF EXISTS `bar_transactions`; +CREATE TABLE `bar_transactions` ( + `transaction_id` int NOT NULL, + `user_id` int DEFAULT NULL, + `item_price` decimal(10,2) DEFAULT NULL, + `item_name` varchar(64) DEFAULT NULL, + `eft_id` varchar(255) DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `item_id` int DEFAULT NULL, + `tab_id` int DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `blacklist` +-- + +DROP TABLE IF EXISTS `blacklist`; +CREATE TABLE `blacklist` ( + `blacklist_id` int NOT NULL, + `ip` varchar(255) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `blogs` +-- + +DROP TABLE IF EXISTS `blogs`; +CREATE TABLE `blogs` ( + `blog_id` int NOT NULL, + `title` varchar(255) DEFAULT NULL, + `date` date DEFAULT NULL, + `category` varchar(255) DEFAULT NULL, + `description` text, + `image` varchar(255) DEFAULT NULL, + `author` int DEFAULT NULL, + `link` varchar(255) DEFAULT NULL, + `members_only` tinyint(1) NOT NULL DEFAULT '1', + `content` text, + `status` enum('draft','published','deleted') CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT 'draft' +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `bookings` +-- + +DROP TABLE IF EXISTS `bookings`; +CREATE TABLE `bookings` ( + `booking_id` int NOT NULL, + `booking_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `user_id` int NOT NULL, + `from_date` date DEFAULT NULL, + `to_date` date DEFAULT NULL, + `num_vehicles` int NOT NULL DEFAULT '1', + `num_adults` int NOT NULL DEFAULT '0', + `num_children` int NOT NULL DEFAULT '0', + `add_firewood` tinyint(1) DEFAULT '0', + `total_amount` decimal(10,2) DEFAULT NULL, + `discount_amount` decimal(10,2) NOT NULL DEFAULT '0.00', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `status` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL, + `payment_id` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `trip_id` int DEFAULT NULL, + `radio` tinyint(1) DEFAULT '0', + `course_id` int DEFAULT NULL, + `course_non_members` int DEFAULT '0', + `eft_id` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, + `accept_indemnity` tinyint(1) DEFAULT '0', + `num_pensioners` int DEFAULT '0', + `notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `campsites` +-- + +DROP TABLE IF EXISTS `campsites`; +CREATE TABLE `campsites` ( + `id` int NOT NULL, + `name` varchar(255) NOT NULL, + `description` text, + `latitude` float(10,6) NOT NULL, + `longitude` float(10,6) NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `website` varchar(255) DEFAULT NULL, + `telephone` varchar(50) DEFAULT NULL, + `thumbnail` varchar(255) DEFAULT NULL, + `user_id` int DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `comments` +-- + +DROP TABLE IF EXISTS `comments`; +CREATE TABLE `comments` ( + `comment_id` int NOT NULL, + `page_id` varchar(255) NOT NULL, + `user_id` varchar(100) NOT NULL, + `comment` text NOT NULL, + `created_at` datetime DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `courses` +-- + +DROP TABLE IF EXISTS `courses`; +CREATE TABLE `courses` ( + `course_id` int NOT NULL, + `course_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `date` date NOT NULL, + `capacity` int NOT NULL, + `booked` int NOT NULL, + `cost_members` decimal(10,2) NOT NULL, + `cost_nonmembers` decimal(10,2) NOT NULL, + `instructor` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `instructor_email` varchar(255) COLLATE utf8mb4_general_ci NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `efts` +-- + +DROP TABLE IF EXISTS `efts`; +CREATE TABLE `efts` ( + `eft_id` varchar(255) NOT NULL, + `booking_id` int DEFAULT NULL, + `user_id` int NOT NULL, + `status` varchar(64) NOT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `amount` decimal(10,2) NOT NULL, + `description` varchar(255) DEFAULT NULL, + `membershipfee_id` int DEFAULT NULL, + `proof_of_payment` varchar(255) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `events` +-- + +DROP TABLE IF EXISTS `events`; +CREATE TABLE `events` ( + `event_id` int NOT NULL, + `date` date DEFAULT NULL, + `time` time DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `image` varchar(255) DEFAULT NULL, + `description` text, + `feature` varchar(255) DEFAULT NULL, + `location` varchar(255) DEFAULT NULL, + `type` varchar(255) DEFAULT NULL, + `promo` varchar(255) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `legacy_members` +-- + +DROP TABLE IF EXISTS `legacy_members`; +CREATE TABLE `legacy_members` ( + `legacy_id` varchar(12) NOT NULL, + `last_name` varchar(255) DEFAULT NULL, + `first_name` varchar(255) DEFAULT NULL, + `amount` varchar(12) DEFAULT NULL, + `phone_number` varchar(16) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `membership_application` +-- + +DROP TABLE IF EXISTS `membership_application`; +CREATE TABLE `membership_application` ( + `application_id` int NOT NULL, + `user_id` int NOT NULL, + `first_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `last_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `id_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `dob` date DEFAULT NULL, + `occupation` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `tel_cell` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `spouse_first_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `spouse_last_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `spouse_id_number` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL, + `spouse_dob` date DEFAULT NULL, + `spouse_occupation` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `spouse_tel_cell` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL, + `spouse_email` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `child_name1` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `child_dob1` date DEFAULT NULL, + `child_name2` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `child_dob2` date DEFAULT NULL, + `child_name3` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `child_dob3` date DEFAULT NULL, + `physical_address` text COLLATE utf8mb4_general_ci, + `postal_address` text COLLATE utf8mb4_general_ci, + `interests_hobbies` text COLLATE utf8mb4_general_ci, + `vehicle_make` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `vehicle_model` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `vehicle_year` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL, + `vehicle_registration` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL, + `secondary_vehicle_make` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `secondary_vehicle_model` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `secondary_vehicle_year` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL, + `secondary_vehicle_registration` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `accept_indemnity` tinyint(1) NOT NULL DEFAULT '0', + `sig` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL, + `code` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `membership_fees` +-- + +DROP TABLE IF EXISTS `membership_fees`; +CREATE TABLE `membership_fees` ( + `fee_id` int NOT NULL, + `user_id` int NOT NULL, + `payment_amount` decimal(10,2) NOT NULL, + `payment_date` date DEFAULT NULL, + `payment_status` varchar(255) COLLATE utf8mb4_general_ci DEFAULT 'PENDING', + `membership_start_date` date NOT NULL, + `membership_end_date` date NOT NULL, + `due_date` date DEFAULT NULL, + `renewal_reminder_sent` tinyint(1) DEFAULT '0', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `payment_id` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `password_resets` +-- + +DROP TABLE IF EXISTS `password_resets`; +CREATE TABLE `password_resets` ( + `id` int NOT NULL, + `user_id` int NOT NULL, + `token` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `expires_at` datetime NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `payments` +-- + +DROP TABLE IF EXISTS `payments`; +CREATE TABLE `payments` ( + `payment_id` varchar(255) NOT NULL, + `user_id` int NOT NULL, + `amount` decimal(10,2) NOT NULL, + `status` varchar(255) NOT NULL, + `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `description` varchar(255) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `prices` +-- + +DROP TABLE IF EXISTS `prices`; +CREATE TABLE `prices` ( + `price_id` int NOT NULL, + `description` varchar(255) DEFAULT NULL, + `type` varchar(255) DEFAULT NULL, + `amount` decimal(10,2) DEFAULT NULL, + `amount_nonmembers` decimal(10,2) DEFAULT NULL, + `detail` text +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `trips` +-- + +DROP TABLE IF EXISTS `trips`; +CREATE TABLE `trips` ( + `trip_id` int NOT NULL, + `trip_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `start_date` date NOT NULL, + `end_date` date NOT NULL, + `short_description` text COLLATE utf8mb4_general_ci NOT NULL, + `long_description` text COLLATE utf8mb4_general_ci NOT NULL, + `vehicle_capacity` int NOT NULL, + `cost_members` decimal(10,2) NOT NULL, + `cost_nonmembers` decimal(10,2) NOT NULL, + `location` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `places_booked` int DEFAULT NULL, + `booking_fee` decimal(10,2) NOT NULL, + `trip_code` varchar(12) COLLATE utf8mb4_general_ci DEFAULT NULL, + `published` tinyint(1) NOT NULL DEFAULT '0', + `cost_pensioner_member` decimal(10,2) NOT NULL, + `cost_pensioner` decimal(10,2) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `user_id` int NOT NULL, + `first_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `last_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `email` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `member` tinyint(1) NOT NULL DEFAULT '0', + `date_joined` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `is_verified` tinyint(1) NOT NULL DEFAULT '0', + `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `phone_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `profile_pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'assets/images/pp/default.png', + `role` enum('user','admin','superadmin','') COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'user', + `type` enum('google','credentials') COLLATE utf8mb4_general_ci NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `visitor_logs` +-- + +DROP TABLE IF EXISTS `visitor_logs`; +CREATE TABLE `visitor_logs` ( + `id` int NOT NULL, + `ip_address` varchar(45) NOT NULL, + `page_url` text NOT NULL, + `referrer_url` text, + `visit_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `user_id` int DEFAULT NULL, + `country` varchar(255) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- +-- Indexes for dumped tables +-- + +-- +-- Indexes for table `bar_items` +-- +ALTER TABLE `bar_items` + ADD PRIMARY KEY (`item_id`); + +-- +-- Indexes for table `bar_tabs` +-- +ALTER TABLE `bar_tabs` + ADD PRIMARY KEY (`tab_id`); + +-- +-- Indexes for table `bar_transactions` +-- +ALTER TABLE `bar_transactions` + ADD PRIMARY KEY (`transaction_id`); + +-- +-- Indexes for table `blacklist` +-- +ALTER TABLE `blacklist` + ADD PRIMARY KEY (`blacklist_id`); + +-- +-- Indexes for table `blogs` +-- +ALTER TABLE `blogs` + ADD PRIMARY KEY (`blog_id`); + +-- +-- Indexes for table `bookings` +-- +ALTER TABLE `bookings` + ADD PRIMARY KEY (`booking_id`), + ADD KEY `user_id` (`user_id`); + +-- +-- Indexes for table `campsites` +-- +ALTER TABLE `campsites` + ADD PRIMARY KEY (`id`); + +-- +-- Indexes for table `comments` +-- +ALTER TABLE `comments` + ADD PRIMARY KEY (`comment_id`); + +-- +-- Indexes for table `courses` +-- +ALTER TABLE `courses` + ADD PRIMARY KEY (`course_id`); + +-- +-- Indexes for table `efts` +-- +ALTER TABLE `efts` + ADD PRIMARY KEY (`eft_id`); + +-- +-- Indexes for table `events` +-- +ALTER TABLE `events` + ADD PRIMARY KEY (`event_id`); + +-- +-- Indexes for table `legacy_members` +-- +ALTER TABLE `legacy_members` + ADD PRIMARY KEY (`legacy_id`); + +-- +-- Indexes for table `membership_application` +-- +ALTER TABLE `membership_application` + ADD PRIMARY KEY (`application_id`); + +-- +-- Indexes for table `membership_fees` +-- +ALTER TABLE `membership_fees` + ADD PRIMARY KEY (`fee_id`); + +-- +-- Indexes for table `password_resets` +-- +ALTER TABLE `password_resets` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `token` (`token`), + ADD KEY `user_id` (`user_id`); + +-- +-- Indexes for table `payments` +-- +ALTER TABLE `payments` + ADD PRIMARY KEY (`payment_id`); + +-- +-- Indexes for table `prices` +-- +ALTER TABLE `prices` + ADD PRIMARY KEY (`price_id`); + +-- +-- Indexes for table `trips` +-- +ALTER TABLE `trips` + ADD PRIMARY KEY (`trip_id`); + +-- +-- Indexes for table `users` +-- +ALTER TABLE `users` + ADD PRIMARY KEY (`user_id`); + +-- +-- Indexes for table `visitor_logs` +-- +ALTER TABLE `visitor_logs` + ADD PRIMARY KEY (`id`); + +-- +-- AUTO_INCREMENT for dumped tables +-- + +-- +-- AUTO_INCREMENT for table `bar_items` +-- +ALTER TABLE `bar_items` + MODIFY `item_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `bar_tabs` +-- +ALTER TABLE `bar_tabs` + MODIFY `tab_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `bar_transactions` +-- +ALTER TABLE `bar_transactions` + MODIFY `transaction_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `blacklist` +-- +ALTER TABLE `blacklist` + MODIFY `blacklist_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `blogs` +-- +ALTER TABLE `blogs` + MODIFY `blog_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `bookings` +-- +ALTER TABLE `bookings` + MODIFY `booking_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `campsites` +-- +ALTER TABLE `campsites` + MODIFY `id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `comments` +-- +ALTER TABLE `comments` + MODIFY `comment_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `courses` +-- +ALTER TABLE `courses` + MODIFY `course_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `events` +-- +ALTER TABLE `events` + MODIFY `event_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `membership_application` +-- +ALTER TABLE `membership_application` + MODIFY `application_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `membership_fees` +-- +ALTER TABLE `membership_fees` + MODIFY `fee_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `password_resets` +-- +ALTER TABLE `password_resets` + MODIFY `id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `prices` +-- +ALTER TABLE `prices` + MODIFY `price_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `trips` +-- +ALTER TABLE `trips` + MODIFY `trip_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `users` +-- +ALTER TABLE `users` + MODIFY `user_id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `visitor_logs` +-- +ALTER TABLE `visitor_logs` + MODIFY `id` int NOT NULL AUTO_INCREMENT; + +-- +-- Constraints for dumped tables +-- + +-- +-- Constraints for table `bookings` +-- +ALTER TABLE `bookings` + ADD CONSTRAINT `bookings_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE; + +-- +-- Constraints for table `password_resets` +-- +ALTER TABLE `password_resets` + ADD CONSTRAINT `password_resets_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE; +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/functions.php b/functions.php index ec9c387e..2f577635 100644 --- a/functions.php +++ b/functions.php @@ -92,14 +92,12 @@ function calculateDaysAndNights($startDate, $endDate) function sendVerificationEmail($email, $name, $token) { - global $mailjet; - $message = [ 'Messages' => [ [ 'From' => [ - 'Email' => "info@4wdcsa.co.za", - 'Name' => "4WDCSA" + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], + 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ @@ -119,36 +117,33 @@ function sendVerificationEmail($email, $name, $token) ]; $client = new Client([ - // Base URI is used with relative requests 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, - 'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] + 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { - return True; + return true; } else { - return False; + return false; } } } function sendInvoice($email, $name, $eft_id, $amount, $description) { - global $mailjet; - $message = [ 'Messages' => [ [ 'From' => [ - 'Email' => "info@4wdcsa.co.za", - 'Name' => "4WDCSA" + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], + 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ @@ -169,22 +164,21 @@ function sendInvoice($email, $name, $eft_id, $amount, $description) ]; $client = new Client([ - // Base URI is used with relative requests 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, - 'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] + 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { - return True; + return true; } else { - return False; + return false; } } } @@ -209,14 +203,12 @@ function getEFTDetails($eft_id) { function sendPOP($fullname, $eft_id, $amount, $description) { - global $mailjet; - $message = [ 'Messages' => [ [ 'From' => [ - 'Email' => "info@4wdcsa.co.za", - 'Name' => "4WDCSA Web Admin" + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], + 'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin' ], 'To' => [ [ @@ -224,7 +216,7 @@ function sendPOP($fullname, $eft_id, $amount, $description) 'Name' => 'Chris Pinto' ], [ - 'Email' => 'info@4wdcsa.co.za', + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Name' => 'Jacqui Boshoff' ], [ @@ -246,36 +238,33 @@ function sendPOP($fullname, $eft_id, $amount, $description) ]; $client = new Client([ - // Base URI is used with relative requests 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, - 'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] + 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { - return True; + return true; } else { - return False; + return false; } } } function sendEmail($email, $subject, $message) { - global $mailjet; - - $message = [ + $messageData = [ 'Messages' => [ [ 'From' => [ - 'Email' => "info@4wdcsa.co.za", - 'Name' => "4WDCSA" + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], + 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ @@ -289,36 +278,33 @@ function sendEmail($email, $subject, $message) ]; $client = new Client([ - // Base URI is used with relative requests 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ - 'json' => $message, - 'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] + 'json' => $messageData, + 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { - return True; + return true; } else { - return False; + return false; } } } function sendAdminNotification($subject, $message) { - global $mailjet; - $mail = [ 'Messages' => [ [ 'From' => [ - 'Email' => "info@4wdcsa.co.za", - 'Name' => "4WDCSA" + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], + 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ @@ -337,36 +323,33 @@ function sendAdminNotification($subject, $message) ]; $client = new Client([ - // Base URI is used with relative requests 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $mail, - 'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] + 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { - return True; + return true; } else { - return False; + return false; } } } function sendPaymentConfirmation($email, $name, $description) { - global $mailjet; - $message = [ 'Messages' => [ [ 'From' => [ - 'Email' => "info@4wdcsa.co.za", - 'Name' => "4WDCSA" + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], + 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ @@ -386,22 +369,21 @@ function sendPaymentConfirmation($email, $name, $description) ]; $client = new Client([ - // Base URI is used with relative requests 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, - 'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] + 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { - return True; + return true; } else { - return False; + return false; } } } @@ -591,7 +573,7 @@ function processPayment($payment_id, $amount, $description) { $conn = openDatabaseConnection(); $status = "AWAITING PAYMENT"; - $domain = 'www.thepinto.co.za/4wdcsa'; + $domain = $_ENV['PAYFAST_DOMAIN']; $user_id = $_SESSION['user_id']; // Insert the order into the orders table $stmt = $conn->prepare(" @@ -625,7 +607,7 @@ function processPayment($payment_id, $amount, $description) * @param null $passPhrase * @return string */ - function generateSignature($data, $passPhrase = 'SheSells7Shells') + function generateSignature($data, $passPhrase = null) { // Create parameter string $pfOutput = ''; @@ -644,8 +626,8 @@ function processPayment($payment_id, $amount, $description) // Construct variables $data = array( // Merchant details - 'merchant_id' => '10021495', - 'merchant_key' => 'yzpdydo934j92', + 'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'], + 'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'], 'return_url' => 'https://' . $domain . '/bookings.php', 'cancel_url' => 'https://' . $domain . '/cancel_booking.php?booking_id=' . $encryptedId, 'notify_url' => 'https://' . $domain . '/confirm.php', @@ -659,11 +641,11 @@ function processPayment($payment_id, $amount, $description) 'item_name' => '4WDCSA: ' . $description // Describe the item(s) or use a generic description ); - $signature = generateSignature($data); // Assuming you have this function defined + $signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']); $data['signature'] = $signature; // Determine the PayFast URL based on the mode - $testingMode = true; + $testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true'; $pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; // Generate the HTML form with hidden inputs and an auto-submit script @@ -690,7 +672,7 @@ function processMembershipPayment($payment_id, $amount, $description) { $conn = openDatabaseConnection(); $status = "AWAITING PAYMENT"; - $domain = 'www.thepinto.co.za/4wdcsa'; + $domain = $_ENV['PAYFAST_DOMAIN']; $user_id = $_SESSION['user_id']; // Insert the order into the orders table $stmt = $conn->prepare(" @@ -724,7 +706,7 @@ function processMembershipPayment($payment_id, $amount, $description) * @param null $passPhrase * @return string */ - function generateSignature($data, $passPhrase = 'SheSells7Shells') + function generateSignature($data, $passPhrase = null) { // Create parameter string $pfOutput = ''; @@ -743,8 +725,8 @@ function processMembershipPayment($payment_id, $amount, $description) // Construct variables $data = array( // Merchant details - 'merchant_id' => '10021495', - 'merchant_key' => 'yzpdydo934j92', + 'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'], + 'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'], 'return_url' => 'https://' . $domain . '/account_settings.php', 'cancel_url' => 'https://' . $domain . '/cancel_application.php?id=' . $encryptedId, 'notify_url' => 'https://' . $domain . '/confirm2.php', @@ -758,11 +740,11 @@ function processMembershipPayment($payment_id, $amount, $description) 'item_name' => $description // Describe the item(s) or use a generic description ); - $signature = generateSignature($data); // Assuming you have this function defined + $signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']); $data['signature'] = $signature; // Determine the PayFast URL based on the mode - $testingMode = true; + $testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true'; $pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; // Generate the HTML form with hidden inputs and an auto-submit script @@ -1904,16 +1886,84 @@ function processLegacyMembership($user_id) { } } +/** + * SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection. + * ONLY call this function with whitelisted table and column names. + * NEVER accept table/column names directly from user input. + * + * Retrieves a single value from a database table. + * @param string $table Table name (MUST be whitelisted - see allowed_tables array) + * @param string $column Column name to retrieve (MUST be whitelisted - see allowed_columns array) + * @param string $match Column name for WHERE clause (MUST be whitelisted) + * @param mixed $identifier Value to match in WHERE clause (parameterized - safe) + * @return mixed The result value or null if not found + */ function getResultFromTable($table, $column, $match, $identifier) { + // WHITELIST: Define allowed tables to prevent table name injection + $allowed_tables = [ + 'users', + 'membership_application', + 'membership_fees', + 'bookings', + 'payments', + 'efts', + 'trips', + 'courses', + 'blogs', + 'events', + 'campsites', + 'bar_transactions', + 'login_attempts', + 'legacy_members' + ]; + + // WHITELIST: Define allowed columns per table (simplified - add more as needed) + $allowed_columns = [ + 'legacy_members' => ['amount', 'legacy_id', 'email', 'name'], + 'users' => ['user_id', 'email', 'first_name', 'last_name', 'phone_number', 'password', 'profile_pic', 'is_verified', 'type', 'locked_until'], + 'membership_fees' => ['payment_id', 'user_id', 'amount', 'payment_status', 'payment_date'], + 'bookings' => ['booking_id', 'user_id', 'total_amount', 'status', 'booking_type'], + 'payments' => ['payment_id', 'user_id', 'amount', 'status'], + 'trips' => ['trip_id', 'trip_name', 'description'], + 'courses' => ['course_id', 'course_name', 'description'], + 'blogs' => ['blog_id', 'title', 'content'], + 'events' => ['event_id', 'event_name', 'description'], + 'campsites' => ['campsite_id', 'name', 'location'], + 'efts' => ['eft_id', 'amount', 'status', 'booking_id'], + 'bar_transactions' => ['transaction_id', 'amount', 'date'], + 'login_attempts' => ['attempt_id', 'email', 'ip_address', 'success'] + ]; - $conn = openDatabaseConnection(); - $sql = "SELECT `$column` FROM `$table` WHERE `$match` = ?"; - $stmt = $conn->prepare($sql); - if (!$stmt) { + // Validate table name is in whitelist + if (!in_array($table, $allowed_tables, true)) { + error_log("Security Warning: getResultFromTable() called with non-whitelisted table: $table"); + return null; + } + + // Validate column name is in whitelist for this table + if (!isset($allowed_columns[$table]) || !in_array($column, $allowed_columns[$table], true)) { + error_log("Security Warning: getResultFromTable() called with non-whitelisted column: $column for table: $table"); + return null; + } + + // Validate match column is in whitelist for this table + if (!isset($allowed_columns[$table]) || !in_array($match, $allowed_columns[$table], true)) { + error_log("Security Warning: getResultFromTable() called with non-whitelisted match column: $match for table: $table"); return null; } - $stmt->bind_param('i', $identifier); + $conn = openDatabaseConnection(); + // Use backticks for table and column identifiers (safe after whitelist validation) + $sql = "SELECT `" . $column . "` FROM `" . $table . "` WHERE `" . $match . "` = ?"; + $stmt = $conn->prepare($sql); + if (!$stmt) { + error_log("Database prepare error: " . $conn->error); + return null; + } + + // Determine parameter type based on identifier + $paramType = is_int($identifier) ? 'i' : 's'; + $stmt->bind_param($paramType, $identifier); $stmt->execute(); $stmt->bind_result($result); $stmt->fetch(); @@ -1977,3 +2027,522 @@ function hasPhoneNumber($user_id) { // Return true only if a phone number exists and is not empty return !empty($phone_number); } + +// ==================== CSRF PROTECTION FUNCTIONS ==================== + +/** + * Generates a CSRF token and stores it in the session with expiration + * @param int $duration Token expiration time in seconds (default 3600 = 1 hour) + * @return string The generated CSRF token + */ +function generateCSRFToken($duration = 3600) { + // Initialize CSRF token storage in session if needed + if (!isset($_SESSION['csrf_tokens'])) { + $_SESSION['csrf_tokens'] = []; + } + + // Clean up expired tokens + cleanupExpiredTokens(); + + // Generate a random token + $token = bin2hex(random_bytes(32)); + + // Store token with expiration timestamp + $_SESSION['csrf_tokens'][$token] = time() + $duration; + + return $token; +} + +/** + * Validates a CSRF token from user input + * @param string $token The token to validate (typically from $_POST['csrf_token']) + * @return bool True if token is valid, false otherwise + */ +function validateCSRFToken($token) { + // Check if token exists in session + if (!isset($_SESSION['csrf_tokens']) || !isset($_SESSION['csrf_tokens'][$token])) { + return false; + } + + // Check if token has expired + if ($_SESSION['csrf_tokens'][$token] < time()) { + unset($_SESSION['csrf_tokens'][$token]); + return false; + } + + // Token is valid - remove it from session (single-use) + unset($_SESSION['csrf_tokens'][$token]); + + return true; +} + +/** + * Removes expired tokens from the session + */ +function cleanupExpiredTokens() { + if (!isset($_SESSION['csrf_tokens'])) { + return; + } + + $currentTime = time(); + foreach ($_SESSION['csrf_tokens'] as $token => $expiration) { + if ($expiration < $currentTime) { + unset($_SESSION['csrf_tokens'][$token]); + } + } +} + +// ==================== INPUT VALIDATION FUNCTIONS ==================== + +/** + * Validates and sanitizes email input + * @param string $email The email to validate + * @param int $maxLength Maximum allowed length (default 254 per RFC 5321) + * @return string|false Sanitized email or false if invalid + */ +function validateEmail($email, $maxLength = 254) { + // Check length + if (strlen($email) > $maxLength) { + return false; + } + + // Filter and validate + $filtered = filter_var($email, FILTER_VALIDATE_EMAIL); + + // Sanitize if valid + return $filtered ? filter_var($filtered, FILTER_SANITIZE_EMAIL) : false; +} + +/** + * Validates phone number format + * @param string $phone The phone number to validate + * @return string|false Sanitized phone number or false if invalid + */ +function validatePhoneNumber($phone) { + // Remove common formatting characters + $cleaned = preg_replace('/[^\d+\-\s().]/', '', $phone); + + // Check length (between 7 and 20 digits) + $digitCount = strlen(preg_replace('/[^\d]/', '', $cleaned)); + if ($digitCount < 7 || $digitCount > 20) { + return false; + } + + return $cleaned; +} + +/** + * Validates and sanitizes a name (first name, last name) + * @param string $name The name to validate + * @param int $minLength Minimum allowed length (default 2) + * @param int $maxLength Maximum allowed length (default 100) + * @return string|false Sanitized name or false if invalid + */ +function validateName($name, $minLength = 2, $maxLength = 100) { + // Trim whitespace + $name = trim($name); + + // Check length + if (strlen($name) < $minLength || strlen($name) > $maxLength) { + return false; + } + + // Only allow letters, spaces, hyphens, and apostrophes + if (!preg_match('/^[a-zA-Z\s\'-]+$/', $name)) { + return false; + } + + return htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); +} + +/** + * Validates a date string in YYYY-MM-DD format + * @param string $date The date string to validate + * @param string $format Expected date format (default 'Y-m-d') + * @return string|false Valid date string or false if invalid + */ +function validateDate($date, $format = 'Y-m-d') { + $d = DateTime::createFromFormat($format, $date); + + // Check if date is valid and in correct format + if (!$d || $d->format($format) !== $date) { + return false; + } + + return $date; +} + +/** + * Validates a numeric amount (for currency) + * @param mixed $amount The amount to validate + * @param float $min Minimum allowed amount (default 0) + * @param float $max Maximum allowed amount (default 999999.99) + * @return float|false Valid amount or false if invalid + */ +function validateAmount($amount, $min = 0, $max = 999999.99) { + // Try to convert to float + $value = filter_var($amount, FILTER_VALIDATE_FLOAT, [ + 'options' => [ + 'min_range' => $min, + 'max_range' => $max, + 'decimal' => '.' + ] + ]); + + // Must have at most 2 decimal places + if ($value !== false) { + $parts = explode('.', (string)$amount); + if (isset($parts[1]) && strlen($parts[1]) > 2) { + return false; + } + } + + return $value; +} + +/** + * Validates an integer within a range + * @param mixed $int The integer to validate + * @param int $min Minimum allowed value (default 0) + * @param int $max Maximum allowed value (default 2147483647) + * @return int|false Valid integer or false if invalid + */ +function validateInteger($int, $min = 0, $max = 2147483647) { + $value = filter_var($int, FILTER_VALIDATE_INT, [ + 'options' => [ + 'min_range' => $min, + 'max_range' => $max + ] + ]); + + return $value !== false ? $value : false; +} + +/** + * Validates South African ID number (13 digits) + * @param string $idNumber The ID number to validate + * @return string|false Valid ID number or false if invalid + */ +function validateSAIDNumber($idNumber) { + // Remove any whitespace + $idNumber = preg_replace('/\s/', '', $idNumber); + + // Must be exactly 13 digits + if (!preg_match('/^\d{13}$/', $idNumber)) { + return false; + } + + // Optional: Validate checksum (Luhn algorithm) + $sum = 0; + for ($i = 0; $i < 13; $i++) { + $digit = (int)$idNumber[$i]; + + // Double every even-positioned digit (0-indexed) + if ($i % 2 == 0) { + $digit *= 2; + if ($digit > 9) { + $digit -= 9; + } + } + + $sum += $digit; + } + + // Last digit should make sum divisible by 10 + if ($sum % 10 != 0) { + return false; + } + + return $idNumber; +} + +/** + * Sanitizes text input, removing potentially dangerous characters + * @param string $text The text to sanitize + * @param int $maxLength Maximum allowed length + * @return string Sanitized text + */ +function sanitizeTextInput($text, $maxLength = 1000) { + // Trim whitespace + $text = trim($text); + + // Limit length + $text = substr($text, 0, $maxLength); + + // Encode HTML special characters + return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); +} + +/** + * Validates file uploads for security + * @param array $file The $_FILES element to validate + * @param array $allowedTypes Array of allowed MIME types (e.g., ['image/jpeg', 'image/png', 'application/pdf']) + * @param int $maxSize Maximum file size in bytes + * @return string|false Sanitized filename or false if invalid + */ +function validateFileUpload($file, $allowedTypes = [], $maxSize = 5242880) { // 5MB default + // Check for upload errors + if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) { + return false; + } + + // Check file size + if (!isset($file['size']) || $file['size'] > $maxSize) { + return false; + } + + // Check MIME type + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + + if (!in_array($mimeType, $allowedTypes, true)) { + return false; + } + + // Generate random filename with original extension + $pathinfo = pathinfo($file['name']); + $extension = strtolower($pathinfo['extension']); + + // Whitelist allowed extensions + $allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'gif', 'webp']; + if (!in_array($extension, $allowedExtensions)) { + return false; + } + + // Generate random filename + $randomName = bin2hex(random_bytes(16)) . '.' . $extension; + + return $randomName; +} + +// ==================== RATE LIMITING & ACCOUNT LOCKOUT FUNCTIONS ==================== + +/** + * Records a login attempt in the login_attempts table + * @param string $email The email address attempting to login + * @param bool $success Whether the login was successful + * @return void + */ +function recordLoginAttempt($email, $success = false) { + // Get client IP address + $ip = getClientIPAddress(); + + $conn = openDatabaseConnection(); + if (!$conn) { + return; + } + + $email = strtolower(trim($email)); + + $sql = "INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, ?)"; + $stmt = $conn->prepare($sql); + if ($stmt) { + $success_int = $success ? 1 : 0; + $stmt->bind_param('ssi', $email, $ip, $success_int); + $stmt->execute(); + $stmt->close(); + } +} + +/** + * Gets the client's IP address safely + * @return string The client's IP address + */ +function getClientIPAddress() { + // Check for IP from share internet + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } + // Check for IP passed from proxy + elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; + } + // Check for remote IP + else { + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + } + + // Validate IP format + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + $ip = '0.0.0.0'; + } + + return $ip; +} + +/** + * Checks if an account is locked and returns lockout status + * @param string $email The email address to check + * @return array ['is_locked' => bool, 'locked_until' => datetime string or null, 'minutes_remaining' => int] + */ +function checkAccountLockout($email) { + $conn = openDatabaseConnection(); + if (!$conn) { + return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; + } + + $email = strtolower(trim($email)); + $now = date('Y-m-d H:i:s'); + + $sql = "SELECT locked_until FROM users WHERE email = ? LIMIT 1"; + $stmt = $conn->prepare($sql); + if (!$stmt) { + return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; + } + + $stmt->bind_param('s', $email); + $stmt->execute(); + $stmt->bind_result($locked_until); + $stmt->fetch(); + $stmt->close(); + + if ($locked_until === null) { + return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; + } + + if ($locked_until > $now) { + // Account is still locked + $lockTime = strtotime($locked_until); + $nowTime = strtotime($now); + $secondsRemaining = max(0, $lockTime - $nowTime); + $minutesRemaining = ceil($secondsRemaining / 60); + + return [ + 'is_locked' => true, + 'locked_until' => $locked_until, + 'minutes_remaining' => $minutesRemaining + ]; + } + + // Lockout has expired, clear it + $sql = "UPDATE users SET locked_until = NULL WHERE email = ?"; + $stmt = $conn->prepare($sql); + if ($stmt) { + $stmt->bind_param('s', $email); + $stmt->execute(); + $stmt->close(); + } + + return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; +} + +/** + * Counts recent failed login attempts for an email + IP combination + * @param string $email The email address + * @param int $minutesBack How many minutes back to check (default 15) + * @return int Number of failed attempts + */ +function countRecentFailedAttempts($email, $minutesBack = 15) { + $conn = openDatabaseConnection(); + if (!$conn) { + return 0; + } + + $email = strtolower(trim($email)); + $ip = getClientIPAddress(); + $cutoffTime = date('Y-m-d H:i:s', time() - ($minutesBack * 60)); + + $sql = "SELECT COUNT(*) as count FROM login_attempts + WHERE email = ? AND ip_address = ? AND success = 0 + AND attempted_at > ?"; + $stmt = $conn->prepare($sql); + if (!$stmt) { + return 0; + } + + $stmt->bind_param('sss', $email, $ip, $cutoffTime); + $stmt->execute(); + $stmt->bind_result($count); + $stmt->fetch(); + $stmt->close(); + + return (int)$count; +} + +/** + * Locks an account for a specified duration + * @param string $email The email address to lock + * @param int $minutes Duration of lockout in minutes (default 15) + * @return bool True if successful, false otherwise + */ +function lockAccount($email, $minutes = 15) { + $conn = openDatabaseConnection(); + if (!$conn) { + return false; + } + + $email = strtolower(trim($email)); + $lockUntil = date('Y-m-d H:i:s', time() + ($minutes * 60)); + + $sql = "UPDATE users SET locked_until = ? WHERE email = ?"; + $stmt = $conn->prepare($sql); + if (!$stmt) { + return false; + } + + $stmt->bind_param('ss', $lockUntil, $email); + $result = $stmt->execute(); + $stmt->close(); + + return $result; +} + +/** + * Unlocks an account (admin function) + * @param string $email The email address to unlock + * @return bool True if successful, false otherwise + */ +function unlockAccount($email) { + $conn = openDatabaseConnection(); + if (!$conn) { + return false; + } + + $email = strtolower(trim($email)); + + $sql = "UPDATE users SET locked_until = NULL WHERE email = ?"; + $stmt = $conn->prepare($sql); + if (!$stmt) { + return false; + } + + $stmt->bind_param('s', $email); + $result = $stmt->execute(); + $stmt->close(); + + return $result; +} + +/** + * Logs an action to the audit log table + * @param int $user_id User ID (null if not authenticated) + * @param string $action Action name (e.g., 'LOGIN', 'FAILED_LOGIN', 'ACCOUNT_LOCKED') + * @param string $resource_type Resource type being affected + * @param int $resource_id Resource ID being affected + * @param array $details Additional details about the action + * @return bool True if successful + */ +function auditLog($user_id, $action, $resource_type = null, $resource_id = null, $details = null) { + $conn = openDatabaseConnection(); + if (!$conn) { + return false; + } + + $ip = getClientIPAddress(); + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $detailsJson = $details ? json_encode($details) : null; + + $sql = "INSERT INTO audit_log (user_id, action, resource_type, resource_id, ip_address, user_agent, details) + VALUES (?, ?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($sql); + if (!$stmt) { + return false; + } + + $stmt->bind_param('issdsss', $user_id, $action, $resource_type, $resource_id, $ip, $userAgent, $detailsJson); + $result = $stmt->execute(); + $stmt->close(); + + return $result; +} diff --git a/migrations/001_phase1_security_schema.sql b/migrations/001_phase1_security_schema.sql new file mode 100644 index 00000000..a23a20d5 --- /dev/null +++ b/migrations/001_phase1_security_schema.sql @@ -0,0 +1,47 @@ +-- ============================================================================ +-- MIGRATION: Phase 1 Security & Stability Database Schema Updates +-- Date: 2025-12-03 +-- Description: Add tables and columns required for Phase 1 security features +-- (login rate limiting, account lockout, audit logging) +-- ============================================================================ + +-- Track failed login attempts for rate limiting and account lockout +CREATE TABLE IF NOT EXISTS login_attempts ( + attempt_id BIGINT PRIMARY KEY AUTO_INCREMENT, + email VARCHAR(255) NOT NULL, + ip_address VARCHAR(45) NOT NULL, + attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + success BOOLEAN DEFAULT FALSE, + INDEX idx_email_ip (email, ip_address), + INDEX idx_attempted_at (attempted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Add account lockout column to users table +-- Stores the timestamp until which the account is locked +-- NULL = account not locked, Future datetime = account locked until this time +ALTER TABLE users ADD COLUMN locked_until DATETIME NULL DEFAULT NULL AFTER is_verified; +CREATE INDEX idx_locked_until ON users (locked_until); + +-- Security audit log for sensitive operations +DROP TABLE IF EXISTS `audit_log`; +CREATE TABLE IF NOT EXISTS audit_log ( + log_id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id INT NULL, + action VARCHAR(100) NOT NULL COMMENT 'e.g., LOGIN, FAILED_LOGIN, ACCOUNT_LOCKED, FILE_UPLOAD', + resource_type VARCHAR(50) COMMENT 'e.g., users, bookings, payments', + resource_id INT COMMENT 'ID of affected resource', + ip_address VARCHAR(45) NOT NULL, + user_agent TEXT NULL, + details TEXT COMMENT 'JSON or text details about the action', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_action (action), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================================ +-- ROLLBACK INSTRUCTIONS (if needed) +-- ============================================================================ +-- ALTER TABLE users DROP COLUMN locked_until; +-- DROP TABLE IF EXISTS login_attempts; +-- DROP TABLE IF EXISTS audit_log; diff --git a/process_application.php b/process_application.php index 2c8cbdf3..7ae7ccfb 100644 --- a/process_application.php +++ b/process_application.php @@ -10,24 +10,53 @@ $status = 'AWAITING PAYMENT'; $description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id); if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'membership_application', null, ['endpoint' => 'process_application.php']); + http_response_code(403); + die('Security token validation failed. Please try again.'); + } - // Get all the form fields - $first_name = $_POST['first_name']; - $last_name = $_POST['last_name']; - $id_number = $_POST['id_number']; - $dob = $_POST['dob']; - $occupation = $_POST['occupation']; - $tel_cell = $_POST['tel_cell']; - $email = $_POST['email']; + // Get all the form fields with validation + $first_name = validateName($_POST['first_name'] ?? ''); + if ($first_name === false) { + die('Invalid first name format.'); + } + + $last_name = validateName($_POST['last_name'] ?? ''); + if ($last_name === false) { + die('Invalid last name format.'); + } + + $id_number = validateSAIDNumber($_POST['id_number'] ?? ''); + if ($id_number === false) { + die('Invalid ID number format.'); + } + + $dob = validateDate($_POST['dob'] ?? ''); + if ($dob === false) { + die('Invalid date of birth format.'); + } + + $occupation = sanitizeTextInput($_POST['occupation'] ?? '', 100); + $tel_cell = validatePhoneNumber($_POST['tel_cell'] ?? ''); + if ($tel_cell === false) { + die('Invalid phone number format.'); + } + + $email = validateEmail($_POST['email'] ?? ''); + if ($email === false) { + die('Invalid email format.'); + } // Spouse or Partner details (optional) - $spouse_first_name = !empty($_POST['spouse_first_name']) ? $_POST['spouse_first_name'] : null; - $spouse_last_name = !empty($_POST['spouse_last_name']) ? $_POST['spouse_last_name'] : null; - $spouse_id_number = !empty($_POST['spouse_id_number']) ? $_POST['spouse_id_number'] : null; - $spouse_dob = !empty($_POST['spouse_dob']) ? $_POST['spouse_dob'] : NULL; // if empty, set to NULL - $spouse_occupation = !empty($_POST['spouse_occupation']) ? $_POST['spouse_occupation'] : null; - $spouse_tel_cell = !empty($_POST['spouse_tel_cell']) ? $_POST['spouse_tel_cell'] : null; - $spouse_email = !empty($_POST['spouse_email']) ? $_POST['spouse_email'] : null; + $spouse_first_name = !empty($_POST['spouse_first_name']) ? validateName($_POST['spouse_first_name']) : null; + $spouse_last_name = !empty($_POST['spouse_last_name']) ? validateName($_POST['spouse_last_name']) : null; + $spouse_id_number = !empty($_POST['spouse_id_number']) ? validateSAIDNumber($_POST['spouse_id_number']) : null; + $spouse_dob = !empty($_POST['spouse_dob']) ? validateDate($_POST['spouse_dob']) : NULL; + $spouse_occupation = !empty($_POST['spouse_occupation']) ? sanitizeTextInput($_POST['spouse_occupation'], 100) : null; + $spouse_tel_cell = !empty($_POST['spouse_tel_cell']) ? validatePhoneNumber($_POST['spouse_tel_cell']) : null; + $spouse_email = !empty($_POST['spouse_email']) ? validateEmail($_POST['spouse_email']) : null; // Children details (optional) $child_name1 = !empty($_POST['child_name1']) ? $_POST['child_name1'] : null; diff --git a/process_booking.php b/process_booking.php index 3c2b9494..a8e99dc9 100644 --- a/process_booking.php +++ b/process_booking.php @@ -11,12 +11,45 @@ $user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null; // Check if the form has been submitted if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_booking.php']); + echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']); + exit(); + } + + // Validate dates and integers + $from_date = validateDate($_POST['from_date'] ?? ''); + if ($from_date === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid from date format.']); + exit(); + } + + $to_date = validateDate($_POST['to_date'] ?? ''); + if ($to_date === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid to date format.']); + exit(); + } + + $num_vehicles = validateInteger($_POST['vehicles'] ?? 0, 1, 10); + if ($num_vehicles === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid number of vehicles.']); + exit(); + } + + $num_adults = validateInteger($_POST['adults'] ?? 0, 0, 20); + if ($num_adults === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid number of adults.']); + exit(); + } + + $num_children = validateInteger($_POST['children'] ?? 0, 0, 20); + if ($num_children === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid number of children.']); + exit(); + } + // Get values from the form - $from_date = $_POST['from_date']; - $to_date = $_POST['to_date']; - $num_vehicles = (int)$_POST['vehicles']; - $num_adults = (int)$_POST['adults']; - $num_children = (int)$_POST['children']; $add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras $is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status $type = "camping"; diff --git a/process_camp_booking.php b/process_camp_booking.php index 00ad56e7..e5aea071 100644 --- a/process_camp_booking.php +++ b/process_camp_booking.php @@ -18,12 +18,45 @@ $is_member = getUserMemberStatus($user_id); // Check if the form has been submitted if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_camp_booking.php']); + echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']); + exit(); + } + + // Validate dates and integers + $from_date = validateDate($_POST['from_date'] ?? ''); + if ($from_date === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid from date format.']); + exit(); + } + + $to_date = validateDate($_POST['to_date'] ?? ''); + if ($to_date === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid to date format.']); + exit(); + } + + $num_vehicles = validateInteger($_POST['vehicles'] ?? 1, 1, 10); + if ($num_vehicles === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid number of vehicles.']); + exit(); + } + + $num_adults = validateInteger($_POST['adults'] ?? 0, 0, 20); + if ($num_adults === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid number of adults.']); + exit(); + } + + $num_children = validateInteger($_POST['children'] ?? 0, 0, 20); + if ($num_children === false) { + echo json_encode(['status' => 'error', 'message' => 'Invalid number of children.']); + exit(); + } + // Get values from the form - $from_date = $_POST['from_date']; - $to_date = $_POST['to_date']; - $num_vehicles = (int)$_POST['vehicles']; - $num_adults = (int)$_POST['adults']; - $num_children = (int)$_POST['children']; $add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras // $is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status $type = "camping"; diff --git a/process_course_booking.php b/process_course_booking.php index d7a56275..f3f6507a 100644 --- a/process_course_booking.php +++ b/process_course_booking.php @@ -18,10 +18,30 @@ $pending_member = getUserMemberStatusPending($user_id); // Check if the form has been submitted if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_course_booking.php']); + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Security token validation failed.']); + exit(); + } + // Input variables from the form (use default values if not provided) - $additional_members = isset($_POST['members']) ? intval($_POST['members']) : 0; // Default to 1 vehicle - $num_adults = isset($_POST['non-members']) ? intval($_POST['non-members']) : 0; // Default to 1 adult - $course_id = isset($_POST['course_id']) ? intval($_POST['course_id']) : 0; // Default to 0 children + $additional_members = validateInteger($_POST['members'] ?? 0, 0, 20); + if ($additional_members === false) $additional_members = 0; + + $num_adults = validateInteger($_POST['non-members'] ?? 0, 0, 20); + if ($num_adults === false) $num_adults = 0; + + $course_id = validateInteger($_POST['course_id'] ?? 0, 1, 999999); + if ($course_id === false) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Invalid course ID.']); + exit(); + } + checkAndRedirectCourseBooking($course_id); // Fetch trip costs from the database $query = "SELECT date, cost_members, cost_nonmembers, course_type FROM courses WHERE course_id = ?"; diff --git a/process_eft.php b/process_eft.php index cc165f9a..de2abba5 100644 --- a/process_eft.php +++ b/process_eft.php @@ -4,9 +4,20 @@ require_once("session.php"); require_once("connection.php"); require_once("functions.php"); checkAdmin(); + +// CSRF Token Validation for POST requests +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog($_SESSION['user_id'] ?? null, 'CSRF_VALIDATION_FAILED', 'efts', null, ['endpoint' => 'process_eft.php']); + http_response_code(403); + die('Security token validation failed.'); + } +} + if (!isset($_GET['token']) || empty($_GET['token'])) { die("Invalid request."); } + $token = $_GET['token']; // echo $token; $eft_id = decryptData($token, $salt); diff --git a/process_signature.php b/process_signature.php index 1e8502c4..482c45e6 100644 --- a/process_signature.php +++ b/process_signature.php @@ -9,6 +9,12 @@ if (!isset($_SESSION['user_id'])) { } if (isset($_POST['signature'])) { + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog($_SESSION['user_id'], 'CSRF_VALIDATION_FAILED', 'membership_application', null, ['endpoint' => 'process_signature.php']); + die(json_encode(['status' => 'error', 'message' => 'Security token validation failed'])); + } + $user_id = $_SESSION['user_id']; // Get the user ID from the session $signature = $_POST['signature']; // Base64 image data diff --git a/process_trip_booking.php b/process_trip_booking.php index 90db52d9..0d96f689 100644 --- a/process_trip_booking.php +++ b/process_trip_booking.php @@ -30,12 +30,27 @@ $is_member = getUserMemberStatus($user_id); // Check if the form has been submitted if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_trip_booking.php']); + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Security token validation failed.']); + exit(); + } + // Input variables from the form (use default values if not provided) - $num_vehicles = isset($_POST['vehicles']) ? intval($_POST['vehicles']) : 1; // Default to 1 vehicle - $num_adults = isset($_POST['adults']) ? intval($_POST['adults']) : 1; // Default to 1 adult - $num_children = isset($_POST['children']) ? intval($_POST['children']) : 0; // Default to 0 children - $num_pensioners = isset($_POST['pensioners']) ? intval($_POST['pensioners']) : 0; // Default to 0 pensioners - // $radio = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras + $num_vehicles = validateInteger($_POST['vehicles'] ?? 1, 1, 10); + if ($num_vehicles === false) $num_vehicles = 1; + + $num_adults = validateInteger($_POST['adults'] ?? 1, 1, 20); + if ($num_adults === false) $num_adults = 1; + + $num_children = validateInteger($_POST['children'] ?? 0, 0, 20); + if ($num_children === false) $num_children = 0; + + $num_pensioners = validateInteger($_POST['pensioners'] ?? 0, 0, 20); + if ($num_pensioners === false) $num_pensioners = 0; // Fetch trip costs from the database $query = "SELECT trip_name, cost_members, cost_nonmembers, cost_pensioner_member, cost_pensioner, booking_fee, start_date, end_date, trip_code FROM trips WHERE trip_id = ?"; $stmt = $conn->prepare($query); diff --git a/register_user.php b/register_user.php index 4ec2b945..a98671dd 100644 --- a/register_user.php +++ b/register_user.php @@ -1,13 +1,12 @@ connect_error) { // Form processing if ($_SERVER['REQUEST_METHOD'] === 'POST') { - // Sanitize and validate input - $first_name = ucwords(strtolower($conn->real_escape_string($_POST['first_name']))); - $last_name = ucwords(strtolower($conn->real_escape_string($_POST['last_name']))); - $phone_number = $conn->real_escape_string($_POST['phone_number']); - $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL); - $password = $_POST['password']; - $password_confirm = $_POST['password_confirm']; - $name = $first_name . " " . $last_name; - - // Basic validation - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog(null, 'CSRF_VALIDATION_FAILED', 'users', null, ['endpoint' => 'register_user.php']); + echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']); + exit(); + } + + // Check rate limiting on registration endpoint (by IP) + $ip = getClientIPAddress(); + $cutoffTime = date('Y-m-d H:i:s', time() - (3600)); // Last hour + + $stmt = $conn->prepare("SELECT COUNT(*) as count FROM audit_log WHERE action = 'REGISTRATION_ATTEMPT' AND ip_address = ? AND created_at > ?"); + $stmt->bind_param('ss', $ip, $cutoffTime); + $stmt->execute(); + $stmt->bind_result($regAttempts); + $stmt->fetch(); + $stmt->close(); + + // Allow max 5 registration attempts per IP per hour + if ($regAttempts >= 5) { + auditLog(null, 'REGISTRATION_RATE_LIMIT_EXCEEDED', 'users', null, ['ip' => $ip, 'attempts' => $regAttempts]); + echo json_encode(['status' => 'error', 'message' => 'Too many registration attempts. Please try again later.']); + exit(); + } + + // Validate and sanitize first name + $first_name = validateName($_POST['first_name'] ?? ''); + if ($first_name === false) { + auditLog(null, 'REGISTRATION_INVALID_FIRST_NAME', 'users'); + echo json_encode(['status' => 'error', 'message' => 'Invalid first name. Only letters, spaces, hyphens, and apostrophes allowed (2-100 characters).']); + exit(); + } + + // Validate and sanitize last name + $last_name = validateName($_POST['last_name'] ?? ''); + if ($last_name === false) { + auditLog(null, 'REGISTRATION_INVALID_LAST_NAME', 'users'); + echo json_encode(['status' => 'error', 'message' => 'Invalid last name. Only letters, spaces, hyphens, and apostrophes allowed (2-100 characters).']); + exit(); + } + + // Validate and sanitize phone number + $phone_number = validatePhoneNumber($_POST['phone_number'] ?? ''); + if ($phone_number === false) { + auditLog(null, 'REGISTRATION_INVALID_PHONE', 'users'); + echo json_encode(['status' => 'error', 'message' => 'Invalid phone number format.']); + exit(); + } + + // Validate email + $email = validateEmail($_POST['email'] ?? ''); + if ($email === false) { + auditLog(null, 'REGISTRATION_INVALID_EMAIL', 'users'); echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']); exit(); } + + $password = $_POST['password'] ?? ''; + $password_confirm = $_POST['password_confirm'] ?? ''; + + // Validate password strength (minimum 8 characters, must contain uppercase, lowercase, number, special char) + if (strlen($password) < 8) { + echo json_encode(['status' => 'error', 'message' => 'Password must be at least 8 characters long.']); + exit(); + } + + if (!preg_match('/[A-Z]/', $password) || !preg_match('/[a-z]/', $password) || + !preg_match('/[0-9]/', $password) || !preg_match('/[!@#$%^&*]/', $password)) { + echo json_encode(['status' => 'error', 'message' => 'Password must contain uppercase, lowercase, number, and special character (!@#$%^&*).']); + exit(); + } + if ($password !== $password_confirm) { echo json_encode(['status' => 'error', 'message' => 'Passwords do not match.']); exit(); @@ -45,6 +102,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt->store_result(); if ($stmt->num_rows > 0) { + auditLog(null, 'REGISTRATION_EMAIL_EXISTS', 'users', null, ['email' => $email]); echo json_encode(['status' => 'error', 'message' => 'Email is already registered.']); $stmt->close(); $conn->close(); @@ -56,7 +114,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Hash password $hashed_password = password_hash($password, PASSWORD_BCRYPT); - // Generate token + // Generate email verification token $token = bin2hex(random_bytes(50)); // Prepare and execute query @@ -68,14 +126,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($stmt->execute()) { $newUser_id = $conn->insert_id; processLegacyMembership($newUser_id); - if (sendVerificationEmail($email, $name, $token)) { - sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name . ' has just created an account using Credentials.'); + auditLog($newUser_id, 'USER_REGISTRATION', 'users', $newUser_id, ['email' => $email]); + + if (sendVerificationEmail($email, $first_name . ' ' . $last_name, $token)) { + sendEmail($_ENV['ADMIN_EMAIL'], '4WDCSA: New User Registration', $first_name . ' ' . $last_name . ' (' . $email . ') has just created an account using Credentials.'); echo json_encode(['status' => 'success', 'message' => 'Registration successful. Please check your email to verify your account.']); } else { echo json_encode(['status' => 'error', 'message' => 'Failed to send verification email.']); } } else { - echo json_encode(['status' => 'error', 'message' => 'Failed to register user: ' . $stmt->error]); + auditLog(null, 'REGISTRATION_DATABASE_ERROR', 'users', null, ['email' => $email]); + echo json_encode(['status' => 'error', 'message' => 'Failed to register user.']); } $stmt->close(); diff --git a/run_migration.php b/run_migration.php new file mode 100644 index 00000000..36ac85b6 --- /dev/null +++ b/run_migration.php @@ -0,0 +1,20 @@ +multi_query($sql)) { + echo "✓ Migration executed successfully\n"; +} else { + echo "✗ Migration error: " . $conn->error . "\n"; +} + +$conn->close(); +?> diff --git a/validate_login.php b/validate_login.php index 3db87373..9c06bd38 100644 --- a/validate_login.php +++ b/validate_login.php @@ -13,8 +13,8 @@ if (!$conn) { // Google Client Setup $client = new Google_Client(); -$client->setClientId('948441222188-8qhboq2urr8o9n35mc70s5h2nhd52v0m.apps.googleusercontent.com'); -$client->setClientSecret('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'); +$client->setClientId($_ENV['GOOGLE_CLIENT_ID']); +$client->setClientSecret($_ENV['GOOGLE_CLIENT_SECRET']); $client->setRedirectUri($_ENV['HOST'] . '/validate_login.php'); $client->addScope("email"); $client->addScope("profile"); @@ -86,18 +86,57 @@ if (isset($_GET['code'])) { // Check if email and password login is requested if (isset($_POST['email']) && isset($_POST['password'])) { - // Retrieve and sanitize form data - $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL); - $password = trim($_POST['password']); // Remove extra spaces - - // Validate input + // CSRF Token Validation + if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + auditLog(null, 'CSRF_VALIDATION_FAILED', 'users', null, ['endpoint' => 'validate_login.php']); + echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']); + exit(); + } + + // Retrieve and validate email input + $email = validateEmail($_POST['email']); + if ($email === false) { + auditLog(null, 'INVALID_EMAIL_FORMAT', 'users', null, ['email' => $_POST['email']]); + echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']); + exit(); + } + + // Retrieve and sanitize password + $password = isset($_POST['password']) ? trim($_POST['password']) : ''; + + // Basic validation if (empty($email) || empty($password)) { echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']); exit(); } - - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']); + + // Check for account lockout + $lockoutStatus = checkAccountLockout($email); + if ($lockoutStatus['is_locked']) { + auditLog(null, 'LOGIN_ATTEMPT_LOCKED_ACCOUNT', 'users', null, [ + 'email' => $email, + 'locked_until' => $lockoutStatus['locked_until'] + ]); + echo json_encode([ + 'status' => 'error', + 'message' => 'Account is temporarily locked due to multiple failed login attempts. Please try again in ' . $lockoutStatus['minutes_remaining'] . ' minutes.' + ]); + exit(); + } + + // Check recent failed attempts + $recentFailedAttempts = countRecentFailedAttempts($email); + if ($recentFailedAttempts >= 5) { + // Lock account for 15 minutes + lockAccount($email, 15); + auditLog(null, 'ACCOUNT_LOCKED_THRESHOLD', 'users', null, [ + 'email' => $email, + 'failed_attempts' => $recentFailedAttempts + ]); + echo json_encode([ + 'status' => 'error', + 'message' => 'Account locked due to multiple failed login attempts. Please try again in 15 minutes.' + ]); exit(); } @@ -120,22 +159,55 @@ if (isset($_POST['email']) && isset($_POST['password'])) { // Check if the user is verified if ($row['is_verified'] == 0) { + recordLoginAttempt($email, false); + auditLog(null, 'LOGIN_ATTEMPT_UNVERIFIED_ACCOUNT', 'users', $row['user_id']); echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']); exit(); } if (password_verify($password, $row['password'])) { + // Record successful attempt + recordLoginAttempt($email, true); + + // Regenerate session ID to prevent session fixation attacks + session_regenerate_id(true); + // Password is correct, set up session - $_SESSION['user_id'] = $row['user_id']; // Adjust as per your table structure - $_SESSION['first_name'] = $row['first_name']; // Adjust as per your table structure + $_SESSION['user_id'] = $row['user_id']; + $_SESSION['first_name'] = $row['first_name']; $_SESSION['profile_pic'] = $row['profile_pic']; + + // Set session timeout (30 minutes) + $_SESSION['login_time'] = time(); + $_SESSION['session_timeout'] = 1800; // 30 minutes in seconds + + auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']); echo json_encode(['status' => 'success', 'message' => 'Successful Login']); } else { - // Password is incorrect - echo json_encode(['status' => 'error', 'message' => 'Invalid password.']); + // Password is incorrect - record failed attempt + recordLoginAttempt($email, false); + auditLog(null, 'LOGIN_FAILED_INVALID_PASSWORD', 'users', null, ['email' => $email]); + + // Check if this was the threshold failure + $newFailureCount = countRecentFailedAttempts($email); + if ($newFailureCount >= 5) { + lockAccount($email, 15); + echo json_encode([ + 'status' => 'error', + 'message' => 'Too many failed login attempts. Account locked for 15 minutes.' + ]); + } else { + $attemptsRemaining = 5 - $newFailureCount; + echo json_encode([ + 'status' => 'error', + 'message' => 'Invalid password. ' . $attemptsRemaining . ' attempts remaining before account lockout.' + ]); + } } } else { - // User does not exist + // User does not exist - still record attempt + recordLoginAttempt($email, false); + auditLog(null, 'LOGIN_FAILED_USER_NOT_FOUND', 'users', null, ['email' => $email]); echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']); }